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