1 module app;
2 
3 import argparse;
4 import boilerplate;
5 import config;
6 import dyaml;
7 import render;
8 import route;
9 import SchemaLoader : OpenApiFile, SchemaLoader;
10 import std.algorithm;
11 import std.array;
12 import std.file;
13 import std.json;
14 import std.path;
15 import std.range;
16 import std.stdio;
17 import std.string;
18 import std.typecons;
19 import std.uni;
20 import std.utf;
21 import text.json.Decode;
22 import ToJson;
23 import types;
24 
25 @(Command.Description("Convert OpenAPI specification to serialized-ready D structs"))
26 struct Arguments
27 {
28     @(PositionalArgument(0).Description("openapi-to-d configuration file"))
29     string config;
30 }
31 
32 mixin CLI!Arguments.main!((const Arguments arguments)
33 {
34     auto loader = new SchemaLoader;
35     auto config = loadConfig(arguments.config);
36     Type[][string] schemas;
37     string[] keysInOrder;
38     OpenApiFile[] files = config.source.map!(a => loader.load(a)).array;
39     string[string] redirects;
40 
41     foreach (file; files)
42     {
43         foreach (key, type; file.schemas)
44         {
45             type.setSource(file.path);
46             schemas[key] ~= type;
47             keysInOrder ~= key;
48         }
49     }
50 
51     // ignore schema types that are pure references; they're just a hack
52     // to work around https://github.com/APIDevTools/swagger-cli/issues/59
53     auto allKeysSet = keysInOrder
54         .filter!(key => !cast(Reference) schemas[key].pickBestType)
55         .filter!(key => config.schemas.get(key.keyToTypeName, SchemaConfig()).include)
56         .map!(key => tuple!("key", "value")(key.keyToTypeName, true))
57         .assocArray;
58     foreach (key, schema; config.schemas)
59     {
60         if (schema.module_.isNull) continue;
61         redirects[key] = schema.module_.get;
62     }
63 
64     // write domain files
65     foreach (key; keysInOrder)
66     {
67         auto types = schemas[key];
68         auto type = pickBestType(types);
69         const name = key.keyToTypeName;
70         auto schemaConfig = config.schemas.get(name, SchemaConfig());
71 
72         if (!schemaConfig.include || !schemaConfig.module_.isNull)
73             continue;
74 
75         while (auto arrayType = cast(ArrayType) type)
76         {
77             type = arrayType.items;
78         }
79 
80         auto render = new Render(config.componentFolder.pathToModule, redirects, allKeysSet, schemas);
81 
82         bool rendered = false;
83 
84         if (auto objectType = cast(ObjectType) type)
85         {
86             // Check if this is an event type with a 'data' field
87             if (schemaConfig.isEventType && objectType.findKey("data"))
88             {
89                 render.types ~= render.renderObject(key, objectType.findKey("data"), schemaConfig, type.description);
90             }
91             else
92             {
93                 render.types ~= render.renderObject(key, type, schemaConfig, type.description);
94             }
95             rendered = true;
96         }
97         else if (auto stringType = cast(StringType) type)
98         {
99             if (!stringType.enum_.empty)
100             {
101                 render.renderEnum(name, stringType.enum_, type.source, type.description);
102                 rendered = true;
103             }
104             else if (name.endsWith("Id"))
105             {
106                 render.renderIdType(name, type.source, type.description);
107                 rendered = true;
108             }
109             else
110             {
111                 // will be inlined
112                 continue;
113             }
114         }
115         else if (cast(BooleanType) type || cast(NumberType) type || cast(IntegerType) type)
116         {
117             // will be inlined
118             continue;
119         }
120         else if (auto allOf = cast(AllOf) type)
121         {
122             foreach (child; allOf.children)
123             {
124                 if (auto objectType = cast(ObjectType) child)
125                 {
126                     // Check if this is an event type with a 'data' field
127                     if (schemaConfig.isEventType && objectType.findKey("data"))
128                     {
129                         render.types ~= render.renderObject(key, objectType.findKey("data"), schemaConfig, type.description);
130                         rendered = true;
131                         break;
132                     }
133                 }
134             }
135             if (!rendered)
136             {
137                 const references = allOf.children.map!(a => cast(Reference) a).filter!"a".array;
138                 const objects = allOf.children.map!(a => cast(ObjectType) a).filter!"a".array;
139 
140                 if (allOf.children.length == references.length + objects.length)
141                 {
142                     // Any mix of refs and objects: refs are just inlined into the object.
143                     render.types ~= render.renderObject(key, type, schemaConfig, type.description);
144                     rendered = true;
145                 }
146             }
147         }
148         else if (auto reference = cast(Reference) type)
149         {
150             // do nothing, we'll get it another way
151             continue;
152         }
153         if (!rendered)
154         {
155             stderr.writefln!"Cannot render value for type %s: %s"(name, type.classinfo.name);
156             return 1;
157         }
158 
159         auto outputPath = buildPath(config.componentFolder, name ~ ".d");
160         auto outputFile = File(outputPath, "w");
161 
162         outputFile.writefln!"// GENERATED FILE, DO NOT EDIT!";
163         outputFile.writefln!"module %s;"(outputPath.stripExtension.pathToModule);
164         outputFile.writefln!"";
165 
166         foreach (import_; render.imports.sort.uniq)
167         {
168             outputFile.writefln!"import %s;"(import_);
169         }
170         outputFile.writefln!"";
171         foreach (generatedType; render.types.retro)
172         {
173             outputFile.write(generatedType);
174         }
175         outputFile.close;
176     }
177     // write service files
178     if (!config.serviceFolder.empty)
179     {
180         foreach (file; files)
181         {
182             auto routes = file.routes
183                 .filter!(a => config.operations.get(a.operationId, OperationConfig()).include)
184                 .array;
185 
186             if (routes.empty)
187                 continue;
188 
189             const packagePrefix = config.serviceFolder.pathToModule;
190             const name = file.path.baseName.stripExtension.kebabToCamelCase;
191             const module_ = only(packagePrefix, name).join(".");
192             const outputPath = buildPath(config.serviceFolder, name ~ ".d");
193             auto outputFile = File(outputPath, "w");
194 
195             auto render = new Render(config.componentFolder.pathToModule, redirects, allKeysSet, schemas);
196 
197             render.types ~= render.renderRoutes(name, file.path, file.description, routes, file.parameters);
198 
199             // TODO render method writeToFile
200             outputFile.writefln!"// GENERATED FILE, DO NOT EDIT!";
201             outputFile.writefln!"module %s;"(module_);
202             outputFile.writefln!"";
203 
204             foreach (import_; render.imports.sort.uniq)
205             {
206                 outputFile.writefln!"import %s;"(import_);
207             }
208             outputFile.writefln!"";
209             foreach (generatedType; render.types.retro)
210             {
211                 outputFile.write(generatedType);
212             }
213 
214             outputFile.close;
215         }
216     }
217 
218     return 0;
219 });
220 
221 private string pathToModule(string path)
222 {
223     return path
224         .split("/")
225         .filter!(a => !a.empty)
226         .removeLeading("src")
227         .removeLeading("export")
228         .join(".");
229 }
230 
231 private alias removeLeading = (range, element) => choose(range.front == element, range.dropOne, range);
232 
233 alias valueObjectify = (string[] range) => range.front.valueObjectify.only.chain(range.dropOne).array;
234 alias valueObjectify = (string line) => format!"This immutable value type represents %s"(
235     line.front.toLower.only.chain(line.dropOne));
236 
237 private alias kebabToCamelCase = text => text
238     .splitter("-")
239     .map!capitalizeFirst
240     .join;
241 
242 unittest
243 {
244     assert("foo".kebabToCamelCase == "Foo");
245     assert("foo-bar".kebabToCamelCase == "FooBar");
246 }
247 
248 private alias capitalizeFirst = range => chain(range.front.toUpper.only, range.drop(1)).toUTF8;