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