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 }