1 /**
2  * Authors: Tomoya Tanjo
3  * Copyright: © 2021 Tomoya Tanjo
4  * License: Apache-2.0
5  */
6 module salad.meta;
7 
8 import salad.type;
9 
10 import dyaml;
11 
12 ///
13 mixin template genCtor()
14 {
15     import dyaml : Node, NodeType;
16     import salad.context : LoadingContext;
17 
18     this() {}
19     this(in Node node, in LoadingContext context = LoadingContext.init) @trusted
20     {
21         import std.algorithm : endsWith;
22         import std.traits : FieldNameTuple;
23 
24         alias This = typeof(this);
25 
26         static foreach(field; FieldNameTuple!This)
27         {
28             static if (field.endsWith("_") && !isConstantMember!(This, field))
29             {
30                 // static if (This.stringof == "RecordField")
31                 //     pragma(msg, Assign!(node, mixin(field)));
32                 mixin("mixin(Assign!(node, "~field~"));");
33             }
34         }
35     }
36 }
37 
38 /**
39  * Bugs: It does not work with self recursive classes
40  */
41 mixin template genToString()
42 {
43     override string toString() const @trusted
44     {
45         import salad.type : isEither, isOptional, match;
46         import std.array : join;
47         import std.format : format;
48         import std.traits : FieldNameTuple;
49 
50         string[] fstrs;
51 
52         alias This = typeof(this);
53 
54         static foreach(field; FieldNameTuple!This)
55         {
56             static if (isOptional!(typeof(mixin(field))))
57             {
58                 mixin(field).match!((None _) { },
59                                     (rest) { fstrs ~= format!"%s: %s"(field, rest); });
60             }
61             else static if (isEither!(typeof(mixin(field))))
62             {
63                 mixin(field).match!(f => fstrs ~= format!"%s: %s"(field, f));
64             }
65             else
66             {
67                 fstrs ~= format!"%s: %s"(field, mixin(field));
68             }
69         }
70         return format!"%s(%s)"(This.stringof, fstrs.join(", "));
71     }
72 }
73 
74 ///
75 mixin template genIdentifier()
76 {
77     import std.traits : getSymbolsByUDA;
78 
79     static if (getSymbolsByUDA!(typeof(this), id).length == 1)
80     {
81         auto identifier() const @nogc nothrow pure @safe
82         {
83             import std.traits : Unqual;
84             auto i = getSymbolsByUDA!(typeof(this), id)[0];
85             alias idType = Unqual!(typeof(i));
86             static assert(is(idType == string) || is(idType == Optional!string));
87             static if (is(idType == string))
88             {
89                 return i;
90             }
91             else
92             {
93                 return i.match!(
94                     (string s) => s,
95                     none => "",
96                 );
97             }
98         }
99     }
100 }
101 
102 /**
103  * UDA for identifier maps
104  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Identifier_maps
105 */
106 struct idMap { string subject; string predicate = ""; }
107 
108 /**
109  * UDA for DSL for types
110  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Domain_Specific_Language_for_types
111 */
112 struct typeDSL{}
113 
114 /** 
115  * UDA for documentRoot
116  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#SaladRecordSchema
117  */
118 struct documentRoot{}
119 
120 /** 
121  * UDA for identifier
122  * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Record_field_annotations
123  */
124 struct id{}
125 
126 enum hasIdentifier(T) = __traits(compiles, { auto id = T.init.identifier(); });
127 
128 ///
129 template DocumentRootType(alias module_)
130 {
131     import std.meta : allSatisfy, ApplyRight, Filter, staticMap;
132     import std.traits : fullyQualifiedName, hasUDA;
133 
134     alias StrToType(string T) = __traits(getMember, module_, T);
135     alias syms = staticMap!(StrToType, __traits(allMembers, module_));
136     alias RootTypes = Filter!(ApplyRight!(hasUDA, documentRoot), syms);
137     static if (RootTypes.length > 0)
138     {
139         static assert(allSatisfy!(hasIdentifier, RootTypes));
140         alias DocumentRootType = SumType!RootTypes;
141     }
142     else
143     {
144         import std.format : format;
145         import std.traits : moduleName;
146         static assert(false, format!"No schemas with `documentRoot: true` in module `%s`"(moduleName!module_));
147     }
148 }
149 
150 ///
151 template IdentifierType(alias module_)
152 {
153     import std.meta : allSatisfy, Filter, staticMap;
154     import std.traits : fullyQualifiedName;
155 
156     alias StrToType(string T) = __traits(getMember, module_, T);
157     alias syms = staticMap!(StrToType, __traits(allMembers, module_));
158     alias IDTypes = Filter!(hasIdentifier, syms);
159 
160     static if (IDTypes.length > 0)
161     {
162         alias IdentifierType = SumType!IDTypes;
163     }
164     else
165     {
166         static assert(false, "No schemas with identifier field");
167     }
168 }
169 
170 /**
171 Returns: a string to construct `T` with a parameter whose variable name is `param`
172 Note: Use this function instead of `param.as!T` to prevent circular references
173 */
174 string ctorStr(T)(string param)
175 {
176     import std.format : format;
177     static if (is(T == class))
178     {
179         return format!"new %1$s(%2$s, context)"(T.stringof, param);
180     }
181     else
182     {
183         return format!"%2$s.as!%1$s"(T.stringof, param);
184     }
185 }
186 
187 enum isConstantMember(T, string M) = is(typeof(__traits(getMember, T, M)) == immutable string);
188 
189 ///
190 template Assign(alias node, alias field)
191 {
192     import std.format : format;
193     import std.traits : getUDAs, hasUDA, select;
194 
195     static if (hasUDA!(field, idMap))
196     {
197         enum idMap_ = getUDAs!(field, idMap)[0];
198     }
199     else
200     {
201         enum idMap_ = idMap.init;
202     }
203 
204     alias T = typeof(field);
205 
206     enum param = field.stringof[0..$-1];
207 
208     enum ImportList = q"EOS
209             import salad.util : edig;
210             import std.algorithm : map;
211             import std.array : array;
212 EOS";
213 
214     static if (isOptional!T)
215     {
216         enum Assign = ImportList~format!q"EOS
217             if (auto f = "%s" in %s)
218             {
219                 %s
220             }
221 EOS"(param, node.stringof, Assign_!("(*f)", field.stringof, T, hasUDA!(field, typeDSL), idMap_));
222     }
223     else static if (isEither!T)
224     {
225         enum Assign = ImportList~Assign_!(format!`%s.edig("%s")`(node.stringof, param),
226                                           field.stringof, T, hasUDA!(field, typeDSL), idMap_);
227     }
228     else
229     {
230         enum Assign = ImportList~Assign_!(format!`%s.edig("%s")`(node.stringof, param),
231                                           field.stringof, T, hasUDA!(field, typeDSL), idMap_);
232     }
233 }
234 
235 version(unittest)
236 {
237     auto stripLeftAll(string str) @safe
238     {
239         import std.algorithm : joiner, map;
240         import std.array : array;
241         import std..string : split, stripLeft;
242         return str.split.map!stripLeft.joiner("\n").array;
243     }
244 }
245 
246 ///
247 @safe unittest
248 {
249     import salad.util : edig;
250 
251     enum fieldName = "strVariable";
252     Node n = [ fieldName: "string value" ];
253     string strVariable_;
254     enum exp = Assign!(n, strVariable_).stripLeftAll;
255     static assert(exp == q"EOS
256         import salad.util : edig;
257         import std.algorithm : map;
258         import std.array : array;
259         strVariable_ = n.edig("strVariable").as!string;
260 EOS".stripLeftAll, exp);
261 
262     mixin(exp);
263     assert(strVariable_ == "string value");
264 }
265 
266 /// optional of non-array type
267 @safe unittest
268 {
269     import std.exception : assertNotThrown;
270 
271     enum fieldName = "param";
272     Node n = [ fieldName: true ];
273     Optional!bool param_;
274     enum exp = Assign!(n, param_).stripLeftAll;
275     static assert(exp == q"EOS
276         import salad.util : edig;
277         import std.algorithm : map;
278         import std.array : array;
279         if (auto f = "param" in n)
280         {
281             param_ = (*f).as!bool;
282         }
283 EOS".stripLeftAll, exp);
284 
285     mixin(exp);
286     assert(param_.tryMatch!((bool b) => b)
287                  .assertNotThrown);
288 }
289 
290 /// optional of array type
291 unittest
292 {
293     import std.algorithm : map;
294     import std.array : array;
295     import std.exception : assertNotThrown;
296 
297     enum fieldName = "params";
298     Node n = [ fieldName: [1, 2, 3] ];
299     Optional!(int[]) params_;
300     enum exp = Assign!(n, params_).stripLeftAll;
301     static assert(exp == q"EOS
302         import salad.util : edig;
303         import std.algorithm : map;
304         import std.array : array;
305         if (auto f = "params" in n)
306         {
307             params_ = (*f).sequence.map!((a)
308             {
309                 int ret;
310                 ret = a.as!int;
311                 return ret;
312             }).array;
313         }
314 EOS".stripLeftAll, exp);
315 
316     mixin(exp);
317     assert(params_.tryMatch!((int[] arr) => arr)
318                   .assertNotThrown == [1, 2, 3]);
319 }
320 
321 template Assign_(string node, string field, T, bool typeDSL = false, idMap idMap_ = idMap.init)
322 if (!isSumType!T)
323 {
324     import std.format : format;
325     import std.traits : isArray, isSomeString;
326 
327     static if (!isSomeString!T && isArray!T)
328     {
329         import std.range : ElementType, empty;
330         import std..string : chomp;
331 
332         enum AssignBase = format!q"EOS
333             %s = %s.sequence.map!((a) {
334                 %s ret;
335                 %s
336                 return ret;
337             }).array;
338 EOS"(field, node, (ElementType!T).stringof, Assign_!("a", "ret", ElementType!T)).chomp;
339 
340         static if (idMap_.subject.empty)
341         {
342             enum Assign_ = AssignBase;
343         }
344         else
345         {
346             static if (idMap_.predicate.empty)
347             {
348                 enum Trans = format!q"EOS
349                     Node a_ = a.value;
350                     a_.add("%s", a.key);
351                     %s ret;
352                     %s
353                     return ret;
354 EOS"(idMap_.subject, (ElementType!T).stringof, Assign_!("a_", "ret", ElementType!T));
355             }
356             else
357             {
358                 enum Trans = format!q"EOS
359                     Node a_;
360                     a_.add("%1$s", a.key);
361                     if (a.value.type == NodeType.mapping && "%2$s" in a.value)
362                     {
363                         foreach(kv; a.value.mapping)
364                         {
365                             a_.add(kv.key, kv.value);
366                         }
367                     }
368                     else
369                     {
370                         a_.add("%2$s", a.value);
371                     }
372                     return %3$s;
373 EOS"(idMap_.subject, idMap_.predicate, ctorStr!(ElementType!T)("a_"));
374             }
375 
376             enum Assign_ = format!q"EOS
377                 if (%2$s.type == NodeType.sequence)
378                 {
379                     %3$s
380                 }
381                 else
382                 {
383                     %1$s = %2$s.mapping.map!((a) {
384                         %4$s
385                     }).array;
386                 }
387 EOS"(field, node, AssignBase, Trans);
388         }
389     }
390     else
391     {
392         static assert(idMap_ == idMap.init);
393         enum Assign_ = format!"%s = %s;"(field, ctorStr!T(node));
394     }
395 }
396 
397 template Assign_(string node, string field, T, bool typeDSL = false, idMap idMap_ = idMap.init)
398 if (isSumType!T)
399 {
400     import std.format : format;
401     static if (isOptional!T && T.Types.length == 2)
402     {
403         enum Assign_ = Assign_!(node, field, T.Types[1], typeDSL, idMap_);
404     }
405     else static if (isEither!T && T.Types.length == 1)
406     {
407         enum Assign_ = Assign_!(node, field, T[0], typeDSL, idMap_);
408     }
409     else
410     {
411         import std.traits : isSomeString;
412         import std.meta : Filter;
413 
414         static if (isOptional!T)
415         {
416             alias Types = T.Types[1..$];
417         }
418         else static if (isEither!T)
419         {
420             alias Types = T.Types;
421         }
422         static if (typeDSL && Filter!(isSomeString, Types).length > 0)
423         {
424             enum Pre = format!q"EOS
425                 Node n;
426                 if (%1$s.type == NodeType.string)
427                 {
428                     import std.algorithm : endsWith;
429                     auto s = %1$s.as!string;
430                     if (s.endsWith("[]?"))
431                     {
432                         n.add("null");
433                         n.add([
434                             "type": "array",
435                             "items": s[0..$-3],
436                         ]);
437                     }
438                     else if (s.endsWith("[]"))
439                     {
440                         n.add([
441                             "type": "array",
442                             "items": s[0..$-2],
443                         ]);
444                     }
445                     else if (s.endsWith("?"))
446                     {
447                         n.add("null");
448                         n.add(s[0..$-1]);
449                     }
450                     else
451                     {
452                         n = Node(s);
453                     }
454                 }
455                 else
456                 {
457                     n = %1$s;
458                 }
459 EOS"(node);
460         }
461         else
462         {
463             enum Pre = format!q"EOS
464                 Node n = %s;
465 EOS"(node);
466         }
467         enum Assign_ = format!q"EOS
468             {
469                 %s
470                 %s = (%s)(n);
471             }
472 EOS"(Pre, field, DispatchFun!(T, Types));
473     }
474 }
475 
476 template DispatchFun(RetType, Types...)
477 {
478     import std.format : format;
479     import std.meta : anySatisfy, Filter, staticMap;
480     import std.traits : isArray, isIntegral, isSomeString;
481 
482     enum isNonStringArray(T) = !isSomeString!T && isArray!T;
483     alias ArrayTypes = Filter!(isNonStringArray, Types);
484     static if (ArrayTypes.length == 0)
485     {
486         enum ArrayStatement = "";
487     }
488     else
489     {
490         enum ArrayStatement = ArrayDispatchStatement!(RetType, ArrayTypes);
491     }
492 
493     enum isRecord(T) = is(T == class) && !__traits(compiles, T.Types);
494     alias RecordTypes = Filter!(isRecord, Types);
495     static if (RecordTypes.length == 0)
496     {
497         enum RecordStatement = "";
498     }
499     else
500     {
501         enum RecordStatement = RecordDispatchStatement!(RetType, RecordTypes);
502     }
503 
504     enum isEnum(T) = is(T == class) && is(T.Types == enum);
505     alias EnumTypes = Filter!(isEnum, Types);
506     enum hasString = anySatisfy!(isSomeString, Types);
507     static if (EnumTypes.length == 0)
508     {
509         static if (hasString)
510         {
511             enum EnumStatement = format!q"EOS
512                 if (a.type == NodeType.string)
513                 {
514                     return %s(a.as!string);
515                 }
516 EOS"(RetType.stringof);
517         }
518         else
519         {
520             enum EnumStatement = "";
521         }
522     }
523     else
524     {
525         enum EnumStatement = EnumDispatchStatement!(RetType, hasString, EnumTypes);
526     }
527 
528     static if (Filter!(isIntegral, Types).length == 0)
529     {
530         enum NumStatement = "";
531     }
532     else
533     {
534         enum NumStatement = format!q"EOS
535                 if (a.type == NodeType.integer)
536                 {
537                     return %s(a.as!int);
538                 }
539 EOS"(RetType.stringof);
540     }
541 
542     static assert(Types.length == 
543         ArrayTypes.length + RecordTypes.length + EnumTypes.length + (hasString ? 1 : 0) + Filter!(isIntegral, Types).length,
544         format!"Internal error: Params: %s (%s) but Array: %s, Record: %s, Enum: %s, hasString: %s, Integer: %s"(
545             Types.stringof, Types.length, ArrayTypes.stringof, RecordTypes.stringof, EnumTypes.stringof,
546             hasString, Filter!(isIntegral, Types).stringof
547         ));
548 
549     import std.algorithm : filter, joiner;
550     import std.array : array;
551     import std.functional : not;
552     import std.range : empty;
553     enum FunBody = [
554         ArrayStatement,
555         RecordStatement,
556         EnumStatement,
557         NumStatement,
558         `throw new DocumentException("Unknown node type in DispatchFun", a);`
559     ].filter!(not!empty).joiner("else ").array;
560 
561     enum DispatchFun = format!q"EOS
562         (a) { %s }
563 EOS"(FunBody);
564 }
565 
566 template ArrayDispatchStatement(RetType, ArrayTypes...)
567 {
568     static if (ArrayTypes.length == 1)
569     {
570         import std.format : format;
571         import std.range : ElementType;
572         alias T = ElementType!(ArrayTypes[0]);
573         static if (isEither!T)
574         {
575             enum ArrayDispatchStatement = format!q"EOS
576                 if (a.type == NodeType.sequence)
577                 {
578                     return %s(a.sequence.map!(
579                         %s
580                     ).array);
581                 }
582 EOS"(RetType.stringof, DispatchFun!(T, T.Types));
583         }
584         else
585         {
586             enum ArrayDispatchStatement = format!q"EOS
587                 if (a.type == NodeType.sequence)
588                 {
589                     return %s(a.sequence.map!(a => %s).array);
590                 }
591 EOS"(RetType.stringof, ctorStr!T("a"));
592         }
593     }
594     else
595     {
596         // It is not used in CWL
597         static assert(false, "It is not supported");
598     }
599 }
600 
601 template RecordDispatchStatement(RetType, RecordTypes...)
602 {
603     import std.format : format;
604 
605     static if (RecordTypes.length == 1)
606     {
607         enum RecordDispatchStatement = format!q"EOS
608             if (a.type == NodeType.mapping)
609             {
610                 return %s(%s);
611             }
612 EOS"(RetType.stringof, ctorStr!(RecordTypes[0])("a"));
613     }
614     else
615     {
616         import std.algorithm : joiner;
617         import std.array : array;
618         import std.meta : ApplyLeft, Filter, staticMap, templateNot;
619         import std.traits : FieldNameTuple;
620 
621         enum ConstantMembersOf(T) = Filter!(ApplyLeft!(isConstantMember, T), FieldNameTuple!T);
622         enum RecordTypeName = ConstantMembersOf!(RecordTypes[0])[0];
623         enum isDispatchable(T) = ConstantMembersOf!T.length != 0 && ConstantMembersOf!T[0] == RecordTypeName;
624         alias NonDispatchableRecords = Filter!(templateNot!isDispatchable, RecordTypes);
625         static assert(NonDispatchableRecords.length <= 1,
626                       "There are too many non-dispatchable record candidates: "~NonDispatchableRecords.stringof);
627 
628         static if (NonDispatchableRecords.length == 0)
629         {
630             enum DefaultCaseStr = format!q"EOS
631             default: throw new DocumentException("Unknown record type: "~a.edig("%1$s").as!string, a.edig("%1$s"));
632 EOS"(RecordTypeName[0..$-1]);
633         }
634         else
635         {
636             enum DefaultCaseStr = format!q"EOS
637             default: return %s(%s);
638 EOS"(RetType.stringof, ctorStr!(NonDispatchableRecords[0])("a"));
639         }
640 
641         enum RecordCaseStr(T) = format!q"EOS
642             case "%s": return %s(%s);
643 EOS"(mixin("(new T)."~RecordTypeName), RetType.stringof, ctorStr!T("a"));
644 
645         enum RecordDispatchStatement = format!q"EOS
646             if (a.type == NodeType.mapping)
647             {
648                 switch(a.edig("%1$s").as!string)
649                 {
650                 %2$s
651                 %3$s
652                 }
653             }
654 EOS"(RecordTypeName[0..$-1],
655      [staticMap!(RecordCaseStr, Filter!(isDispatchable, RecordTypes))].joiner("").array,
656      DefaultCaseStr);
657     }
658 }
659 
660 template EnumDispatchStatement(RetType, bool hasString, EnumTypes...)
661 {
662     import std.algorithm : joiner, map;
663     import std.array : array;
664     import std.format : format;
665     import std.meta : staticMap;
666     import std.traits : EnumMembers;
667 
668     enum EnumCaseStr(T) = format!q"EOS
669         case %s: return %s(a.as!%s);
670 EOS"([EnumMembers!(T.Types)].map!(m => format!`"%s"`(cast(string)m))
671                             .joiner(", ")
672                             .array,
673      RetType.stringof, T.stringof);
674     static if (hasString)
675     {
676         enum DefaultStr = format!q"EOS
677             return %s(value);
678 EOS"(RetType.stringof);
679     }
680     else
681     {
682         enum DefaultStr = `throw new DocumentException("Unknown symbol value: "~a.as!string, a);`;
683     }
684     enum EnumDispatchStatement = format!q"EOS
685         if (a.type == NodeType.string)
686         {
687             auto value = a.as!string;
688             switch(value)
689             {
690             %1$s
691             default:
692             %2$s
693             }
694         }
695 EOS"([staticMap!(EnumCaseStr, EnumTypes)].joiner("").array, DefaultStr);
696 }