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;