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 }