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