1 ///
2 module jaster.serialise.builder;
3 
4 version(Jasterialise_Unittests) import fluent.asserts;
5 
6 public import std.typecons : Flag, Yes, No;
7 
8 /// Passed to functions such as `CodeBuilder.put` to control automatic tabbing.
9 alias UseTabs = Flag!"tabs";
10 
11 /// Passed to functions such as `CodeBuilder.put` to control automatic new lines.
12 alias UseNewLines = Flag!"newLines";
13 
14 /// A delegate that can be passed to functions such as `addFuncDeclaration` to allow flexible
15 /// generation of code.
16 alias CodeFunc = void delegate(CodeBuilder);
17 
18 /++
19  + A glorified wrapper around an `Appender` which supports automatic tabbing and new lines.
20  +
21  + On it's own, `CodeBuilder` may already be more desirable than manually formatting, tabbing new lining, 
22  + a code string manually.
23  +
24  + UFCS can also be used to create functions that can ease the generation of code, such as `addFuncCall`,
25  + `addFuncDeclaration`, `addReturn`, etc.
26  ++/
27 final class CodeBuilder
28 {
29     import std.array : Appender;
30 
31     private
32     {
33         Appender!(dchar[]) _data;
34         size_t             _tabs;
35         static const dchar _tabChar = '\t';
36 
37         size_t  _disableTabCount;
38         size_t  _disableLinesCount;
39     }
40 
41     public
42     {
43         /++
44          + Increases the tab count, meaning anytime `CodeBuilder.put` is used an extra tab will be written before
45          + the data passed to it.
46          +
47          + Notes:
48          +  If the tab count is the same value as `size_t.max` then this function does nothing.
49          + ++/
50         @safe @nogc
51         void entab() nothrow pure
52         {
53             // Overflow protection
54             if(this._tabs == size_t.max)
55                 return;
56 
57             this._tabs += 1;
58         }
59 
60         /++
61          + Decreases the tab count.
62          +
63          + Notes:
64          +  If the tab count is 0 then this function does nothing.
65          + ++/
66         @safe @nogc
67         void detab() nothrow pure
68         {
69             // Underflow protection
70             if(this._tabs == 0)
71                 return;
72 
73             this._tabs -= 1;
74         }
75 
76         /++
77          + Disables automatic tabbing and/or new line insertion.
78          +
79          + Notes:
80          +  For every call to `disable`, a call to `enable` is required to re-enable the functionality.
81          +
82          +  For example, if 2 calls to `disable` are made to disable tabbing, then 2 calls to `enable` for tabbing must be made
83          +  before tabbing is re-enabled.
84          +
85          + Params:
86          +  disableTabs  = If `Yes.tabs` then automatic tabbing will be disabled.
87          +  disableLines = If `Yes.newLines` then automatic new line insertion will be disabled.
88          +
89          + See_Also:
90          +  `CodeBuilder.enable`
91          + ++/
92         @safe @nogc
93         void disable(UseTabs disableTabs = Yes.tabs, UseNewLines disableLines = Yes.newLines) nothrow pure
94         {
95             void _disable(ref size_t counter, bool doAction)
96             {
97                 if(counter == size_t.max || !doAction)
98                     return;
99 
100                 counter += 1;
101             }
102 
103             _disable(this._disableLinesCount, disableLines);
104             _disable(this._disableTabCount,   disableTabs);
105         }
106 
107         /++
108          + Enables automatic tabbing and/or new line insertion.
109          +
110          + Params:
111          +  enableTabs  = If `Yes.tabs` then automatic tabbing will be enabled.
112          +  enableLines = If `Yes.newLines` then automatic new line insertion will be enabled.
113          +
114          + See_Also:
115          +  `CodeBuilder.disable`
116          + ++/
117         @safe @nogc
118         void enable(UseTabs enableTabs = Yes.tabs, UseNewLines enableLines = Yes.newLines) nothrow pure
119         {
120             void _enable(ref size_t counter, bool doAction)
121             {
122                 if(counter == 0 || !doAction)
123                     return;
124 
125                 counter -= 1;
126             }
127 
128             _enable(this._disableLinesCount, enableLines);
129             _enable(this._disableTabCount,   enableTabs);
130         }
131 
132         /++
133          + Inserts data into the code string.
134          +
135          + Notes:
136          +  `T` can be anything supported by `Appender!(dchar[])`
137          +
138          +  `CodeBuilder.enable` and `CodeBuilder.disable` are used to enable/disable the functionality of
139          +  `doTabs` and `doLines` regardless of their values.
140          +
141          +   For ranges of `dchar[]` (such as `dchar[][]`) the functionality of `doTabs` and `doLines` will be applied to each
142          +   `dchar[]` given.
143          +
144          + Params:
145          +  data    = The data to insert.
146          +  doTabs  = If `Yes.tabs` then a certain amount of tabs (see `CodeBuilder.entab`) will be inserted
147          +            before `data` is inserted.
148          +  doLines = If `Yes.newLines` then a new line will be inserted after `data`.
149          + ++/
150         void put(T)(T data, UseTabs doTabs = Yes.tabs, UseNewLines doLines = Yes.newLines)
151         {
152             import std.array;
153             import std.algorithm : map;
154             import std.range     : repeat, isInputRange, chain, ElementEncodingType, take;
155 
156             // For now, I'm just going to rely on the compiler's error message for when
157             // the user passes something that Appender doesn't like.
158             
159             if(this._disableLinesCount > 0)
160                 doLines = No.newLines;
161             
162             if(this._disableTabCount > 0)
163                 doTabs = No.tabs;
164 
165             auto tabs = this._tabChar.repeat((doTabs) ? this._tabs : 0);
166             auto line = ['\n'].take((doLines) ? 1 : 0);
167 
168             static if(isInputRange!T && is(ElementEncodingType!T : dchar[])) // ranges of dchar[]
169             {
170                 // TODO: Actually bother to test this. `chain` wouldn't work in the else statement, so possibly won't work here.
171                 this._data.put(data.map!(str => chain(tabs, str, line)));
172             }
173             else // dstring/ranges of dchar
174             {
175                 this._data.put(tabs);
176                 this._data.put(data);
177                 this._data.put(line);
178             }
179         }
180 
181         /// overload ~=
182         void opOpAssign(string op : "~", T)(T data)
183         {
184             this.put(data);
185         }
186 
187         /++
188          + Returns:
189          +  The code currently generated.
190          + ++/
191         @property @safe @nogc
192         const(dchar)[] data() nothrow pure const
193         {
194             return this._data.data;
195         }
196     }
197 }
198 
199 /// Describes a variable.
200 struct Variable
201 {
202     /// The name of the variable's type.
203     dstring typeName;
204 
205     /// The name of the variable.
206     dstring name;
207 
208     /// The `CodeFunc` which generates the default value of the variable.
209     CodeFunc defaultValue;
210 }
211 
212 /++
213  + A helper function that entabs the given `CodeBuilder`, calls a delegate to generate some code, and then detabs the `CodeBuilder`.
214  +
215  + Params:
216  +  builder = The `CodeBuilder` to use.
217  +  code    = The `CodeFunc` to use.
218  +
219  + Returns:
220  +  `builder`
221  + ++/
222 CodeBuilder putEntabbed(CodeBuilder builder, CodeFunc code)
223 {
224     builder.entab();
225     code(builder);
226     builder.detab();
227 
228     return builder;
229 }
230 ///
231 version(Jasterialise_Unittests)
232 unittest
233 {
234     auto builder = new CodeBuilder();
235 
236     builder.put("Hello");
237     builder.data.should.equal("Hello\n");
238 
239     builder.putEntabbed(b => b.put("World"));
240     builder.data.should.equal("Hello\n\tWorld\n");
241 }
242 
243 /++
244  + A helper function to write the given code in between two '"'s
245  +
246  + Notes:
247  +  `T` can be any type that can be passed to `CodeBuilder.put`.
248  +
249  + Params:
250  +  builder = The `CodeBuilder` to use.
251  +  str     = The code to write.
252  +
253  + Returns:
254  +  `builder`
255  + ++/
256 CodeBuilder putString(T)(CodeBuilder builder, T str)
257 {
258     builder.disable();
259 
260     builder.put('"');
261     builder.put(str);
262     builder.put('"');
263 
264     builder.enable();
265     return builder;
266 }
267 ///
268 version(Jasterialise_Unittests)
269 unittest
270 {
271     auto builder = new CodeBuilder();
272 
273     builder.put("Hello");
274     builder.putString("World!");
275 
276     builder.data.should.equal("Hello\n\"World!\"");
277 }
278 
279 /++
280  +
281  + ++/
282 CodeBuilder putScope(CodeBuilder builder, CodeFunc func)
283 {
284     builder.put('{');
285     builder.putEntabbed(b => func(b));
286     builder.put('}');
287 
288     return builder;
289 }
290 ///
291 version(Jasterialise_Unittests)
292 unittest
293 {
294     auto builder = new CodeBuilder();
295     builder.putScope(
296         (b)
297         {
298             b.addFuncCall("writeln", "\"Hello world!\"");
299         });
300 
301     builder.data.should.equal("{\n\twriteln(\"Hello world!\");\n}\n");
302 }
303 
304 /++
305  + Formatted version of `CodeBuilder.put`.
306  +
307  + Params:
308  +  builder   = The `CodeBuilder` to use.
309  +  formatStr = The format string to pass to `std.format.format`
310  +  params    = The paramters to pass to `std.format.format`
311  +
312  + Returns:
313  +  `builder`
314  + ++/
315 CodeBuilder putf(Params...)(CodeBuilder builder, dstring formatStr, Params params)
316 {
317     import std.format : format;
318 
319     builder.put(format(formatStr, params));
320 
321     return builder;
322 }
323 ///
324 version(Jasterialise_Unittests)
325 unittest
326 {
327     auto builder = new CodeBuilder();
328     builder.putf("if(%s == %s)", "\"Hello\"", "\"World\"");
329 
330     builder.data.should.equal("if(\"Hello\" == \"World\")\n");
331 }
332 
333 /++
334  + A helper function which accepts a wide variety of parameters to pass to `CodeBuilder.put`.
335  +
336  + Supported_Types:
337  +  InputRanges of characters (dstring, for example) - Written in as-is, with no modification.
338  +
339  +  `CodeFunc` - The `CodeFunc` is called with `builder` as it's parameter.
340  +
341  +  `Variable` - The name of the variable is written.
342  +
343  +  Any built-in D type - The result of passing the parameter to `std.conv.to!string` is written.
344  +
345  + Params:
346  +  builder = The `CodeBuilder` to use.
347  +  param   = The parameter to put.
348  +
349  + Returns:
350  +  `builder`
351  + ++/
352 CodeBuilder putExtended(T)(CodeBuilder builder, T param)
353 {
354     import std.range  : isInputRange;
355     import std.conv   : to;
356     import std.traits : isBuiltinType, isSomeFunction;
357 
358     alias PType = T;
359 
360     static if(is(PType : dstring) || isInputRange!PType)
361         builder.put(param);
362     else static if(is(PType : CodeFunc))
363         param(builder);
364     else static if(is(PType == Variable))
365         builder.put(param.name);
366     else static if(isBuiltinType!PType)
367         builder.put(param.to!dstring);
368     else static if(isSomeFunction!PType) // CodeFunc desctibes a delegate, so for functions we need to turn them into delegates first.
369     {
370         import std.functional : toDelegate;
371 
372         auto del = param.toDelegate;
373         static assert(is(typeof(del) : CodeFunc), "Function of type '" ~ PType.stringof ~ "' is not convertable to a CodeFunc");
374 
375         builder.putExtended(del);
376     }
377     else
378         static assert(false, "Unsupported type: " ~ PType.stringof);
379 
380     return builder;
381 }
382 ///
383 version(Jasterialise_Unittests)
384 unittest
385 {
386     auto builder = new CodeBuilder();
387 
388     builder.putExtended("Hello"d)                           // strings
389            .putExtended((CodeBuilder b) => b.put("World!")) // CodeFuncs
390            .putExtended(Variable("int", "myVar", null))     // Variables (only their names are written)
391            .putExtended(true);                              // Built-in D types (bools, ints, floats, etc.)
392 
393     builder.data.should.equal("Hello\nWorld!\nmyVar\ntrue\n"d);
394 }
395 
396 /++
397  + Creates a function using the given data.
398  +
399  + Params:
400  +  builder     = The `CodeBuilder` to use.
401  +  returnType  = The name of the type that the function returns.
402  +  name        = The name of the function.
403  +  params      = The function's parameters.
404  +  body_       = The `CodeFunc` which generates the code for the function's body.
405  +
406  + Returns:
407  +  `builder`
408  + ++/
409 CodeBuilder addFuncDeclaration(CodeBuilder builder, dstring returnType, dstring name, Variable[] params, CodeFunc body_)
410 {
411     import std.algorithm : map, joiner;
412 
413     builder.put(returnType ~ " " ~ name, Yes.tabs, No.newLines);
414 
415     builder.disable();
416     builder.put("(");        
417     builder.put(params.map!(v => v.typeName ~ " " ~ v.name)
418                         .joiner(", "));
419     builder.enable();
420     builder.put(")", No.tabs);
421 
422     builder.putScope(body_);
423     return builder;
424 }
425 ///
426 version(Jasterialise_Unittests)
427 unittest
428 {
429     auto builder = new CodeBuilder();
430 
431     builder.addFuncDeclaration("int", "sum", [Variable("int", "a"), Variable("int", "b")], (b){b.addReturn("a + b"d);});
432 
433     builder.data.should.equal("int sum(int a, int b)\n{\n\treturn a + b;\n}\n");
434 }
435 
436 /++
437  + Creates a function using the given data.
438  +
439  + Params:
440  +  returnType  = The type that the function return.
441  +
442  +  builder     = The `CodeBuilder` to use.
443  +  name        = The name of the function.
444  +  params      = The function's parameters.
445  +  body_       = The `CodeFunc` which generates the code for the function's body.
446  +
447  + Returns:
448  +  `builder`
449  + ++/
450 CodeBuilder addFuncDeclaration(returnType)(CodeBuilder builder, dstring name, Variable[] params, CodeFunc body_)
451 {
452     import std.traits : fullyQualifiedName;
453 
454     return builder.addFuncDeclaration(fullyQualifiedName!returnType, name, params, body_);
455 }
456 ///
457 version(Jasterialise_Unittests)
458 unittest
459 {
460     auto builder = new CodeBuilder();
461 
462     builder.addFuncDeclaration!int("six", null, (b){b.addReturn("6"d);});
463 
464     builder.data.should.equal("int six()\n{\n\treturn 6;\n}\n");
465 }
466 
467 /++
468  + Creates an import statement.
469  +
470  + Notes:
471  +  If `selection` is `null`, then the entire module is imported.
472  +  Otherwise, only the specified symbols are imported.
473  +
474  + Params:
475  +  builder = The `CodeBuilder` to use.
476  +  moduleName = The name of the module to import.
477  +  selection = An array of which symbols to import from the module.
478  +
479  + Returns:
480  +  `builder`
481  + ++/
482 CodeBuilder addImport(CodeBuilder builder, dstring moduleName, dstring[] selection = null)
483 {
484     builder.put("import " ~ moduleName, Yes.tabs, No.newLines);
485     
486     if(selection !is null)
487     {
488         import std.algorithm : joiner;
489         builder.disable(); // Disables both (tabbing and new lines) by default.
490 
491         builder.put(" : ");
492         builder.put(selection.joiner(", "d));
493 
494         builder.enable(); // Likewise, enables both by default
495     }
496 
497     builder.put(';', No.tabs);
498     return builder;
499 }
500 ///
501 version(Jasterialise_Unittests)
502 unittest
503 {
504     auto builder = new CodeBuilder();
505 
506     // Import entire module
507     builder.addImport("std.stdio");
508     builder.data.should.equal("import std.stdio;\n");
509 
510     builder = new CodeBuilder(); // Just to keep the asserts clean to read.
511 
512 
513     // Selective imports
514     builder.addImport("std.stdio", ["readln"d, "writeln"d]);
515     builder.data.should.equal("import std.stdio : readln, writeln;\n");
516 }
517 
518 /++
519  + Declares a variable, and returns a `Variable` which can be used to easily reference the variable.
520  +
521  + Notes:
522  +  `valueFunc` may be `null`.
523  +
524  + Params:
525  +  builder   = The `CodeBuilder` to use.
526  +  type      = The name of the variable's type.
527  +  name      = The name of the variable.
528  +  valueFunc = The `CodeFunc` which generates the code to set the variable's intial value.
529  +
530  + Returns:
531  +  A `Variable` describing the variable declared by this function.
532  + ++/
533 Variable addVariable(CodeBuilder builder, dstring type, dstring name, CodeFunc valueFunc = null)
534 {
535     builder.put(type ~ " " ~ name, Yes.tabs, No.newLines);
536 
537     if(valueFunc !is null)
538     {
539         builder.disable(); // Disable automatic tabs and new lines.
540 
541         builder.put(" = ");
542         valueFunc(builder);
543 
544         builder.enable(); // Enable them both
545     }
546 
547     builder.put(";", No.tabs);
548     return Variable(type, name, valueFunc);
549 }
550 ///
551 version(Jasterialise_Unittests)
552 unittest
553 {
554     auto builder = new CodeBuilder();
555 
556     // Declare the variable without setting it.
557     auto six = builder.addVariable("int", "six");
558     builder.data.should.equal("int six;\n"d);
559     six.should.equal(Variable("int", "six"));
560 
561     builder = new CodeBuilder();
562 
563 
564     // Declare the variable, and set it's value.
565     CodeFunc func = (b){b.put("6");};
566              six  = builder.addVariable("int", "six", func);
567 
568     builder.data.should.equal("int six = 6;\n");
569     six.should.equal(Variable("int", "six", func));
570 }
571 
572 /// A helper function to more easily specify the variable's type.
573 Variable addVariable(T)(CodeBuilder builder, dstring name, CodeFunc valueFunc = null)
574 {
575     import std.traits : fullyQualifiedName;
576 
577     return builder.addVariable(fullyQualifiedName!T, name, valueFunc);
578 }
579 ///
580 version(Jasterialise_Unittests)
581 unittest
582 {
583     auto builder = new CodeBuilder();
584 
585     builder.addVariable!int("six", (b){b.put("6");});
586     builder.data.should.equal("int six = 6;\n");
587 }
588 
589 /// A helper function for `addVariable` which creates an alias.
590 Variable addAlias(CodeBuilder builder, dstring name, CodeFunc valueFunc)
591 {
592     return builder.addVariable("alias", name, valueFunc);
593 }
594 ///
595 version(Jasterialise_Unittests)
596 unittest
597 {
598     auto builder = new CodeBuilder();
599     builder.addAlias("SomeType", (b){b.put("int");});
600     builder.data.should.equal("alias SomeType = int;\n");
601 }
602 
603 /// A helper function for `addVariable` which creates an enum value.
604 Variable addEnumValue(CodeBuilder builder, dstring name, CodeFunc valueFunc)
605 {
606     return builder.addVariable("enum", name, valueFunc);
607 }
608 ///
609 version(Jasterialise_Unittests)
610 unittest
611 {
612     auto builder = new CodeBuilder();
613     builder.addEnumValue("SomeValue", (b){b.put("6");});
614     builder.data.should.equal("enum SomeValue = 6;\n");
615 }
616 
617 /++
618  + Creates a return statement.
619  +
620  + Notes:
621  +  `T` can be any type supported by `putExtended`.
622  +
623  + Params:
624  +  builder = The `CodeBuilder` to use.
625  +  code    = The code to use in the return statement.
626  +
627  + Returns:
628  +  `builder`
629  + ++/
630 CodeBuilder addReturn(T)(CodeBuilder builder, T code)
631 {
632     builder.put("return ", Yes.tabs, No.newLines);
633     builder.disable();
634 
635     builder.putExtended(code);
636 
637     builder.enable();
638     builder.put(";", No.tabs, Yes.newLines);
639 
640     return builder;
641 }
642 ///
643 version(Jasterialise_Unittests)
644 unittest
645 {
646     auto builder = new CodeBuilder();
647 
648     // Option #1: Pass in a dstring, and it'll be added as-is.
649     builder.addReturn("21 * 8"d);
650     builder.data.should.equal("return 21 * 8;\n");
651 
652     builder = new CodeBuilder();
653 
654 
655     // Option #2: Pass in an instance of Variable, and the variable's name is added.
656     builder.addReturn(Variable("int", "someNumber"));
657     builder.data.should.equal("return someNumber;\n");
658 
659     builder = new CodeBuilder();
660 
661 
662     // Option #3: Pass in a CodeFunc, and let it deal with generating the code it needs.
663     CodeFunc func = (b){b.put("200 / someNumber");};
664     builder.addReturn(func);
665     builder.data.should.equal("return 200 / someNumber;\n");
666 }
667 
668 /++
669  + Creates a call to a function.
670  +
671  + Notes:
672  +  `params` can be made up of any combination of values supported by `putExtended`.
673  +
674  +  Strings $(B won't) be automatically enclosed between speech marks('"').
675  +
676  + Params:
677  +  semicolon = If `Yes.semicolon`, then a ';' is inserted at the end of the function call.
678  +
679  +  builder  = The `CodeBuilder` to use.
680  +  funcName = The name of the function to call.
681  +  params   = The parameters to pass to the function.
682  +
683  + Returns:
684  +  `builder`
685  + ++/
686 CodeBuilder addFuncCall(Flag!"semicolon" semicolon = Yes.semicolon, Params...)(CodeBuilder builder, dstring funcName, Params params)
687 {
688     import std.conv   : to;
689     import std.range  : isInputRange;
690     import std.traits : isBuiltinType;
691 
692     builder.put(funcName, Yes.tabs, No.newLines);
693     builder.disable();
694     builder.put('(');
695 
696     foreach(i, param; params)
697     {
698         builder.putExtended(param);
699 
700         static if(i != params.length - 1)
701             builder.put(", ");
702     }
703 
704     builder.enable();
705     builder.put(')', No.tabs, No.newLines);
706 
707     static if(semicolon)
708         builder.put(';', No.tabs);
709 
710     return builder;
711 }
712 ///
713 version(Jasterialise_Unittests)
714 unittest
715 {
716     auto builder = new CodeBuilder();
717 
718     // DStrings(Including input ranges of them), CodeFuncs, built-in types(int, bool, float, etc.), and Variables can all be passed as parameters.
719     dstring  str  = "\"Hello\""d;
720     CodeFunc func = (b){b.putString("World!");};
721     Variable vari = Variable("int", "someVar");
722 
723     builder.addFuncCall("writeln", str, func, vari);
724 
725     builder.data.should.equal("writeln(\"Hello\", \"World!\", someVar);\n");
726 }