#!/usr/bin/env dub /+ dub.sdl: name "openapigenerator.d" dependency "dyaml" version="~>0.8.0" dependency "handlebars" version="~>0.2.2" stringImportPaths "codegen" +/ // The following copyright string also applies to this file. string COPYRIGHT = q"EOL /* * Sailfin: a Jellyfin client written using Qt * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ EOL"; import std.algorithm; import std.array; import std.conv; import std.exception; import std.file : mkdirRecurse; import std.functional; import std.path : buildPath, dirSeparator; import std.parallelism : parallel; import std.range; import std.regex; import std.string; import std.stdio; import std.uni; import dyaml; import handlebars.tpl; /* * Dear future (potential) employers, hereby I swear I will not brew up such unmaintainable code * if you would hire me and treat me well. */ /* * What should I use? * I cannot use GTAD. * I cannot use GTAD, because it does not support OpenAPI 3. * * What should I use? * I cannot use openapi-generator. * I cannot use openapi-generator, because it generates too much. * * … * * I was in doubt about writing my own script. * Since it sounds easy and fun to do. * I was in doubt about writing my own script. * Since using DLANG fills me with joy. * * |: I was even in two minds * But I took no risk * I've been thinking about writing my own script. :| [2×] * * My own script? * My own script? * My— own— script~? * * |: Is there life on Pluto? * Are we able to dance on the moon? * Is there some space between the stars I where I'm able to go to? :| [2×] * */ static this() { COPYRIGHT ~= q"EOS /* * WARNING: THIS IS AN AUTOMATICALLY GENERATED FILE! PLEASE DO NOT EDIT THIS, AS YOUR EDITS WILL GET * OVERWRITTEN AT SOME POINT! * * If there is a bug in this file, please fix the code generator used to generate this file found in * core/openapigenerator.d. * * This file is generated based on Jellyfin's OpenAPI description, "openapi.json". Please update that * file with a newer file if needed instead of manually updating the files. */ EOS"; } // CODE GENERATION SETTINGS // File name of the CMake file this generated should generate. string CMAKE_INCLUDE_FILE = "GeneratedSources.cmake"; string CMAKE_VAR_PREFIX = "openapi"; string INCLUDE_PREFIX = "JellyfinQt"; string SRC_PREFIX = ""; string MODEL_FOLDER = "dto"; string SUPPORT_FOLDER = "support"; string LOADER_FOLDER = "loader"; string HTTP_LOADER_FOLDER = buildPath("loader", "http"); string[string] compatAliases; string[string] memberAliases; static this() { memberAliases["id"] = "jellyfinId"; memberAliases["static"] = "staticStreaming"; } CasePolicy OPENAPI_CASING = CasePolicy.PASCAL; static immutable string[1] CPP_NAMESPACE = ["Jellyfin"]; static immutable string[2] CPP_NAMESPACE_DTO = ["Jellyfin", "DTO"]; static immutable string[2] CPP_NAMESPACE_SUPPORT = ["Jellyfin", "Support"]; static immutable string[2] CPP_NAMESPACE_LOADER = ["Jellyfin", "Loader"]; static immutable string[3] CPP_NAMESPACE_LOADER_HTTP = ["Jellyfin", "Loader", "HTTP"]; CasePolicy CPP_FILENAME_CASING = CasePolicy.LOWER; CasePolicy CPP_CLASS_CASING = CasePolicy.PASCAL; CasePolicy CPP_CLASS_MEMBER_CASING = CasePolicy.CAMEL; // Prefix for class members. string CPP_CLASS_MEMBER_PREFIX = "m_"; CasePolicy CPP_CLASS_METHOD_CASING = CasePolicy.CAMEL; bool GENERATE_PROPERTIES = true; string outputDirectory = "generated"; // END CODE GENERATION SETTINGS. static immutable MimeType MIME_APPLICATION_JSON = MimeType("application", "json"); // Implementation enum CasePolicy { KEEP, // Do not modify PASCAL, // PascalCase CAMEL, // camelCase SNAKE, // snake_case SCREAMING_SNAKE, // SCREAMING_SNAKE_CASE LOWER, // lowercase UPPER // UPPERCASE } string USAGE = "USAGE: %s "; class CLIArgumentException : Exception { mixin basicExceptionCtors; } int main(string[] args) { try { realMain(args); } catch (CLIArgumentException e) { stderr.writeln(e.message); return -1; } return 0; } void realMain(string[] args) { enforce!CLIArgumentException(args.length >= 2, USAGE.format(args[0])); string schemeFile = args[1]; if (args.length >= 3) outputDirectory = args[2]; mkdirRecurse(buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER)); mkdirRecurse(buildPath(outputDirectory, "include", INCLUDE_PREFIX, HTTP_LOADER_FOLDER)); mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER)); mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX, HTTP_LOADER_FOLDER)); Node root = Loader.fromFile(schemeFile).load(); Appender!(string[]) headerFiles, implementationFiles; foreach(string key, ref const Node scheme; root["components"]["schemas"]) { string fileBase = key.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING); string headerFileName = buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER, fileBase ~ ".h"); string implementationFileName = buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER, fileBase ~ ".cpp"); headerFiles ~= [headerFileName]; implementationFiles ~= [implementationFileName]; File headerFile = File(headerFileName, "w+"); File implementationFile = File(implementationFileName, "w+"); generateFileForSchema(key, scheme, root["components"]["schemas"], headerFile, implementationFile); } Appender!(Endpoint[]) endpoints; Node[][string] tags; foreach(string path, ref Node operations; root["paths"]) { foreach (string operation, ref Node endpoint; operations) { endpoint["path"] = path; endpoint["operation"] = operation; string tag; if ("tags" in endpoint && endpoint["tags"].length > 0) { tag = endpoint["tags"][0].as!string; } else { tag = "untagged"; } if (tag in tags) { tags[tag] ~= endpoint; } else { tags[tag] = [endpoint]; } } } foreach(tag, operations; tags) { string fileBase = tag.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING); string headerFileName = buildPath(outputDirectory, "include", INCLUDE_PREFIX, HTTP_LOADER_FOLDER, fileBase ~ ".h"); string implementationFileName = buildPath(outputDirectory, "src", SRC_PREFIX, HTTP_LOADER_FOLDER, fileBase ~ ".cpp"); headerFiles ~= [headerFileName]; implementationFiles ~= [implementationFileName]; File headerFile = File(headerFileName, "w+"); File implementationFile = File(implementationFileName, "w+"); generateFileForEndpoints(operations, root["components"]["schemas"], headerFile, implementationFile, endpoints, fileBase); } string typesHeaderPath = buildPath(outputDirectory, "include", INCLUDE_PREFIX, LOADER_FOLDER, "requesttypes.h"); string typesImplementationPath = buildPath(outputDirectory, "src", SRC_PREFIX, LOADER_FOLDER, "requesttypes.cpp"); File typesHeader = File(typesHeaderPath, "w+"); File typesImplementation = File(typesImplementationPath, "w+"); headerFiles ~= [typesHeaderPath]; implementationFiles ~= [typesImplementationPath]; writeRequestTypesFile(typesHeader, typesImplementation, endpoints[].sort!((a, b) => a.name < b.name)); writeCMakeFile(headerFiles[], implementationFiles[]); } /** * Writes a CMake file that includes the specified files. */ void writeCMakeFile(string[] headerFiles, string[] implementationFiles) { File output = File(buildPath(outputDirectory, CMAKE_INCLUDE_FILE), "w+"); output.writeln("cmake_minimum_required(VERSION 3.0)"); // Peek laziness: wrapping a C++ comment inside a CMake block comment because I couldn't be // donkey'd to do otherwise. output.writeln("#[["); output.writeln(COPYRIGHT); output.writeln("]]"); output.writef("set(%s_HEADERS", CMAKE_VAR_PREFIX); foreach (headerFile; headerFiles) { output.writeln(); output.writef("\t%s", headerFile); } output.writeln(")"); output.writeln(); output.writef("set(%s_SOURCES", CMAKE_VAR_PREFIX); foreach (implementationFile; implementationFiles) { output.writeln(); output.writef("\t%s", implementationFile); } output.writeln(")"); } /** * Writes the file with all the types that are just used for making requests to the API endpoint * * Params: * headerFile = The file to write the header to * implementationFile = The file to write the implememntation to * endpoints = A list of endpoints to extract request type information from. */ void writeRequestTypesFile(R)(File headerFile, File implementationFile, R endpoints) if(is(ElementType!R : Endpoint)) { string[] collectImports(R range, bool function(MetaTypeInfo) predicate) { return endpoints // Create a list of all parameter types used .map!(e => e.parameters) .joiner .map!(e => e.type) // Weed out container types .map!((MetaTypeInfo e) { if (e.isContainer) { MetaTypeInfo c = e.containerType; while (c.isContainer) { c = c.containerType; } return c; } else { return e; } }) // Filter out the ones we need according to the predicate .filter!predicate // Map MetaTypeInfo -> typename: string .map!(e => e.typeName) // Filter out qint32 etc .filter!(e => !e.startsWith("qint")) // Sort and filter out duplicates .array .sort .uniq .array; } RequestParameter[] getParameters(RequestParameter[] params, bool function(RequestParameter) pred) { return params .filter!pred .array .sort!((a, b) => a.name < b.name) .array .array; } string[] systemImports = collectImports(endpoints, e => e.needsSystemImport) ~ ["QList", "QStringList", "optional"]; string[] userImports = collectImports(endpoints, e => e.needsLocalImport) .map!(e => buildPath(MODEL_FOLDER, e.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h")) .array; struct EndpointController { string name; RequestParameter[] requiredPathParameters = []; RequestParameter[] requiredQueryParameters = []; RequestParameter[] optionalQueryParameters = []; RequestParameter[] requiredParameters = []; RequestParameter[] optionalParameters = []; RequestParameter[] parameters = []; RequestParameter[] bodyParameters = []; } struct Controller { EndpointController[] endpoints; string dtoNamespace = namespaceString!CPP_NAMESPACE_DTO; } Controller controller; foreach(endpoint; endpoints) { EndpointController endpointController; endpointController.name = endpoint.parameterType; endpointController.requiredPathParameters = getParameters(endpoint.parameters, (e => e.required && e.location == ParameterLocation.PATH)); endpointController.requiredQueryParameters = getParameters(endpoint.parameters, (e => e.required && e.location == ParameterLocation.QUERY)); endpointController.optionalQueryParameters = getParameters(endpoint.parameters, (e => !e.required && e.location == ParameterLocation.QUERY)); endpointController.bodyParameters = getParameters(endpoint.parameters, (e => e.location == ParameterLocation.BODY)); with (endpointController) { parameters = requiredPathParameters ~ requiredQueryParameters ~ optionalQueryParameters ~ bodyParameters; requiredParameters = requiredPathParameters ~ requiredQueryParameters; optionalParameters = optionalQueryParameters; } controller.endpoints ~= [endpointController]; } headerFile.writeHeaderPreamble(CPP_NAMESPACE_LOADER, "RequestTypes", systemImports, userImports); headerFile.writeln(render!(import("loader_types_header.hbs"), Controller)(controller)); headerFile.writeHeaderPostamble(CPP_NAMESPACE_LOADER, "RequestTypes"); implementationFile.writeImplementationPreamble(CPP_NAMESPACE_LOADER, LOADER_FOLDER, "RequestTypes"); implementationFile.writeln(render!(import("loader_types_implementation.hbs"), Controller)(controller)); implementationFile.writeImplementationPostamble(CPP_NAMESPACE_LOADER, "RequestTypes"); } /** * Generates files for endpoints in a category * Params: * endpointNode = YAML node representing the endpoint. * allSchemas = YAML node containing the schemas that values in the endpointNOde could reference. * headerFile = File to write the header (.h) file to * implementationFile = File to write the implementation (.cpp) file to. * endpoints = Appender to add any requestParameters encountered to. * categoryName = name of the category */ void generateFileForEndpoints(ref const Node[] endpointNodes, ref const Node allSchemas, ref scope File headerFile, ref scope File implementationFile, ref scope Appender!(Endpoint[]) endpoints, ref const string categoryName) { class EndpointController { string className; //MetaTypeInfo[] properties; string responseType = "void"; string parameterType = "void"; Endpoint endpoint; string operation; string pathStringInterpolation() { string result = "QStringLiteral(\"" ~ endpoint.path ~ "\")"; foreach(p; endpoint.parameters.filter!(p => p.location == ParameterLocation.PATH)) { result = result.replace("{" ~ p.name ~ "}", "\") + Support::toString< " ~ p.type.typeNameWithQualifiers ~">(params." ~ p.type.name ~ "()) + QStringLiteral(\""); } result = result.replace(`+ QStringLiteral("")`, ""); return result; } } Appender!(EndpointController[]) endpointControllers = appender!(EndpointController[]); endpointControllers.reserve(endpointNodes.length); string[] systemImports = ["optional"]; string[] userImports = [ buildPath(SUPPORT_FOLDER, "jsonconv.h"), buildPath(SUPPORT_FOLDER, "loader.h"), buildPath(LOADER_FOLDER, "requesttypes.h") ]; foreach(endpointNode; endpointNodes) { string name = endpointNode["operationId"].as!string; Endpoint endpoint = new Endpoint(); endpoint.name = name; endpoint.parameterType = name ~ "Params"; endpoint.description = endpointNode.getOr!string("summary", ""); endpoint.path = endpointNode["path"].as!string; endpoint.operation = endpointNode["operation"].as!string.toLower(); // Find the most likely result response. foreach(string code, const Node response; endpointNode["responses"]) { int codeNo = to!int(code); if ([200, 201].canFind(codeNo)) { foreach(string contentType, const Node content; response["content"]) { if (contentType == "application/json") { endpoint.hasSuccessResponse = true; if ("$ref" in content["schema"]) { string reference = content["schema"]["$ref"].as!string.chompPrefix("#/components/schemas/"); endpoint.resultIsReference = true; endpoint.resultType = reference; userImports ~= [buildPath(MODEL_FOLDER, reference.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h")]; } else if ("schema" in content){ endpoint.resultIsReference = false; string typeName = endpoint.name ~ "Response"; MetaTypeInfo responseType = getType(typeName, content["schema"], allSchemas); endpoint.resultType = responseType.typeName; if (responseType.needsLocalImport && !responseType.isContainer) { userImports ~= [buildPath(MODEL_FOLDER, endpoint.resultType)]; } MetaTypeInfo t = responseType; while(t.isContainer) { t = t.containerType; if (t.needsLocalImport) { userImports ~= [buildPath(MODEL_FOLDER, t.fileName)]; } else if (t.needsSystemImport && !t.isContainer){ systemImports ~= [t.typeName]; } } } } } } if (codeNo == 204 /* No content */) { endpoint.resultType = "void"; endpoint.hasSuccessResponse = true; } } if ("requestBody" in endpointNode) { string description = ""; if ("description" in endpointNode["requestBody"]) { description = endpointNode["requestBody"]["description"].as!string; } MimeType[] similarMimeTypes = []; foreach(string contentType, const Node content; endpointNode["requestBody"]["content"]) { MimeType mimeType = MimeType.parse(contentType); // Skip if we already had a similar mime type before. if (similarMimeTypes.any!((t) => t.compatible(mimeType))) continue; similarMimeTypes ~= [mimeType]; RequestParameter param = new RequestParameter(); param.location = ParameterLocation.BODY; param.description = description; param.required = true; // Hardcode this because the openapi description of Jellyfin seems to contain both if (mimeType.type == "text" && mimeType.subtype == "json") continue; if (mimeType.compatible(MIME_APPLICATION_JSON)) { string name = "body"; param.type = getType(name, content["schema"], allSchemas); } else { MetaTypeInfo info = new MetaTypeInfo(); info.name = "body"; info.originalName = "body"; info.typeName = "QByteArray"; info.isTypeNullable = true; info.needsSystemImport = true; info.typeNullableCheck = ".isEmpty()"; info.typeNullableSetter = ".clear()"; param.type = info; } endpoint.bodyParameters ~= [param]; endpoint.parameters ~= [param]; } } // Build the parameter structure. if ("parameters" in endpointNode && endpointNode["parameters"].length > 0) { foreach (ref const Node yamlParameter; endpointNode["parameters"]) { RequestParameter param = new RequestParameter(); param.name = yamlParameter["name"].as!string; param.required = yamlParameter.getOr!bool("required", false); param.description = yamlParameter.getOr!string("description", ""); param.type = getType(param.name, yamlParameter["schema"], allSchemas); if (!param.type.isNullable && !param.required && !param.type.hasDefaultValue) { param.type.isNullable = true; } switch(yamlParameter["in"].as!string.toLower) { case "path": param.location = ParameterLocation.PATH; endpoint.requiredPathParameters ~= [param]; break; case "query": param.location = ParameterLocation.QUERY; if (param.required) { endpoint.requiredQueryParameters ~= [param]; } else { endpoint.optionalQueryParameters ~= [param]; } break; default: assert(false); } endpoint.parameters ~= [param]; } } endpoints ~= [endpoint]; EndpointController endpointController = new EndpointController(); endpointController.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING); endpointController.endpoint = endpoint; endpointController.operation = endpoint.operation.applyCasePolicy(CasePolicy.CAMEL, CasePolicy.PASCAL); endpointControllers ~= [endpointController]; } // Render templates struct Controller { EndpointController[] endpoints; string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT; string dtoNamespace = namespaceString!CPP_NAMESPACE_DTO; } Controller controller = Controller(); controller.endpoints = endpointControllers[]; writeHeaderPreamble(headerFile, CPP_NAMESPACE_LOADER_HTTP, categoryName, systemImports, userImports); headerFile.writeln(render!(import("loader_header.hbs"), Controller)(controller)); writeHeaderPostamble(headerFile, CPP_NAMESPACE_LOADER_HTTP, categoryName); writeImplementationPreamble(implementationFile, CPP_NAMESPACE_LOADER_HTTP, HTTP_LOADER_FOLDER, categoryName); implementationFile.writeln(render!(import("loader_implementation.hbs"), Controller)(controller)); writeImplementationPostamble(implementationFile, CPP_NAMESPACE_LOADER_HTTP, categoryName); } /** * Generates a file containing a class generated based on the given JSON Schema. */ void generateFileForSchema(ref const string name, ref const Node scheme, Node allSchemas, ref scope File headerFile, ref scope File implementationFile) { // Check if this JSON "thing" is an enum if ("enum" in scheme) { string[3] imports = ["QJsonValue", "QObject", "QString"]; string[1] userImports = [buildPath(SUPPORT_FOLDER, "jsonconv.h")]; Appender!(string[]) values; foreach (string value; scheme["enum"]) { values ~= value; } writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, imports, userImports); writeEnumHeader(headerFile, name, values[]); writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name); writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name); writeEnumImplementation(implementationFile, name, values[]); writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name); } // Check if this is an object if (scheme["type"].as!string == "object" && "properties" in scheme) { // Determine all imports Appender!(string[]) systemImports, userImports; Appender!(string[]) forwardDeclarations; systemImports ~= ["optional", "QJsonObject", "QJsonValue"]; userImports ~= [buildPath(SUPPORT_FOLDER, "jsonconv.h")]; MetaTypeInfo[] usedTypes = collectTypeInfo(scheme["properties"], allSchemas); usedTypes[$-1].isLast = true; bool importedContainers = false; void collectImports(MetaTypeInfo type) { if (type.needsPointer && !systemImports[].canFind("QSharedPointer")) { systemImports ~= ["QSharedPointer"]; } if (type.needsSystemImport && !systemImports[].canFind(type.typeName)) { if (type.isContainer) { if (!importedContainers) { systemImports ~= ["QList", "QStringList"]; importedContainers = true; } if (type.containerType !is null) { collectImports(type.containerType); } } else { systemImports ~= [type.typeName]; } } else { string userImport = buildPath(MODEL_FOLDER, type.typeName.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h"); if (type.needsLocalImport && !userImports[].canFind(userImport) && type.typeName != name) { userImports ~= userImport; } } } foreach (type; usedTypes) { collectImports(type); } foreach (ref type; usedTypes.retro) { if (type.isNotNullable) { type.isLastNonNullable = true; break; } } // Sort them for nicer reading string[] sortedSystemImports = sort(systemImports[]).array; string[] sortedUserImports = sort(userImports[]).array; string[] sortedForwardDeclarations = sort(forwardDeclarations[]).array; // Write implementation files writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, sortedSystemImports, sortedUserImports); writeObjectHeader(headerFile, name, usedTypes, sortedForwardDeclarations); writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name); writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name); writeObjectImplementation(implementationFile, name, usedTypes); writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name); } else if (scheme["type"] == "object") { // Write implementation files writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, ["QJsonObject"]); headerFile.writefln("using %s = QJsonObject;", name); writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name); writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name); headerFile.writeln("// No implementation needed"); writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name); } } // Object // We need to recurse (sometimes) /** * Create a MetaTypeInfo object based on a JSON schema * * In the future, this implementation should use some form of configuration file * which contains data about the built-in types, since hard-coding doesn't seem like a * good idea. * * Params: * name = The name of this object * node = The node containing the JSON Schema of this object * allSchemas = The node containing the a map of names to JSON Schemas, which the node * parameter could refrence. */ MetaTypeInfo getType(ref const string name, const ref Node node, const ref Node allSchemas) { MetaTypeInfo info = new MetaTypeInfo(); info.originalName = name; info.name = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_MEMBER_CASING); info.defaultValue = node.getOr!string("default", ""); if ("description" in node) { info.description = node["description"].as!string; } // Special case for QML info.name = memberAliases.get(info.name.toLower(), info.name); // Check if this schema is a reference to another schema if ("$ref" in node) { string type = node["$ref"].as!string()["#/components/schemas/".length..$]; info.needsLocalImport = true; info.typeName = type; if (type in allSchemas) { if ("enum" in allSchemas[type]) { info.isTypeNullable = true; info.typeNullableCheck = "== " ~ info.typeName ~ "::EnumNotSet"; info.typeNullableSetter = "= " ~ info.typeName ~ "::EnumNotSet"; } else if ("type" in allSchemas[type] && allSchemas[type]["type"].as!string == "object") { writefln("Type %s is an object", type); info.needsPointer = true; info.isTypeNullable = true; info.typeNullableCheck = ".isNull()"; info.typeNullableSetter = ".clear()"; } } return info; } // Type is an enumeration if ("enum" in node) { info.isTypeNullable = true; info.typeNullableCheck = "== " ~ info.typeName ~ "::EnumNotSet"; info.typeNullableSetter = "= " ~ info.typeName ~ "::EnumNotSet"; return info; } // No type information specified. As a fallback, use a QVariant. if (!("type" in node)) { info.typeName = "QVariant"; info.isTypeNullable = true; info.needsSystemImport = true; info.typeNullableCheck = ".isNull()"; info.typeNullableSetter = ".clear()"; return info; } info.isNullable = node.getOr!bool("nullable", false); switch(node["type"].as!string) { case "boolean": info.typeName = "bool"; return info; case "string": if ("format" in node) { switch(node["format"].as!string) { case "date-time": info.typeName= "QDateTime"; info.needsSystemImport = true; info.isTypeNullable = true; info.typeNullableCheck = ".isNull()"; info.typeNullableSetter = "= QDateTime()"; return info; /+case "uuid": info.typeName = "QUuid"; info.needsSystemImport = true; info.isTypeNullable = true; info.typeNullableCheck = ".isNull()"; info.typeNullableSetter = "= QGuid()"; return info;+/ default: break; } } info.isTypeNullable = true; info.typeName = "QString"; info.needsSystemImport = true; info.typeNullableCheck = ".isNull()"; info.typeNullableSetter = ".clear()"; return info; case "integer": if ("format" in node) { info.typeName= "q" ~ node["format"].as!string; return info; } goto default; case "number": if ("format" in node) { switch(node["format"].as!string) { case "float": case "double": info.typeName = node["format"].as!string; return info; default: break; } } goto default; case "object": info.typeName = "QJsonObject"; // This'll do for now info.isTypeNullable = true; info.typeNullableCheck = ".isEmpty()"; info.typeNullableSetter = "= QJsonObject()"; return info; case "array": string containedTypeName = "arrayItem"; MetaTypeInfo containedType = getType(containedTypeName, node["items"], allSchemas); containedType.needsPointer = false; info.needsLocalImport = containedType.needsLocalImport; info.needsSystemImport = true; info.isContainer = true; info.containerType = containedType; info.isTypeNullable = true; info.typeNullableCheck = ".size() == 0"; info.typeNullableSetter = ".clear()"; if (containedType.typeName == "QString") { info.typeName = "QStringList"; } else { info.typeName = "QList<" ~ containedType.typeNameWithQualifiers ~ ">"; } return info; default: info.typeName = "UNIMPLEMENTED"; return info; } } /** * Given a list of JSON schemes, this will generate a list of MetaTypeInfo[] */ MetaTypeInfo[] collectTypeInfo(const ref Node properties, const ref Node allSchemas) { Appender!(MetaTypeInfo[]) result; foreach(ref string name, const ref Node node; properties) { result ~= getType(name, node, allSchemas); } return result[]; } void writeObjectHeader(File output, string name, MetaTypeInfo[] properties, string[] userImports) { class Controller { string className; MetaTypeInfo[] properties; string[] userImports; string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT; bool hasRequiredProperties; } Controller controller = new Controller(); controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING); controller.properties = properties; controller.userImports = userImports; controller.hasRequiredProperties = properties.canFind!((x) => !x.isNullable); output.writeln(render!(import("object_header.hbs"), Controller)(controller)); } void writeObjectImplementation(File output, string name, MetaTypeInfo[] properties) { class Controller { string className; MetaTypeInfo[] properties; string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT; bool hasRequiredProperties; } Controller controller = new Controller(); controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING); controller.properties = properties; controller.hasRequiredProperties = properties.canFind!((x) => !x.isNullable); output.writeln(render!(import("object_implementation.hbs"), Controller)(controller)); } // Enum void writeEnumHeader(File output, string name, string[] values, string doc = "") { class Controller { string className; string[] values; string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT; } Controller controller = new Controller(); controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING); controller.values = values; output.writeln(render!(import("enum_header.hbs"), Controller)(controller)); } void writeEnumImplementation(File output, string name, string[] values) { class Controller { string className; string[] values; string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT; } Controller controller = new Controller(); controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING); controller.values = values; output.writeln(render!(import("enum_implementation.hbs"), Controller)(controller)); } // Common void writeHeaderPreamble(File output, immutable string[] fileNamespace, string className, string[] imports = [], string[] userImports = []) { output.writeln(COPYRIGHT); string guard = guardName(fileNamespace, className); output.writefln("#ifndef %s", guard); output.writefln("#define %s", guard); output.writeln(); foreach(file; imports) { output.writefln("#include <%s>", file); } if (imports.length > 0) output.writeln(); foreach(file; userImports) { output.writefln("#include \"%s\"", buildPath(INCLUDE_PREFIX, file)); } if (userImports.length > 0) output.writeln(); // FIXME: Should be configurable output.writefln("namespace Jellyfin {"); output.writefln("// Forward declaration"); output.writefln("class ApiClient;"); output.writefln("}"); foreach (namespace; fileNamespace) { output.writefln("namespace %s {", namespace); } output.writeln(); } void writeHeaderPostamble(File output, immutable string[] fileNamespace, string className) { output.writeln(); foreach_reverse(namespace; fileNamespace) { output.writefln("} // NS %s", namespace); } output.writeln(); output.writefln("#endif // %s", guardName(fileNamespace, className)); } void writeImplementationPreamble(File output, immutable string[] fileNamespace, string folder, string className, string[] imports = []) { output.writeln(COPYRIGHT); output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, folder, className.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h")); output.writeln(); foreach(file; imports) { output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, file)); } if (imports.length > 0) output.writeln(); foreach (namespace; fileNamespace) { output.writefln("namespace %s {", namespace); } output.writeln(); } void writeImplementationPostamble(File output, immutable string[] fileNamespace, string className) { output.writeln(); foreach_reverse(namespace; fileNamespace) { output.writefln("} // NS %s", namespace); } } // Helper functions /** * Transforsm the given string from the input casing system to the ouptut casing system. * * Params: * source = The string to transform * */ string applyCasePolicy(string source, CasePolicy input, CasePolicy output) { if (input == output) return source; switch(output) { case CasePolicy.KEEP: return source; case CasePolicy.PASCAL: if (input == CasePolicy.CAMEL) { char[] result = source.dup; result[0] = cast(char) toUpper(result[0]); return cast(string) result; } else { throw new Exception("Not implemented"); } case CasePolicy.CAMEL: if (input == CasePolicy.PASCAL) { char[] result = source.dup; result[0] = cast(char) toLower(result[0]); return cast(string) result; } else { throw new Exception("Not implemented"); } case CasePolicy.LOWER: if (input == CasePolicy.CAMEL || input == CasePolicy.PASCAL) { return source.toLower(); } throw new Exception("Not implemented"); case CasePolicy.UPPER: if (input == CasePolicy.CAMEL || input == CasePolicy.PASCAL) { return source.toUpper(); } throw new Exception("Not implemented"); case CasePolicy.SCREAMING_SNAKE: if (input == CasePolicy.CAMEL || input == CasePolicy.PASCAL) { Appender!(char[]) result; foreach(window; source.slide!(Yes.withPartial)(2)) { dchar c = window.front; window.popFront(); dchar n = window.front; if (isLower(c) && !isLower(n)) { result ~= toUpper(c); result ~= '_'; } else { result ~= toUpper(c); } } result ~= toUpper(source[$ - 1]); return cast(string) result[]; } throw new Exception("Not implemented"); default: throw new Exception("Not implemented"); } } unittest { assert("fooBar".applyCasePolicy(CasePolicy.CAMEL, CasePolicy.SNAKE) == "foo_bar"); } class MetaTypeInfo { public: string originalName = ""; string name = ""; string typeName = ""; /// Description of this property. string description = ""; /// If this property is nullable according to the OpenAPI spec. bool isNullable = false; bool needsPointer = false; /// If the type needs a system import (Such as Qt types) bool needsSystemImport = false; /// If the type needs a local import (such as types elsewhere in this project). bool needsLocalImport = false; /// If the type is a container type bool isContainer = false; /// If this type has a non-ambigious null state. bool isTypeNullable = false; /// If `isContainer` is true, the type of the container. MetaTypeInfo containerType = null; /// For use in templating bool isLast = false; bool isLastNonNullable = false; string defaultValue = ""; bool hasDefaultValue() const { return defaultValue.length > 0; } string writeName() const { return name.applyCasePolicy(CPP_CLASS_MEMBER_CASING, CasePolicy.PASCAL); } string memberName() const { return CPP_CLASS_MEMBER_PREFIX ~ name; } string typeNameWithQualifiers() const { if (needsPointer) { return "QSharedPointer<" ~ typeName ~ ">"; } if (needsOptional) { return "std::optional<" ~ typeName ~ ">"; } else { return typeName; } } bool needsOptional() const { return (isNullable || hasDefaultValue) && !isTypeNullable; } string typeNullableCheck; string nullableCheck() const { if (needsOptional) { return "!" ~ memberName ~ ".has_value()"; } else if (typeNullableCheck.length > 0) { return memberName ~ typeNullableCheck; } return "Q_ASSERT(false)"; } string typeNullableSetter = ""; string nullableSetter() const { if (needsOptional) { return " = std::nullopt"; } return typeNullableSetter; } string defaultInitializer() const { if (needsPointer) return "QSharedPointer<" ~ typeName ~ ">()"; if (needsOptional) return "std::nullopt"; return ""; } bool isNotNullable() const { return !isNullable; } string fileName (){ return typeName.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h"; } } /** * Represents an API endpoint. */ class Endpoint { bool resultIsReference = false; bool hasSuccessResponse = false; string name; /// The type of the string resultType; /// The name of the structure containing the parameters for this endpoint. string parameterType = "void"; /// HTTP path for this endpoint. string path; /// Description/documentation for this endpoint string description; /// HTTP method for this endpoint string operation; /// List of all parameters for this request RequestParameter[] parameters = []; RequestParameter[] requiredPathParameters; RequestParameter[] requiredQueryParameters; RequestParameter[] optionalQueryParameters; RequestParameter[] bodyParameters; } enum ParameterLocation { PATH, QUERY, COOKIE, HEADER, BODY } class RequestParameter { string name; ParameterLocation location; bool required; string description; MetaTypeInfo type; // Only for body parameters. string mimeType; } /** * Generates a guard name based on a namespace and class name. * * Params: * namespace = Array of namespaces this class is in. * className = The name of this class (or enum or whatever). */ string guardName(immutable string[] namespace, string className) { return namespace.map!toUpper().join("_") ~ "_" ~ className.applyCasePolicy(OPENAPI_CASING, CasePolicy.UPPER) ~ "_H"; } unittest { assert(guardName(["foo", "bar", "Baz"] == "FOO_BAR_BAZ_H") } /** * Converts a string array of namespaces into a C++ namespace string. */ string namespaceString(string[] name)() { string result; static foreach(idx, part; name) { static if (idx != 0) { result ~= "::"; } result ~= part; } return result; } /// Ditto string namespaceString(immutable string[] name) { string result; foreach(idx, part; name) { if (idx != 0) { result ~= "::"; } result ~= part; } return result; } unittest { assert(namespaceString!["foo", "bar"] == "foo::bar"); } bool areMimesCompatible(string expectedMime, string mime) @safe nothrow { try { MimeType type1 = MimeType.parse(expectedMime); MimeType type2 = MimeType.parse(mime); return type1.compatible(type2); } catch(Exception e) { return false; } } class MimeParseException : Exception { mixin basicExceptionCtors; } /** * Data structure representing a mime type. */ struct MimeType { public: this(string type, string subtype, string suffix = null, string parameter = null) @safe { this.type = type; this.subtype = subtype; this.suffix = suffix; this.parameter = parameter; } string toString() const { Appender!string result = appender!string; result.reserve(type.length + subtype.length + (suffix ? suffix.length + 1 : 0) + (parameter ? parameter.length + 2 : 0)); result ~= type; result ~= "/"; result ~= subtype; if (suffix) { result ~= "+"; result ~= suffix; } if (parameter) { result ~= "; "; result ~= parameter; } return result[]; } static MimeType parse(string mime) @safe { auto parts = mime.matchFirst(mimeRegex); enforce(parts, mime ~ " is not a valid mimetype"); return MimeType(parts["type"], parts["subtype"], parts["suffix"], parts["parameter"]); } static auto mimeRegex = regex(`(?P\w*)\/(?P(([\w\-]+\.)+)?[\w\-\*]+)(\+(?P[\w\-\.]+))?(; (?P[\w+-\.=]+))?`); string type = null; string subtype = null;; string suffix = null; string parameter = null; /** * Checks if two mime types are compatible. * * Two mime types are considered compatible if either one of the following * conditions is true: * 1. The mime types are exactly the same * 2. The expected mime type is in the form "x/*" and the mime type is in the form "x/y" * 3. The expected mime type is in the form "x/z+y" and the mime type is in the form "x/y" * 4. The expected mime type is in the form "x/y" and the mime type is in the form "x/z+y" */ bool compatible(const ref MimeType other) const @safe { if (type != other.type) return false; if (subtype == other.subtype || subtype == "*" || other.subtype == "*") return true; if (subtype == other.suffix || suffix == other.subtype) return true; return false; } } /** * Retrieves the given key from the node, if it does not exists, returns the or parameter. */ T getOr(T)(const ref Node node, string key, T or) { if (key in node) { try { return node[key].get!T; } catch (Exception e) { return or; } } else { //stdout.writefln("Could not find %s", key); return or; } }