1 module render;
2 
3 import boilerplate;
4 import config;
5 import reverseResponseCodes;
6 import route;
7 import SchemaLoader : SchemaLoader;
8 import std.algorithm;
9 import std.array;
10 import std.exception;
11 import std.file;
12 import std.format;
13 import std.path;
14 import std.range;
15 import std.stdio;
16 import std.string;
17 import std.typecons;
18 import std.uni;
19 import std.utf;
20 import types;
21 
22 class Render
23 {
24     @(This.Default!(() => ["boilerplate"]))
25     string[] imports;
26 
27     @(This.Default)
28     string[] types;
29 
30     string modulePrefix;
31 
32     string[string] redirected;
33 
34     bool[string] typesBeingGenerated;
35 
36     // for resolving references when inlining
37     Type[][string] schemas;
38 
39     string renderObject(string key, const Type value, const SchemaConfig config, string description)
40     {
41         const name = key.keyToTypeName;
42 
43         if (auto objectType = cast(ObjectType) value)
44         {
45             return renderStruct(name, objectType, config, description);
46         }
47         if (auto allOf = cast(AllOf) value)
48         {
49             Type loadSchema(string target)
50             {
51                 const string relPath = target.until("#/").array.toUTF8;
52                 const string schemaName = target.find("#/").drop("#/".length);
53                 if (relPath.empty)
54                 {
55                     // try to find schema in own set.
56                     assert(schemaName in this.schemas, format!"%s missing in %s"(
57                         schemaName, this.schemas.keys));
58                     return this.schemas[schemaName].pickBestType;
59                 }
60                 // We don't usually look at reference targets, so
61                 // we manually invoke the loader just this once.
62                 const string path = value.source.dirName.buildNormalizedPath(relPath);
63                 auto loader = new SchemaLoader;
64                 auto file = loader.load(path);
65                 assert(schemaName in file.schemas, format!"%s missing in %s"(
66                     schemaName, file.schemas.keys));
67                 return file.schemas[schemaName];
68             }
69             Type[] flattenAllOf(AllOf allOf)
70             {
71                 Type[] result = null;
72                 foreach (child; allOf.children)
73                 {
74                     if (auto nextAllOf = cast(AllOf) child)
75                     {
76                         result ~= flattenAllOf(nextAllOf);
77                         continue;
78                     }
79                     if (auto refChild = cast(Reference) child)
80                     {
81                         auto schema = loadSchema(refChild.target);
82                         if (auto nextAllOf = cast(AllOf) schema) {
83                             result ~= flattenAllOf(nextAllOf);
84                             continue;
85                         }
86                     }
87                     result ~= child;
88                 }
89                 return result;
90             }
91             auto children = flattenAllOf(allOf);
92             auto refChildren = children.map!(a => cast(Reference) a).filter!"a".array;
93             auto objChildren = children.map!(a => cast(ObjectType) a).filter!"a".array;
94 
95             if (children.length == refChildren.length + objChildren.length)
96             {
97                 // generate object with all refs but one inlined
98                 auto substitute = new ObjectType(null, null);
99                 string extra = null;
100 
101                 substitute.setSource(value.source);
102                 /**
103                  * We can make exactly one ref child the alias-this.
104                  * How do we pick? Easy: Use the one with the most properties.
105                  */
106                 Reference refWithMostProperties = null;
107                 // Resolve all references in the course of looking for the fattest child
108                 Type[string] resolvedReferences;
109 
110                 foreach (ref child; children)
111                 {
112                     auto refChild = cast(Reference) child;
113                     if (!refChild)
114                         continue;
115                     auto schema = loadSchema(refChild.target);
116                     if (refChild.target.startsWith("#/"))
117                     {
118                         if (auto nextRef = cast(Reference) schema)
119                         {
120                             /**
121                              * Reference in the same file that points at another reference?
122                              * This must be the workaround for https://github.com/APIDevTools/swagger-cli/issues/59
123                              * Bypass the first reference entirely.
124                              */
125                             child = nextRef;
126                             refChild = nextRef;
127                             schema = loadSchema(nextRef.target);
128                         }
129                     }
130                     resolvedReferences[refChild.target] = schema;
131                     if (auto obj = cast(ObjectType) schema)
132                     {
133                         if (!refWithMostProperties)
134                         {
135                             refWithMostProperties = refChild;
136                         }
137                         else if (auto oldObj = cast(ObjectType) resolvedReferences[refWithMostProperties.target])
138                         {
139                             if (obj.properties.length > oldObj.properties.length)
140                             {
141                                 refWithMostProperties = refChild;
142                             }
143                         }
144                     }
145                 }
146 
147                 foreach (child; children)
148                 {
149                     if (auto obj = cast(ObjectType) child)
150                     {
151                         substitute.properties ~= obj.properties;
152                         substitute.required ~= obj.required;
153                     }
154                     else if (auto refChild = cast(Reference) child)
155                     {
156                         if (refChild is refWithMostProperties)
157                             continue;
158                         if (!refChild.target.canFind("#/"))
159                         {
160                             stderr.writefln!"Don't understand reference target %s"(refChild.target);
161                             assert(false);
162                         }
163 
164                         auto schema = resolvedReferences[refChild.target];
165 
166                         if (auto obj = cast(ObjectType) schema)
167                         {
168                             substitute.properties ~= obj.properties;
169                             substitute.required ~= obj.required;
170                         }
171                         else
172                         {
173                             stderr.writefln!"Reference %s target %s isn't an object; cannot inline"(
174                                 refChild, schema);
175                             assert(false);
176                         }
177                     }
178                     else assert(false);
179                 }
180                 if (refWithMostProperties)
181                 {
182                     // use it for the alias-this reference
183                     const fieldName = refWithMostProperties.target.keyToTypeName.asFieldName;
184 
185                     substitute.properties ~= TableEntry!Type(fieldName, refWithMostProperties);
186                     substitute.required ~= fieldName;
187                     extra = format!"alias %s this;"(fieldName);
188                 }
189                 return renderStruct(name, substitute, config, description, extra);
190             }
191         }
192         stderr.writefln!"ERR: not renderable %s; %s"(key, value.classinfo.name);
193         assert(false);
194     }
195 
196     string renderStruct(string name, ObjectType objectType, const SchemaConfig config, string description,
197         string extra = null)
198     {
199         const(string)[] invariants = config.invariant_;
200         string result;
201 
202         if (!description.empty)
203         {
204             result ~= description.renderComment(0, objectType.source);
205         }
206         result ~= format!"immutable struct %s\n{\n"(name);
207         string extraTypes, members;
208         foreach (tableEntry; objectType.properties)
209         {
210             const fieldName = tableEntry.key.fixReservedIdentifiers;
211 
212             if (!config.properties.empty && !config.properties.canFind(fieldName))
213                 continue;
214 
215             const required = objectType.required.canFind(tableEntry.key);
216             const optional = !required;
217             const allowNull = true;
218 
219             members ~= renderMember(fieldName, tableEntry.value,
220                 optional, allowNull, extraTypes);
221             members ~= "\n";
222         }
223         if (!objectType.additionalProperties.isNull)
224         {
225             Type elementType = objectType.additionalProperties.get.type;
226             Nullable!int minProperties = objectType.additionalProperties.get.minProperties;
227             const optional = false, allowNull = true;
228 
229             members ~= renderMember("additionalProperties", elementType, optional, allowNull, extraTypes, "[string]");
230             members ~= "\n";
231             if (!minProperties.isNull)
232             {
233                 invariants ~= format!"this.additionalProperties.length >= %s"(minProperties.get);
234             }
235         }
236 
237         result ~= extraTypes;
238         result ~= members;
239         foreach (invariant_; invariants)
240         {
241             result ~= format!"    invariant (%s);\n\n"(invariant_);
242         }
243         if (!extra.empty)
244         {
245             result ~= format!"    %s\n\n"(extra);
246         }
247         if (!objectType.required.filter!(a => config.properties.empty || config.properties.canFind(a)).empty)
248         {
249             // disabling this() on a struct with all-optional fields
250             // results in an unconstructable type
251             result ~= "    @disable this();\n\n";
252         }
253         if (!objectType.additionalProperties.isNull)
254         {
255             result ~= "    alias additionalProperties this;\n\n";
256         }
257         result ~= "    mixin(GenerateAll);\n";
258         result ~= "}\n";
259         return result;
260     }
261 
262     void renderEnum(string name, string[] members, string source, string description)
263     {
264         string result;
265 
266         if (!description.empty)
267         {
268             result ~= description.renderComment(0, source);
269         }
270         result ~= format!"enum %s\n{\n"(name);
271         foreach (member; members)
272         {
273             result ~= "    " ~ member.screamingSnakeToCamelCase.fixReservedIdentifiers ~ ",\n";
274         }
275         result ~= "}\n";
276         types ~= result;
277     }
278 
279     void renderIdType(string name, string source, string description)
280     {
281         string result;
282 
283         if (!description.empty)
284         {
285             result ~= description.renderComment(0, source);
286         }
287         result ~= format!"struct %s\n{\n"(name);
288         result ~= "    import util.IdType : IdType;\n\n";
289         result ~= format!"    mixin IdType!%s;\n"(name);
290         result ~= "}\n";
291         types ~= result;
292     }
293 
294     string renderMember(string name, Type type, bool optional, bool allowNull, ref string extraTypes,
295         string modifier = "")
296     {
297         if (auto booleanType = cast(BooleanType) type)
298         {
299             if (optional)
300             {
301                 assert(modifier == "");
302                 if (!booleanType.default_.isNull)
303                 {
304                     return format!"    @(This.Default!%s)\n    bool %s;\n"(booleanType.default_.get, name);
305                 }
306                 const fieldAllowNull = false;
307                 return format!"    @(This.Default)\n    %s %s;\n"(nullableType("bool", "", fieldAllowNull), name);
308             }
309             return format!"    bool%s %s;\n"(modifier, name);
310         }
311         string renderDType(string dType)
312         {
313             if (optional)
314             {
315                 const nullableDType = nullableType(dType, modifier, allowNull);
316 
317                 return format!"    @(This.Default)\n    %s %s;\n"(nullableDType, name);
318             }
319             return format!"    %s%s %s;\n"(dType, modifier, name);
320         }
321         if (auto numberType = cast(NumberType) type)
322         {
323             return renderDType("double");
324         }
325         if (auto integerType = cast(IntegerType) type)
326         {
327             return renderDType(integerType.toDType);
328         }
329         if (auto stringType = cast(StringType) type)
330         {
331             string udaPrefix = "";
332             if (!stringType.minLength.isNull && stringType.minLength.get == 1)
333             {
334                 udaPrefix = "    @NonEmpty\n";
335             }
336 
337             auto result = resolveSimpleStringType(stringType);
338 
339             string actualType = "string";
340             if (!result.isNull)
341             {
342                 actualType = result.get.typeName;
343                 imports ~= result.get.imports;
344             }
345             else if (!stringType.enum_.empty)
346             {
347                 actualType = name.capitalize;
348                 extraTypes ~= format!"    enum %s\n    {\n"(actualType);
349                 foreach (member; stringType.enum_)
350                 {
351                     extraTypes ~= "        " ~ member.screamingSnakeToCamelCase.fixReservedIdentifiers ~ ",\n";
352                 }
353                 extraTypes ~= "    }\n\n";
354             }
355 
356             if (optional)
357             {
358                 return format!"%s    @(This.Default)\n    %s %s;\n"(
359                     udaPrefix, nullableType(actualType, modifier, allowNull), name);
360             }
361             return format!"%s    %s%s %s;\n"(udaPrefix, actualType, modifier, name);
362         }
363         if (auto objectType = cast(ObjectType) type)
364         {
365             if (objectType.properties.empty)
366             {
367                 // string[string] object
368                 if (objectType.additionalProperties.apply!(a => cast(StringType) a.type !is null).get(false))
369                 {
370                     string prefix = null;
371                     string typeStr = "string[string]";
372                     if (optional)
373                     {
374                         prefix ~= "    @(This.Default)\n";
375                         typeStr = nullableType("string", "[string]", allowNull);
376                     }
377                     if (objectType.additionalProperties.get.minProperties.apply!(a => a == 1).get(false))
378                     {
379                         prefix ~= "    @NonEmpty\n";
380                     }
381                     return format!"%s    %s%s %s;\n"(prefix, typeStr, modifier, name);
382                 }
383                 imports ~= "std.json";
384                 if (optional)
385                 {
386                     return format!"    @(This.Default)\n    %s %s;\n"(
387                         nullableType("JSONValue", modifier, allowNull), name);
388                 }
389                 return format!"    JSONValue%s %s;\n"(modifier, name);
390             }
391         }
392         if (auto arrayType = cast(ArrayType) type)
393         {
394             // if we want an invariant, we must allow Nullable.
395             const allowElementNull = arrayType.minItems.isNull;
396             const member = renderMember(name, arrayType.items, optional, allowElementNull, extraTypes, modifier ~ "[]");
397 
398             if (!arrayType.minItems.isNull)
399             {
400                 if (arrayType.minItems.get == 1)
401                 {
402                     return "    @NonEmpty\n" ~ member;
403                 }
404                 if (arrayType.minItems.get > 1)
405                 {
406                     return member ~ format!"\n    invariant (this.%s.length >= %s);\n"(name, arrayType.minItems.get);
407                 }
408             }
409             return member;
410         }
411         if (auto reference = cast(Reference) type)
412         {
413             string tryInline()
414             {
415                 if (!reference.target.canFind("#/")) return null;
416 
417                 const targetSchema = reference.target.find("#/").drop("#/".length);
418 
419                 if (targetSchema !in this.schemas) return null;
420 
421                 const typeName = reference.target.keyToTypeName;
422 
423                 if (!matchingImports(typeName).empty)
424                 {
425                     return null;
426                 }
427 
428                 auto schema = this.schemas[targetSchema].pickBestType;
429                 string inlineModifier = modifier;
430                 // TODO factor out into helper (compare app.d toplevel simple-schema resolution)
431                 while (auto arrayType = cast(ArrayType) schema)
432                 {
433                     schema = arrayType.items;
434                     inlineModifier ~= "[]";
435                 }
436                 if (auto stringType = cast(StringType) schema)
437                 {
438                     if (!stringType.enum_.empty || typeName.endsWith("Id")) return null;
439                 }
440                 else if (cast(BooleanType) schema || cast(NumberType) schema || cast(IntegerType) schema)
441                 {
442                     // inline alias
443                 }
444                 else
445                 {
446                     return null;
447                 }
448 
449                 // inline alias
450                 return renderMember(name, schema, optional, allowNull, extraTypes, inlineModifier);
451             }
452 
453             if (auto result = tryInline)
454             {
455                 return result;
456             }
457 
458             const result = resolveReference(reference);
459 
460             if (!result.import_.isNull)
461             {
462                 imports ~= result.import_.get;
463             }
464             const typeName = result.typeName;
465 
466             if (optional)
467             {
468                 return format!"    @(This.Default)\n    %s %s;\n"(nullableType(typeName, modifier, allowNull), name);
469             }
470             return format!"    %s%s %s;\n"(typeName, modifier, name);
471         }
472 
473         // render as subtype
474         const capitalizedName = name.capitalizeFirst;
475         const typeName = modifier.isArrayModifier ? capitalizedName.singularize : capitalizedName;
476 
477         extraTypes ~= renderObject(typeName, type, SchemaConfig(), null).indent ~ "\n";
478         if (optional)
479         {
480             return format!"    @(This.Default)\n    %s %s;\n"(nullableType(typeName, modifier, allowNull), name);
481         }
482         return format!"    %s%s %s;\n"(typeName, modifier, name);
483     }
484 
485     string renderRoutes(string name, string source, string description, const Route[] routes,
486         const Parameter[string] parameterComponents)
487     {
488         string[] lines;
489         string[] extraTypes;
490 
491         imports ~= "messaging.Context : Context";
492         imports ~= "net.http.ResponseCode";
493         imports ~= "net.rest.Method";
494 
495         lines ~= "/**";
496         lines ~= linebreak(" * ", " * ", ["This boundary interface has been generated from ", source ~ ":"]);
497         lines ~= description.split("\n").map!strip.strip!(a => a.empty).map!(a => stripRight(" * " ~ a)).array;
498         lines ~= " */";
499         lines ~= format!"interface %s"(name);
500         lines ~= "{";
501         foreach (i, route; routes)
502         {
503             const availableParameters = route.parameters
504                 .map!(a => resolveParameter(a, parameterComponents))
505                 .filter!(a => a.in_ == "path")
506                 .array;
507 
508             const(ValueParameter) findParameterWithName(string name)
509             {
510                 enforce(availableParameters.any!(a => a.name == name),
511                     format!"route parameter with name \"%s\" not found"(name));
512                 return availableParameters.find!(a => a.name == name).front;
513             }
514 
515             const urlParameters = route.url.split("/")
516                 .filter!(a => a.startsWith("{") && a.endsWith("}"))
517                 .map!(name => findParameterWithName(name.dropOne.dropBackOne))
518                 .array;
519             string[] dParameters = null;
520 
521             void addDParameter(const Type type, const string name, bool required = true)
522             {
523                 if (!required)
524                 {
525                     imports ~= "std.typecons";
526                 }
527                 if (auto refType = cast(Reference) type)
528                 {
529                     const result = resolveReference(refType);
530                     const typeName = required ? result.typeName : format!"Nullable!%s"(result.typeName);
531 
532                     if (!result.import_.isNull)
533                     {
534                         imports ~= result.import_.get(null);
535                     }
536                     dParameters ~= format!"const %s %s"(typeName, name);
537                 }
538                 else if (auto strType = cast(StringType) type)
539                 {
540                     auto stringType = "string";
541                     const result = resolveSimpleStringType(strType);
542                     if (!result.isNull)
543                     {
544                         stringType = result.get.typeName;
545                         imports ~= result.get.imports;
546                     }
547                     dParameters ~= format!"const %s%s %s"(required ? "" : "Nullable!", stringType, name);
548                 }
549                 else
550                 {
551                     assert(false, format!"Type currently unsupported for URL parameters: %s"(type));
552                 }
553             }
554 
555             foreach (urlParameter; urlParameters)
556             {
557                 addDParameter(urlParameter.schema, urlParameter.name);
558             }
559 
560             string typeToString(Type type)
561             {
562                 if (type is null) return "void";
563                 if (auto refType = cast(Reference) type)
564                 {
565                     const result = resolveReference(refType);
566 
567                     if (!result.import_.isNull)
568                     {
569                         imports ~= result.import_.get;
570                     }
571                     return result.typeName;
572                 }
573                 if (auto integerType = cast(IntegerType) type)
574                 {
575                     return integerType.toDType;
576                 }
577                 if (auto stringType = cast(StringType) type)
578                 {
579                     if (stringType.enum_.empty && stringType.format_.isNull)
580                     {
581                         return "string";
582                     }
583                 }
584                 if (auto arrayType = cast(ArrayType) type)
585                 {
586                     return typeToString(arrayType.items) ~ "[]";
587                 }
588                 const bodyType = route.operationId.capitalizeFirst;
589 
590                 extraTypes ~= "\n" ~ renderObject(bodyType, type, SchemaConfig(), null);
591                 return bodyType;
592             }
593 
594             const string bodyType = typeToString(cast() route.schema);
595 
596             if (bodyType != "void")
597             {
598                 dParameters ~= "const " ~ bodyType;
599             }
600 
601             const queryParameters = route.parameters
602                 .map!(a => cast(ValueParameter) a)
603                 .filter!"a !is null"
604                 .filter!(a => a.in_ == "query")
605                 .array;
606 
607             foreach (queryParameter; queryParameters)
608             {
609                 addDParameter(queryParameter.schema, queryParameter.name, queryParameter.required.get(false));
610             }
611 
612             string urlWithQueryParams = route.url;
613 
614             if (!queryParameters.empty)
615             {
616                 urlWithQueryParams ~= "?" ~ queryParameters.map!(a => format!"%s={%s}"(a.name, a.name)).join("&");
617             }
618 
619             if (i > 0) lines ~= "";
620             lines ~= route.description.strip.split("\n").strip!(a => a.empty).renderComment(4);
621             lines ~= linebreak((4).spaces, (12).spaces, [
622                 format!"@(Method.%s!("(route.method.capitalizeFirst),
623                 "JsonFormat, ",
624                 format!"%s, "(route.schema ? bodyType : "void"),
625                 format!"\"%s\"))"(urlWithQueryParams),
626             ]);
627             Type responseType = null;
628             foreach (response; route.responses)
629             {
630                 if (response.code.startsWith("2")) {
631                     assert(responseType is null);
632                     responseType = cast() response.schema;
633                     continue;
634                 }
635                 // produced by the networking lib
636                 if (response.code == "422") continue;
637 
638                 enforce(response.schema is null, "Error response cannot return body");
639 
640                 const member = codeToMember(response.code);
641                 const exception = pickException(response.code);
642 
643                 lines ~= format!"    @(Throws!(%s, ResponseCode.%s.expand))"(exception, member);
644             }
645             const string returnType = typeToString(responseType);
646 
647             lines ~= linebreak((4).spaces, (8).spaces, [
648                 format!"public %s %s("(returnType, route.operationId),
649             ] ~ dParameters.map!(a => a ~ ", ").array ~ [
650                 "const Context context);"
651             ]);
652         }
653         lines ~= "}";
654         // retro to counteract retro in app.d (sorry)
655         types ~= extraTypes.retro.array;
656         return lines.join("\n") ~ "\n";
657     }
658 
659     private string pickException(string responseCode)
660     {
661         string from(string package_, string member)
662         {
663             imports ~= package_;
664             return member;
665         }
666         switch (responseCode)
667         {
668             case "404": return from("util.NoSuchElementException", "NoSuchElementException");
669             case "409": return from("util.IllegalArgumentException", "IllegalArgumentException");
670             case "422": return from("util.IllegalArgumentException", "IllegalArgumentException");
671             case "503": return from("util.ServiceUnavailableException", "ServiceUnavailableException");
672             case "512": return from("util.ConcurrentModificationException", "ConcurrentModificationException");
673             default: return from("std.exception", "Exception");
674         }
675     }
676 
677     Tuple!(string, "typeName", Nullable!string, "import_") resolveReference(const Reference reference)
678     {
679         const typeName = reference.target.keyToTypeName;
680         if (typeName in this.redirected)
681         {
682             return typeof(return)(typeName, this.redirected[typeName].nullable);
683         }
684         if (typeName in this.typesBeingGenerated)
685         {
686             return typeof(return)(typeName, Nullable!string(this.modulePrefix ~ "." ~ typeName));
687         }
688         return .resolveReference(reference);
689     }
690 
691     private string nullableType(string type, string modifier, bool allowNullInit)
692     {
693         if (allowNullInit && modifier.isArrayModifier)
694         {
695             // we can just use the type itself as the nullable type
696             return type ~ modifier;
697         }
698         imports ~= "std.typecons";
699         if (modifier.empty)
700         {
701             return format!"Nullable!%s"(type);
702         }
703         return format!"Nullable!(%s%s)"(type, modifier);
704     }
705 
706     mixin(GenerateThis);
707 }
708 
709 Nullable!(Tuple!(string, "typeName", string[], "imports")) resolveSimpleStringType(const StringType type)
710 {
711     if (!type.enum_.empty)
712     {
713         return typeof(return)();
714     }
715     if (type.format_ == "date-time")
716     {
717         return tuple!("typeName", "imports")("SysTime", ["std.datetime"]).nullable;
718     }
719     if (type.format_ == "date")
720     {
721         return tuple!("typeName", "imports")("Date", ["std.datetime"]).nullable;
722     }
723     if (type.format_ == "duration")
724     {
725         return tuple!("typeName", "imports")("Duration", ["std.datetime"]).nullable;
726     }
727     return typeof(return)();
728 }
729 
730 // If we have both a type definition for X and a link to X in another yml,
731 // then ignore the reference declarations.
732 Type pickBestType(Type[] list)
733 {
734     auto nonReference = list.filter!(a => !cast(Reference) a);
735 
736     if (!nonReference.empty)
737     {
738         return nonReference.front;
739     }
740     return list.front;
741 }
742 
743 // Given a list of fragments, linebreak and indent them to avoid exceeding 120 columns per line.
744 private string[] linebreak(string firstLineIndent, string restLineIndent, string[] fragments)
745 {
746     string[] lines = null;
747     string line = null;
748 
749     string lineIndent() { return lines.empty ? firstLineIndent : restLineIndent; }
750 
751     void flush()
752     {
753         if (line.empty) return;
754         lines ~= (lineIndent ~ line.stripLeft).stripRight;
755         line = null;
756     }
757 
758     foreach (fragment; fragments)
759     {
760         if (lineIndent.length + line.length + fragment.length > 120) flush;
761         line ~= fragment;
762     }
763     flush;
764     return lines;
765 }
766 
767 private const(ValueParameter) resolveParameter(const Parameter param, const Parameter[string] components)
768 {
769     if (auto reference = cast(RefParameter) param)
770     {
771         enforce(reference.target.startsWith("#/"), format!"cannot resolve indirect $ref parameter (TODO) \"%s\""(
772             reference.target));
773 
774         const target = reference.target["#/".length .. $];
775 
776         enforce(target in components,
777             format!"cannot find target for $ref parameter \"%s\""(reference.target));
778         return components[target].resolveParameter(components);
779     }
780     if (auto valParameter = cast(ValueParameter) param)
781     {
782         return valParameter;
783     }
784     assert(false, format!"Unknown parameter type %s"(param.classinfo.name));
785 }
786 
787 alias Resolution = Tuple!(string, "typeName", Nullable!string, "import_");
788 
789 __gshared const(string)[] allFiles;
790 __gshared const(string)[][string] moduleCache;
791 __gshared Object cacheLock;
792 
793 shared static this()
794 {
795     cacheLock = new Object;
796     allFiles = dirEntries("src", "*.d", SpanMode.depth)
797         .chain(dirEntries("include", "*.d", SpanMode.depth))
798         .filter!(file => !file.name.endsWith("Test.d"))
799         .map!(a => a.readText)
800         .array;
801 }
802 
803 private Resolution resolveReference(const Reference reference)
804 {
805     const typeName = reference.target.keyToTypeName;
806     const matchingImports = .matchingImports(typeName);
807 
808     if (matchingImports.empty)
809     {
810         stderr.writefln!"WARN: no import found for type %s"(reference.target);
811     }
812 
813     if (matchingImports.length > 1)
814     {
815         stderr.writefln!"WARN: multiple module sources for %s: %s, using %s"(
816             reference.target, matchingImports, matchingImports.front);
817     }
818 
819     return Resolution(typeName, matchingImports.empty ? Nullable!string() : matchingImports.front.nullable);
820 }
821 
822 private const(string)[] matchingImports(const string typeName)
823 {
824     synchronized (cacheLock)
825     {
826         if (auto ptr = typeName in moduleCache)
827         {
828             return *ptr;
829         }
830 
831         const matches = allFiles
832             .filter!(a => a.canFind(format!"struct %s\n"(typeName))
833                 || a.canFind(format!"enum %s\n"(typeName)))
834             .map!(a => a.find("module ").drop("module ".length).until(";").toUTF8)
835             .array;
836 
837         moduleCache[typeName] = matches;
838         return matches;
839     }
840 }
841 
842 private string renderComment(string comment, int indent, string source)
843 {
844     const lines = [format!"This value object has been generated from %s:"(source)] ~ comment
845         .strip
846         .split("\n")
847         .strip!(a => a.empty);
848 
849     return renderComment(lines, indent).join("\n") ~ "\n";
850 }
851 
852 private string[] renderComment(const string[] lines, int indent)
853 {
854     const spacer = ' '.repeat(indent).array;
855 
856     return [format!"%s/**"(spacer)]
857         ~ lines.map!(line => format!"%s * %s"(spacer, line).stripRight).array
858         ~ format!"%s */"(spacer);
859 }
860 
861 private bool isArrayModifier(string modifier)
862 {
863     return modifier.endsWith("[]");
864 }
865 
866 public alias keyToTypeName = target => target.split("/").back;
867 
868 public alias asFieldName = type => chain(type.front.toLower.only, type.dropOne).toUTF8;
869 
870 unittest
871 {
872     assert("Foo".asFieldName == "foo");
873     assert("FooBar".asFieldName == "fooBar");
874 }
875 
876 private alias screamingSnakeToCamelCase = a => a
877     .split("_")
878     .map!toLower
879     .capitalizeAllButFirst
880     .join;
881 
882 unittest
883 {
884     assert("FOO".screamingSnakeToCamelCase == "foo");
885     assert("FOO_BAR".screamingSnakeToCamelCase == "fooBar");
886 }
887 
888 private alias capitalizeAllButFirst = range => chain(range.front.only, range.drop(1).map!capitalize);
889 
890 private alias capitalizeFirst = range => chain(range.front.toUpper.only, range.drop(1)).toUTF8;
891 
892 // Quick and dirty plural to singular conversion.
893 private string singularize(string name)
894 {
895     if (name.endsWith("s"))
896     {
897         return name.dropBack(1);
898     }
899     return name;
900 }
901 
902 private string fixReservedIdentifiers(string name)
903 {
904     switch (name)
905     {
906         static foreach (identifier; reservedIdentifiers)
907         {
908         case identifier:
909             return identifier ~ "_";
910         }
911         default:
912             return name;
913     }
914 }
915 
916 private enum reservedIdentifiers = [
917     "abstract", "alias", "align", "asm", "assert", "auto",
918     "body", "bool", "break", "byte",
919     "case", "cast", "catch", "cdouble", "cent", "cfloat", "char", "class", "const", "continue", "creal",
920     "dchar", "debug", "default", "delegate", "delete", "deprecated", "do", "double",
921     "else", "enum", "export", "extern",
922     "false", "final", "finally", "float", "for", "foreach", "foreach_reverse", "function",
923     "goto",
924     "idouble", "if", "ifloat", "immutable", "import", "in", "inout", "int", "interface", "invariant", "ireal", "is",
925     "lazy", "long",
926     "macro", "mixin", "module",
927     "new", "nothrow", "null",
928     "out", "override",
929     "package", "pragma", "private", "protected", "public", "pure",
930     "real", "ref", "return",
931     "scope", "shared", "short", "static", "struct", "super", "switch", "synchronized",
932     "template", "this", "throw", "true", "try", "typeid", "typeof",
933     "ubyte", "ucent", "uint", "ulong", "union", "unittest", "ushort",
934     "version", "void",
935     "wchar", "while", "with",
936 ];
937 
938 private string indent(string text)
939 {
940     string indentLine(string line)
941     {
942         return (4).spaces ~ line;
943     }
944 
945     return text
946         .split("\n")
947         .map!(a => a.empty ? a : indentLine(a))
948         .join("\n");
949 }
950 
951 private alias spaces = i => ' '.repeat(i).array.idup;