1 module SchemaLoader;
2 
3 import boilerplate;
4 import dyaml;
5 import route;
6 import std.algorithm;
7 import std.array;
8 import std.format;
9 import std.json;
10 import std.path;
11 import std.string;
12 import std.typecons;
13 import text.json.Decode;
14 import ToJson;
15 import types;
16 
17 class SchemaLoader
18 {
19     OpenApiFile[string] files;
20 
21     OpenApiFile load(string path)
22     {
23         path = path.asNormalizedPath.array;
24         if (path !in this.files)
25         {
26             const root = Loader.fromFile(path).load;
27             const JSONValue jsonValue = root.toJson(Yes.ordered);
28             Route[] routes = null;
29             Type[string] schemas = null;
30             Parameter[string] parameters = null;
31 
32             if (jsonValue.hasKey("paths"))
33             {
34                 foreach (JSONValue pair; jsonValue.getEntry("paths").array)
35                 {
36                     const url_ = pair["key"].str;
37                     const routeJson = pair["value"].toObject.object;
38 
39                     auto routeParametersJson = routeJson.get("parameters", JSONValue((JSONValue[]).init));
40                     auto routeParameters_ = routeParametersJson.decodeJson!(Parameter[], route.decode);
41 
42                     foreach (string method_, JSONValue endpoint; routeJson)
43                     {
44                         if (method_ == "parameters")
45                             continue;
46 
47                         auto routeDto = endpoint.toObject.decodeJson!(RouteDto, route.decode);
48 
49                         Response[] responses_;
50                         foreach (string code, JSONValue response; endpoint.getEntry("responses").toObject.object)
51                         {
52                             Type schema = null;
53                             if (response.hasKey("content", "application/json"))
54                             {
55                                 schema = response.getEntry("content")
56                                     .getEntry("application/json")
57                                     .getEntry("schema")
58                                     .decodeJson!(Type, types.decode);
59                             }
60                             else if (response.hasKey("content"))
61                             {
62                                 // unknown content
63                                 schema = new ArrayType(new IntegerType("int8".nullable));
64                             }
65                             responses_ ~= Response(code, schema);
66                         }
67 
68                         Type schema_ = null;
69                         if (endpoint.hasKey("requestBody", "content", "application/json"))
70                         {
71                             schema_ = endpoint.getEntry("requestBody")
72                                 .getEntry("content")
73                                 .getEntry("application/json")
74                                 .getEntry("schema")
75                                 .decodeJson!(Type, types.decode);
76                         }
77                         else if (endpoint.hasKey("requestBody", "content"))
78                         {
79                             // unknown content
80                             schema_ = new ArrayType(new IntegerType("int8".nullable));
81                         }
82 
83                         with (Route.Builder())
84                         {
85                             url = url_;
86                             method = method_;
87                             description = mergeComments(routeDto.summary, routeDto.description);
88                             operationId = routeDto.operationId;
89                             schema = schema_;
90                             parameters = routeParameters_ ~ routeDto.parameters;
91                             responses = responses_;
92                             routes ~= builderValue;
93                         }
94                     }
95                 }
96             }
97 
98             string description = null;
99             if (jsonValue.hasKey("info") && jsonValue.getEntry("info").hasKey("description"))
100             {
101                 description = jsonValue.getEntry("info").getEntry("description").str;
102             }
103 
104             foreach (JSONValue pair; jsonValue.getEntry("components").getEntry("schemas").array)
105             {
106                 const key = pair["key"].str;
107 
108                 schemas[format!"components/schemas/%s"(key)] = pair["value"]
109                     .decodeJson!(Type, types.decode);
110             }
111             if (jsonValue.getEntry("components").hasKey("parameters"))
112             {
113                 foreach (JSONValue pair; jsonValue.getEntry("components").getEntry("parameters").array)
114                 {
115                     const key = pair["key"].str;
116 
117                     parameters[format!"components/parameters/%s"(key)] = pair["value"]
118                         .decodeJson!(Parameter, route.decode);
119                 }
120             }
121             this.files[path] = new OpenApiFile(path, description, routes, schemas, parameters);
122         }
123         return this.files[path];
124     }
125 
126     mixin(GenerateToString);
127 }
128 
129 class OpenApiFile
130 {
131     string path;
132 
133     string description;
134 
135     Route[] routes;
136 
137     Type[string] schemas;
138 
139     Parameter[string] parameters;
140 
141     mixin(GenerateAll);
142 }
143 
144 struct RouteDto
145 {
146     @(This.Default)
147     Nullable!string description;
148 
149     @(This.Default)
150     Nullable!string summary;
151 
152     string operationId;
153 
154     @(This.Default)
155     Parameter[] parameters;
156 
157     mixin(GenerateAll);
158 }
159 
160 private string mergeComments(const Nullable!string first, const Nullable!string second)
161 {
162     const string firstString = first.get(null).strip;
163     const string secondString = second.get(null).strip;
164     if (secondString.empty)
165     {
166         return firstString;
167     }
168     if (firstString.empty)
169     {
170         return secondString;
171     }
172     return format!"%s\n\n%s"(firstString, secondString);
173 }