1 /** 2 Copyright: Copyright (c) 2016, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 */ 6 module dsrcgen.plantuml; 7 8 import std.format : format; 9 import std.meta : AliasSeq, staticIndexOf; 10 import std.traits : ReturnType; 11 import std.typecons : Flag, Yes, No, Typedef, Tuple; 12 13 import dsrcgen.base; 14 15 version (Have_unit_threaded) { 16 import unit_threaded : Name, shouldEqual; 17 } else { 18 private struct Name { 19 string n; 20 } 21 22 /// Fallback when unit_threaded doon't exist. 23 private void shouldEqual(T0, T1)(T0 value, T1 expect) { 24 assert(value == expect, value); 25 } 26 } 27 28 @safe: 29 30 /** A plantuml comment using ''' as is. 31 * 32 * Compared to Text a comment is affected by indentation. 33 */ 34 class Comment : BaseModule { 35 mixin Attrs; 36 37 private string contents; 38 39 /// Construct a one-liner comment from contents. 40 this(string contents) { 41 this.contents = contents; 42 } 43 44 override string renderIndent(int parent_level, int level) { 45 if ("begin" in attrs) { 46 return indent(attrs["begin"] ~ contents, parent_level, level); 47 } 48 49 return indent("' " ~ contents, parent_level, level); 50 } 51 } 52 53 /** Affected by attribute end. 54 * stmt ~ end 55 * <recursive> 56 */ 57 class Stmt(T) : T { 58 private string headline; 59 60 /// 61 this(string headline) { 62 this.headline = headline; 63 } 64 65 /// See_Also: BaseModule 66 override string renderIndent(int parent_level, int level) { 67 auto end = "end" in attrs; 68 string r = headline ~ (end is null ? "" : *end); 69 70 if ("noindent" !in attrs) { 71 r = indent(r, parent_level, level); 72 } 73 74 return r; 75 } 76 } 77 78 /** A plantuml block. 79 * 80 * Affected by attribute begin, end, noindent. 81 * headline ~ begin 82 * <recursive> 83 * end 84 * noindent affects post_recursive. If set no indention there. 85 * r.length > 0 catches the case when begin or end is empty string. Used in switch/case. 86 */ 87 class Suite(T) : T { 88 private string headline; 89 90 /// 91 this(string headline) { 92 this.headline = headline; 93 } 94 95 override string renderIndent(int parent_level, int level) { 96 import std.ascii : newline; 97 98 string r; 99 if (auto begin = "begin" in attrs) { 100 r = headline ~ *begin; 101 } else { 102 r = headline ~ " {" ~ newline; 103 } 104 105 if (r.length > 0 && "noindent" !in attrs) { 106 r = indent(r, parent_level, level); 107 } 108 return r; 109 } 110 111 override string renderPostRecursive(int parent_level, int level) { 112 string r = "}"; 113 if (auto end = "end" in attrs) { 114 r = *end; 115 } 116 117 if (r.length > 0 && "noindent" !in attrs) { 118 r = indent(r, parent_level, level); 119 } 120 return r; 121 } 122 } 123 124 enum Relate { 125 WeakRelate, 126 Relate, 127 Compose, 128 Aggregate, 129 Extend, 130 ArrowTo, 131 AggregateArrowTo, 132 DotArrowTo 133 } 134 135 /// Converter for enum Relate to plantuml syntax. 136 private string relateToString(Relate relate) { 137 string r_type; 138 final switch (relate) with (Relate) { 139 case WeakRelate: 140 r_type = ".."; 141 break; 142 case Relate: 143 r_type = "--"; 144 break; 145 case Compose: 146 r_type = "o--"; 147 break; 148 case Aggregate: 149 r_type = "*--"; 150 break; 151 case Extend: 152 r_type = "--|>"; 153 break; 154 case ArrowTo: 155 r_type = "-->"; 156 break; 157 case AggregateArrowTo: 158 r_type = "*-->"; 159 break; 160 case DotArrowTo: 161 r_type = "->"; 162 break; 163 } 164 165 return r_type; 166 } 167 168 enum LabelPos { 169 Left, 170 Right, 171 OnRelation 172 } 173 174 alias ClassModuleType = Typedef!(PlantumlModule, null, "ClassModuleType"); 175 alias ClassAsType = Typedef!(Text!PlantumlModule, null, "ComponentAsType"); 176 alias ClassSpotType = Typedef!(PlantumlModule, null, "ClassSpotType"); 177 alias ClassNameType = Typedef!(string, string.init, "ClassNameType"); 178 alias ClassType = Tuple!(ClassNameType, "name", ClassModuleType, "m", 179 ClassSpotType, "spot", ClassAsType, "as"); 180 181 alias ComponentModuleType = Typedef!(PlantumlModule, null, "ComponentModuleType"); 182 alias ComponentAsType = Typedef!(Text!PlantumlModule, null, "ComponentAsType"); 183 alias ComponentNameType = Typedef!(string, string.init, "ComponentNameType"); 184 alias ComponentType = Tuple!(ComponentNameType, "name", ComponentModuleType, 185 "m", ComponentAsType, "as"); 186 187 alias NoteType = Typedef!(PlantumlModule, null, "NoteType"); 188 189 alias RelationType = Typedef!(ReturnType!(PlantumlModule.stmt), 190 ReturnType!(PlantumlModule.stmt).init, "RelationType"); 191 192 /** A relation in plantuml has three main positions that can be modified. 193 * 194 * Block 195 * left middle right 196 */ 197 private mixin template RelateTypes(Tleft, Tright, Trel, Tblock) { 198 alias RelateLeft = Typedef!(Tleft, Tleft.init, "RelateLeft"); 199 alias RelateRight = Typedef!(Tright, Tright.init, "RelateRight"); 200 alias RelateMiddle = Typedef!(Trel, Trel.init, "RelateMiddle"); 201 alias RelateBlock = Typedef!(Tblock, Tblock.init, "RelationBlock"); 202 alias Relation = Tuple!(RelateLeft, "left", RelateRight, "right", 203 RelateMiddle, "rel", RelateBlock, "block"); 204 } 205 206 mixin RelateTypes!(Text!PlantumlModule, Text!PlantumlModule, 207 Text!PlantumlModule, PlantumlModule); 208 209 // Types that can be related between each other 210 alias CanRelateSeq = AliasSeq!(ClassNameType, ComponentNameType); 211 enum CanRelate(T) = staticIndexOf!(T, CanRelateSeq) >= 0; 212 213 private mixin template PlantumlBase() { 214 /** Access to self. 215 * 216 * Useful in with-statements. 217 */ 218 auto _() { 219 return this; 220 } 221 222 /** An empty node holdig other nodes. 223 * 224 * Not affected by indentation. 225 */ 226 auto empty() { 227 auto e = new Empty!(typeof(this)); 228 append(e); 229 return e; 230 } 231 232 /** Make a Comment followed by a separator. 233 * 234 * Affected by indentation. 235 * 236 * TODO should have an addSep like stmt have. 237 */ 238 auto comment(string comment) { 239 auto e = new Comment(comment); 240 e.sep; 241 append(e); 242 return e; 243 } 244 245 /** Make a raw Text. 246 * 247 * Note it is intentional that the text object do NOT have a separator. It 248 * is to allow detailed "surgical" insertion of raw text/data when no 249 * semantical "helpers" exist for a specific use case. 250 */ 251 auto text(string content) pure { 252 auto e = new Text!(typeof(this))(content); 253 append(e); 254 return e; 255 } 256 257 /** A basic building block with no content. 258 * 259 * Useful when a "node" is needed to add further content in. 260 * The node is affected by indentation. 261 */ 262 auto base() { 263 auto e = new typeof(this); 264 append(e); 265 return e; 266 } 267 268 /** Make a statement with an optional separator. 269 * 270 * A statement is commonly an individual item or at the most a line. 271 * 272 * Params: 273 * stmt_ = raw text to use as the statement 274 * separator = flag determining if a separator is added 275 * 276 * Returns: Stmt instance stored in this. 277 */ 278 auto stmt(string stmt_, Flag!"addSep" separator = Yes.addSep) { 279 auto e = new Stmt!(typeof(this))(stmt_); 280 append(e); 281 if (separator) { 282 sep(); 283 } 284 return e; 285 } 286 287 /** Make a suite/block as a child of "this" with an optional separator. 288 * 289 * The separator is inserted after the block. 290 * 291 * Returns: Suite instance stored in this. 292 */ 293 auto suite(string headline, Flag!"addSep" separator = Yes.addSep) { 294 auto e = new Suite!(typeof(this))(headline); 295 append(e); 296 if (separator) { 297 sep(); 298 } 299 return e; 300 } 301 302 } 303 304 /** Semantic representation in D of PlantUML elements. 305 * 306 * Design: 307 * All created instances are stored internally. 308 * The returned instances is thus to allow the user to further manipulate or 309 * add nesting content. 310 */ 311 class PlantumlModule : BaseModule { 312 mixin Attrs; 313 mixin PlantumlBase; 314 315 this() pure { 316 super(); 317 } 318 319 /** Make a UML class without any content. 320 * 321 * Return: A tuple allowing further modification. 322 */ 323 ClassType class_(string name) { 324 auto e = stmt(format(`class "%s"`, name)); 325 auto as = e.text(""); 326 auto spot = as.text(""); 327 328 return ClassType(ClassNameType(name), ClassModuleType(e), 329 ClassSpotType(spot), ClassAsType(as)); 330 } 331 332 /** Make a UML component without any content. 333 * 334 * Return: A tuple allowing further modification. 335 */ 336 auto component(string name) { 337 auto e = stmt(format(`component "%s"`, name)); 338 auto as = e.text(""); 339 340 return ComponentType(ComponentNameType(name), ComponentModuleType(e), ComponentAsType(as)); 341 } 342 343 /** Make a relation between two things in plantuml. 344 * 345 * Ensured that the relation is well formed at compile time. 346 * Allows further manipulation of the relation and still ensuring 347 * correctness at compile time. 348 * 349 * Params: 350 * a = left relation 351 * b = right relation 352 * relate = type of relation between a/b 353 */ 354 auto relate(T)(T a, T b, Relate relate) if (CanRelate!T) { 355 static if (is(T == ClassNameType)) { 356 enum side_format = `"%s"`; 357 } else static if (is(T == ComponentNameType)) { 358 // BUG PlantUML 8036 and lower errors when a component relation uses apostrophe (") 359 enum side_format = `%s`; 360 } 361 362 auto block = stmt(""); 363 auto left = block.text(format(side_format, cast(string) a)); 364 auto middle = block.text(format(" %s ", relateToString(relate))); 365 auto right = block.text(format(side_format, cast(string) b)); 366 367 auto rl = Relation(RelateLeft(left), RelateRight(right), 368 RelateMiddle(middle), RelateBlock(block)); 369 370 return rl; 371 } 372 373 /** Raw relate of a "type" b. 374 */ 375 auto unsafeRelate(string a, string b, string type) { 376 return RelationType(stmt(format(`%s %s %s`, a, type, b))); 377 } 378 379 /** Make a floating note. 380 * 381 * It will need to be related to an object. 382 */ 383 auto note(string name) { 384 ///TODO only supporting free floating for now 385 auto block = stmt(""); 386 auto body_ = block.text(`note "`); 387 block.text(`" as ` ~ name); 388 389 return NoteType(body_); 390 } 391 392 // Suites 393 394 /** Make a UML namespace with an optional separator. 395 * The separator is inserted after the block. 396 */ 397 auto namespace(string name, Flag!"addSep" separator = Yes.addSep) { 398 auto e = suite("namespace " ~ name); 399 if (separator) { 400 sep(); 401 } 402 return e; 403 } 404 405 /** Make a PlantUML block for an inline Graphviz graph with an optional 406 * separator. 407 * The separator is inserted after the block. 408 */ 409 auto digraph(string name, Flag!"addSep" separator = Yes.addSep) { 410 auto e = suite("digraph " ~ name); 411 if (separator) { 412 sep(); 413 } 414 return e; 415 } 416 417 /** Make a UML class with content (methods, members). 418 * 419 * Return: A tuple allowing further modification. 420 */ 421 ClassType classBody(string name) { 422 auto e = stmt(format(`class "%s"`, name)); 423 auto as = e.text(""); 424 auto spot = as.text(""); 425 426 e.text(" {"); 427 e.sep; 428 auto s = e.base; 429 s.suppressIndent(1); 430 e.stmt("}", No.addSep).suppressThisIndent(1); 431 432 return ClassType(ClassNameType(name), ClassModuleType(s), 433 ClassSpotType(spot), ClassAsType(as)); 434 } 435 436 /** Make a UML component with content. 437 * 438 * Return: A tuple allowing further modification. 439 */ 440 auto componentBody(string name) { 441 auto e = stmt(format(`component "%s"`, name)); 442 auto as = e.text(""); 443 444 e.text(" {"); 445 e.sep; 446 auto s = e.base; 447 s.suppressIndent(1); 448 e.stmt("}", No.addSep).suppressThisIndent(1); 449 450 return ComponentType(ComponentNameType(name), ComponentModuleType(s), ComponentAsType(as)); 451 } 452 } 453 454 private string paramsToString(T...)(auto ref T args) { 455 import std.conv : to; 456 457 string params; 458 if (args.length >= 1) { 459 params = to!string(args[0]); 460 } 461 if (args.length >= 2) { 462 foreach (v; args[1 .. $]) { 463 params ~= ", " ~ to!string(v); 464 } 465 } 466 return params; 467 } 468 469 /** Add a label to an existing relation. 470 * 471 * The meaning of LabelPos. 472 * A "Left" -- "Right" B : "OnRelation" 473 */ 474 auto label(Relation m, string txt, LabelPos pos) { 475 final switch (pos) with (LabelPos) { 476 case Left: 477 m.left.text(format(` "%s"`, txt)); 478 break; 479 case Right: 480 // it is not a mistake to put the right label on middle 481 m.rel.text(format(`"%s" `, txt)); 482 break; 483 case OnRelation: 484 m.right.text(format(` : "%s"`, txt)); 485 break; 486 } 487 488 return m; 489 } 490 491 /// 492 unittest { 493 auto m = new PlantumlModule; 494 auto c0 = m.class_("A"); 495 auto c1 = m.class_("B"); 496 auto r0 = m.relate(c0.name, c1.name, Relate.Compose); 497 r0.label("foo", LabelPos.Right); 498 } 499 500 // Begin: Class Diagram functions 501 private alias CanHaveMethodSeq = AliasSeq!(ClassType, ClassModuleType); 502 private enum CanHaveMethod(T) = staticIndexOf!(T, CanHaveMethodSeq) >= 0; 503 504 private auto getContainedModule(T)(T m) { 505 static if (is(T == ClassModuleType)) { 506 return m; 507 } else static if (is(T == ClassType)) { 508 return m.m; 509 } else { 510 static assert(false, "Type not supported " ~ T.stringof); 511 } 512 } 513 514 /** Make a method in a UML class diagram. 515 * 516 * Only possible for those that it makes sense such as class diagrams. 517 * 518 * Params: 519 * m = ? 520 * txt = raw text representing the method. 521 * 522 * Example: 523 * --- 524 * auto m = new PlantumlModule; 525 * class_ = m.classBody("A"); 526 * class_.method("void fun();"); 527 * --- 528 */ 529 auto method(T)(T m, string txt) if (CanHaveMethod!T) { 530 auto e = m.getContainedModule.stmt(txt); 531 return e; 532 } 533 534 /// 535 unittest { 536 auto m = new PlantumlModule; 537 auto class_ = m.classBody("A"); 538 class_.method("void fun();"); 539 } 540 541 /** Make a method that takes no parameters in a UML class diagram. 542 * 543 * A helper function to get the representation of virtual, const etc correct. 544 * 545 * Params: 546 * m = ? 547 * return_type = ? 548 * name = name of the class to create a d'tor for 549 * isConst = ? 550 */ 551 auto method(T)(T m, Flag!"isVirtual" isVirtual, string return_type, string name, 552 Flag!"isConst" isConst) if (CanHaveMethod!T) { 553 auto e = m.getContainedModule.stmt(format("%s%s %s()%s", isVirtual 554 ? "virtual " : "", return_type, name, isConst ? " const" : "")); 555 return e; 556 } 557 558 /** Make a method that takes arbitrary parameters in a UML class diagram. 559 * 560 * The parameters are iteratively converted to strings. 561 * 562 * Params: 563 * m = ? 564 * return_type = ? 565 * name = name of the class to create a d'tor for 566 * isConst = ? 567 */ 568 auto method(T0, T...)(T m, Flag!"isVirtual" isVirtual, string return_type, 569 string name, Flag!"isConst" isConst, auto ref T args) if (CanHaveMethod!T) { 570 string params = m.paramsToString(args); 571 572 auto e = m.getContainedModule.stmt(format("%s%s %s(%s)%s", isVirtual 573 ? "virtual " : "", return_type, name, params, isConst ? " const" : "")); 574 return e; 575 } 576 577 /** Make a constructor without any parameters in a UML class diagram. 578 * 579 * Params: 580 * m = ? 581 * class_name = name of the class to create a d'tor for. 582 */ 583 auto ctor(T)(T m, string class_name) if (CanHaveMethod!T) { 584 auto e = m.getContainedModule.stmt(class_name ~ "()"); 585 return e; 586 } 587 588 /** Make a constructor that takes arbitrary number of parameters. 589 * 590 * Only applicable for UML class diagram. 591 * 592 * The parameters are iteratively converted to strings. 593 * 594 * Params: 595 * m = ? 596 * class_name = name of the class to create a d'tor for. 597 */ 598 auto ctorBody(T0, T...)(T0 m, string class_name, auto ref T args) 599 if (CanHaveMethod!T) { 600 string params = this.paramsToString(args); 601 602 auto e = m.getContainedModule.class_suite(class_name, format("%s(%s)", class_name, params)); 603 return e; 604 } 605 606 /** Make a destructor in a UML class diagram. 607 * Params: 608 * m = ? 609 * isVirtual = if evaluated to true prepend with virtual. 610 * class_name = name of the class to create a d'tor for. 611 */ 612 auto dtor(T)(T m, Flag!"isVirtual" isVirtual, string class_name) 613 if (CanHaveMethod!T) { 614 auto e = m.getContainedModule.stmt(format("%s%s%s()", isVirtual 615 ? "virtual " : "", class_name[0] == '~' ? "" : "~", class_name)); 616 return e; 617 } 618 619 /// 620 unittest { 621 auto m = new PlantumlModule; 622 auto class_ = m.classBody("Foo"); 623 class_.dtor(Yes.isVirtual, "Foo"); 624 } 625 626 /** Make a destructor in a UML class diagram. 627 * Params: 628 * m = ? 629 * class_name = name of the class to create a d'tor for. 630 */ 631 auto dtor(T)(T m, string class_name) if (CanHaveMethod!T) { 632 auto e = m.getContainedModule.stmt(format("%s%s()", class_name[0] == '~' ? "" : "~", class_name)); 633 return e; 634 } 635 636 /** Add a "spot" to a class in a class diagram. 637 * 638 * TODO i think there is a bug here. There is an order dependency of who is 639 * called first, addSpot or addAs. Both extend "as" which means that if 640 * addSpot is called before addAs it will be "interesting". 641 * 642 * The documentation for PlantUML describes what it is. 643 * Example of a spot: 644 * class A << I, #123456 >> 645 * '--the spot----' 646 * 647 * Example: 648 * --- 649 * auto m = new PlantumlModule; 650 * auto class_ = m.class_("A"); 651 * class_.addSpot("<< I, #123456 >>"); 652 * --- 653 */ 654 auto addSpot(T)(ref T m, string spot) if (is(T == ClassType)) { 655 m.spot.clearChildren; 656 m.spot = m.as.text(" " ~ spot); 657 658 return m.spot; 659 } 660 661 /// Creating a plantuml spot. 662 /// Output: 663 /// class A << I, #123456 >> 664 /// '--the spot----' 665 unittest { 666 auto m = new PlantumlModule; 667 auto class_ = m.class_("A"); 668 class_.addSpot("<< I, #123456 >>"); 669 670 m.render.shouldEqual(` class "A" << I, #123456 >> 671 `); 672 } 673 674 // End: Class Diagram functions 675 676 // Begin: Component Diagram functions 677 678 /** Add a PlantUML renaming of a class or component. 679 */ 680 auto addAs(T)(ref T m) if (is(T == ComponentType) || is(T == ClassType)) { 681 m.as.clearChildren; 682 683 auto as = m.as.text(" as "); 684 m.as = as; 685 686 return as; 687 } 688 // End: Component Diagram functions 689 690 /** Add a raw label "on" the relationship line. 691 */ 692 auto label(Relation m, string txt) { 693 m.right.text(format(` : "%s"`, txt)); 694 return m; 695 } 696 697 /** Specialization of an ActivityBlock. 698 */ 699 private enum ActivityKind { 700 Unspecified, 701 /// if-stmt with an optional Then 702 IfThen, 703 /// if-stmt that has consumed Then 704 If, 705 /// foo 706 Else 707 } 708 709 /** Used to realise type safe if/else/endif blocks. 710 */ 711 struct ActivityBlock(ActivityKind kind_) { 712 /// Access the compile time for easier constraint checking. 713 private enum kind = kind_; 714 715 /// Module that is meant to be enclosed between the for example if-else. 716 private ActivityModule current_; 717 718 /** Point where a new "else" can be injected. 719 * 720 * An array to allow blocks of different kinds to carry more than one 721 * injection point. Use enums to address the points for clarity. 722 */ 723 private ActivityModule[] injectBlock; 724 725 @property auto current() { 726 return current_; 727 } 728 729 /// Operations are performed on current 730 alias current this; 731 } 732 733 /// Addressing the injectBlock of an ActivityBlock!(ActivityKind.If) 734 private enum ActivityBlockIfThen { 735 Next, 736 Then 737 } 738 739 /** Semantic representation in D for Activity Diagrams. 740 * 741 * The syntax of the activity diagrams is the one that as of plantuml-8045 is 742 * marked as "beta". 743 */ 744 class ActivityModule : BaseModule { 745 mixin Attrs; 746 mixin PlantumlBase; 747 748 /// Call the module to add text. 749 auto opCall(string txt) pure { 750 return text(txt); 751 } 752 753 // Statements 754 755 /// Start a diagram. 756 auto start() { 757 return stmt("start"); 758 } 759 760 /// Stop an diagram. 761 auto stop() { 762 return stmt("stop"); 763 } 764 765 /** Basic activity. 766 * 767 * :middle; 768 * 769 * Returns: The middle text object to be filled with content. 770 */ 771 auto activity(string content) { 772 auto e = stmt(":")[$.end = ""]; 773 auto rval = e.text(content); 774 e.text(";"); 775 sep(); 776 777 rval.suppressIndent(1); 778 779 return rval; 780 } 781 782 /// Add a condition. 783 auto if_(string condition_) { 784 // the sep from cond is AFTER all this. 785 // the cond is the holder of the structure. 786 auto cond = stmt(format("if (%s)", condition_), No.addSep); 787 auto then = cond.empty; 788 cond.sep; 789 790 auto next = base; 791 next.suppressIndent(1); 792 stmt("endif"); 793 794 return ActivityBlock!(ActivityKind.IfThen)(cond, [next, then]); 795 } 796 797 auto unsafeIf(string condition, string then = null) { 798 if (then is null) { 799 return stmt(format("if (%s)", condition)); 800 } else { 801 return stmt(format("if (%s) then (%s)", condition, then)); 802 } 803 } 804 805 auto unsafeIfElse(string condition, string then = null) { 806 if (then is null) { 807 return stmt(format("else if (%s)", condition)); 808 } else { 809 return stmt(format("else if (%s) then (%s)", condition, then)); 810 } 811 } 812 813 /// Type unsafe else. 814 auto unsafeElse_() { 815 return stmt("else"); 816 } 817 818 auto unsafeEndif() { 819 return stmt("endif"); 820 } 821 } 822 823 @Name("Should be a start-stuff-stop activity diagram") 824 /// 825 unittest { 826 auto m = new ActivityModule; 827 with (m) { 828 start; 829 activity("hello")(" world"); 830 stop; 831 } 832 833 m.render.shouldEqual(" start\n :hello world;\n stop\n"); 834 } 835 836 @Name("Should be a single if-condition") 837 /// 838 unittest { 839 auto m = new ActivityModule; 840 auto if_ = m.if_("branch?"); 841 842 m.render.shouldEqual(" if (branch?) 843 endif 844 "); 845 846 with (if_) { 847 activity("inside"); 848 } 849 850 m.render.shouldEqual(" if (branch?) 851 :inside; 852 endif 853 "); 854 } 855 856 auto then(ActivityBlock!(ActivityKind.IfThen) if_then, string content) { 857 with (if_then.injectBlock[ActivityBlockIfThen.Then]) { 858 text(format(" then (%s)", content)); 859 } 860 861 auto if_ = ActivityBlock!(ActivityKind.If)(if_then.current, 862 [if_then.injectBlock[ActivityBlockIfThen.Next]]); 863 return if_; 864 } 865 866 @Name("Should be an if-condition with a marked branch via 'then'") 867 /// 868 unittest { 869 auto m = new ActivityModule; 870 with (m.if_("branch?").then("yes")) { 871 activity("inside"); 872 } 873 874 m.render.shouldEqual(" if (branch?) then (yes) 875 :inside; 876 endif 877 "); 878 } 879 880 import std.algorithm : among; 881 882 auto else_(T)(T if_) if (T.kind.among(ActivityKind.IfThen, ActivityKind.If)) { 883 auto curr = if_.injectBlock[ActivityBlockIfThen.Next].stmt("else", No.addSep); 884 curr.sep; 885 886 return ActivityBlock!(ActivityKind.Else)(curr, []); 887 } 888 889 @Name("Should be an if-else-condition") 890 /// 891 unittest { 892 auto m = new ActivityModule; 893 auto cond = m.if_("cond?"); 894 cond.activity("stuff yes"); 895 with (cond.else_) { 896 activity("stuff no"); 897 } 898 899 m.render.shouldEqual(" if (cond?) 900 :stuff yes; 901 else 902 :stuff no; 903 endif 904 "); 905 } 906 907 auto else_if(T)(T if_, string condition) 908 if (T.kind.among(ActivityKind.IfThen, ActivityKind.If)) { 909 auto cond = if_.injectBlock[ActivityBlockIfThen.Next].stmt(format("else if (%s)", 910 condition), No.addSep); 911 auto then = cond.empty; 912 cond.sep; 913 914 auto next = if_.base; 915 next.suppressIndent(1); 916 917 return ActivityBlock!(ActivityKind.IfThen)(cond, [next, then]); 918 } 919 920 @Name("Should be an if-else_if-else with 'then'") 921 unittest { 922 auto m = new ActivityModule; 923 auto cond = m.if_("cond1?"); 924 cond.then("yes"); 925 cond.activity("stuff1"); 926 927 auto else_if = cond.else_if("cond2?"); 928 else_if.then("yes"); 929 else_if.activity("stuff2"); 930 931 m.render.shouldEqual(" if (cond1?) then (yes) 932 :stuff1; 933 else if (cond2?) then (yes) 934 :stuff2; 935 endif 936 "); 937 } 938 939 @Name("Should be complex conditions") 940 unittest { 941 auto m = new ActivityModule; 942 943 auto cond = m.if_("cond1"); 944 with (cond.then("yes")) { 945 activity("stuff"); 946 activity("stuff"); 947 948 auto cond2 = if_("cond2"); 949 with (cond2) { 950 activity("stuff"); 951 } 952 with (cond2.else_) { 953 activity("stuff"); 954 } 955 } 956 957 with (cond.else_) { 958 activity("stuff"); 959 } 960 961 m.render.shouldEqual(" if (cond1) then (yes) 962 :stuff; 963 :stuff; 964 if (cond2) 965 :stuff; 966 else 967 :stuff; 968 endif 969 else 970 :stuff; 971 endif 972 "); 973 } 974 975 /** Generate a plantuml block ready to be rendered. 976 */ 977 struct PlantumlRootModule { 978 private PlantumlModule root; 979 980 /// Make a root module with suppressed indent of the first level. 981 static auto make() { 982 typeof(this) r; 983 r.root = new PlantumlModule; 984 r.root.suppressIndent(1); 985 986 return r; 987 } 988 989 /// Make a module contained in the root suitable for plantuml diagrams. 990 PlantumlModule makeUml() { 991 import std.ascii : newline; 992 993 auto e = root.suite("")[$.begin = "@startuml" ~ newline, $.end = "@enduml"]; 994 return e; 995 } 996 997 /// Make a module contained in the root suitable for grahviz dot diagrams. 998 PlantumlModule makeDot() { 999 import std.ascii : newline; 1000 1001 auto dot = root.suite("")[$.begin = "@startdot" ~ newline, $.end = "@enddot"]; 1002 return dot; 1003 } 1004 1005 /// Textually render the module tree. 1006 auto render() 1007 in { 1008 assert(root !is null); 1009 } 1010 body { 1011 return root.render(); 1012 } 1013 } 1014 1015 @Name("should be a complete plantuml block ready to be rendered") 1016 unittest { 1017 auto b = PlantumlRootModule.make(); 1018 b.makeUml; 1019 1020 b.render().shouldEqual("@startuml 1021 @enduml 1022 "); 1023 } 1024 1025 @Name("should be a block with a class") 1026 unittest { 1027 auto r = PlantumlRootModule.make(); 1028 auto c = r.makeUml; 1029 1030 c.class_("A"); 1031 1032 r.render.shouldEqual(`@startuml 1033 class "A" 1034 @enduml 1035 `); 1036 } 1037 1038 // from now on assuming the block works correctly 1039 @Name("should be two related classes") 1040 unittest { 1041 auto c = new PlantumlModule; 1042 1043 auto a = c.class_("A"); 1044 auto b = c.class_("B"); 1045 1046 c.relate(a.name, b.name, Relate.WeakRelate); 1047 c.relate(a.name, b.name, Relate.Relate); 1048 c.relate(a.name, b.name, Relate.Compose); 1049 c.relate(a.name, b.name, Relate.Aggregate); 1050 c.relate(a.name, b.name, Relate.Extend); 1051 c.relate(a.name, b.name, Relate.ArrowTo); 1052 c.relate(a.name, b.name, Relate.AggregateArrowTo); 1053 1054 c.render.shouldEqual(` class "A" 1055 class "B" 1056 "A" .. "B" 1057 "A" -- "B" 1058 "A" o-- "B" 1059 "A" *-- "B" 1060 "A" --|> "B" 1061 "A" --> "B" 1062 "A" *--> "B" 1063 `); 1064 } 1065 1066 @Name("should be two related components") 1067 unittest { 1068 auto c = new PlantumlModule; 1069 1070 auto a = c.component("A"); 1071 auto b = c.component("B"); 1072 1073 c.relate(a.name, b.name, Relate.WeakRelate); 1074 c.relate(a.name, b.name, Relate.Relate); 1075 c.relate(a.name, b.name, Relate.Compose); 1076 c.relate(a.name, b.name, Relate.Aggregate); 1077 c.relate(a.name, b.name, Relate.Extend); 1078 c.relate(a.name, b.name, Relate.ArrowTo); 1079 c.relate(a.name, b.name, Relate.AggregateArrowTo); 1080 1081 c.render.shouldEqual(` component "A" 1082 component "B" 1083 A .. B 1084 A -- B 1085 A o-- B 1086 A *-- B 1087 A --|> B 1088 A --> B 1089 A *--> B 1090 `); 1091 } 1092 1093 @Name("should be a labels on the relation between two components") 1094 unittest { 1095 auto c = new PlantumlModule; 1096 1097 auto a = c.component("A"); 1098 auto b = c.component("B"); 1099 1100 auto l = c.relate(a.name, b.name, Relate.Relate); 1101 l.label("related"); 1102 1103 c.render.shouldEqual(` component "A" 1104 component "B" 1105 A -- B : "related" 1106 `); 1107 } 1108 1109 @Name("should be a labels on the components over the relation line") 1110 unittest { 1111 auto c = new PlantumlModule; 1112 1113 auto a = c.component("A"); 1114 auto b = c.component("B"); 1115 1116 auto l = c.relate(a.name, b.name, Relate.Relate); 1117 1118 l.label("1", LabelPos.Left); 1119 l.label("2", LabelPos.Right); 1120 l.label("related", LabelPos.OnRelation); 1121 1122 c.render.shouldEqual(` component "A" 1123 component "B" 1124 A "1" -- "2" B : "related" 1125 `); 1126 } 1127 1128 @Name("Should be a class with a spot") 1129 unittest { 1130 auto m = new PlantumlModule; 1131 1132 { 1133 auto c = m.class_("A"); 1134 c.addSpot("<< (D, orchid) >>"); 1135 } 1136 1137 { 1138 auto c = m.classBody("B"); 1139 c.addSpot("<< (I, orchid) >>"); 1140 c.method("fun()"); 1141 } 1142 1143 m.render.shouldEqual(` class "A" << (D, orchid) >> 1144 class "B" << (I, orchid) >> { 1145 fun() 1146 } 1147 `); 1148 } 1149 1150 @Name("Should be a spot separated from the class name in a root module") 1151 unittest { 1152 auto r = PlantumlRootModule.make; 1153 auto m = r.makeUml; 1154 1155 { 1156 auto c = m.class_("A"); 1157 c.addSpot("<< (D, orchid) >>"); 1158 } 1159 1160 { 1161 auto c = m.classBody("B"); 1162 c.addSpot("<< (I, orchid) >>"); 1163 c.method("fun()"); 1164 } 1165 1166 r.render.shouldEqual(`@startuml 1167 class "A" << (D, orchid) >> 1168 class "B" << (I, orchid) >> { 1169 fun() 1170 } 1171 @enduml 1172 `); 1173 } 1174 1175 @Name("Should be a component with an 'as'") 1176 unittest { 1177 auto m = new PlantumlModule; 1178 auto c = m.component("A"); 1179 1180 c.addAs.text("a"); 1181 1182 m.render.shouldEqual(` component "A" as a 1183 `); 1184 } 1185 1186 @Name("Should be a namespace") 1187 unittest { 1188 auto m = new PlantumlModule; 1189 auto ns = m.namespace("ns"); 1190 1191 m.render.shouldEqual(` namespace ns { 1192 } 1193 `); 1194 }