1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-11-24 18:15:16 +00:00
harbour-sailfin/core/openapigenerator.d

1284 lines
40 KiB
D
Raw Permalink Normal View History

2021-02-20 22:33:07 +00:00
#!/usr/bin/env dub
/+ dub.sdl:
name "openapigenerator.d"
dependency "dyaml" version="~>0.8.0"
dependency "handlebars" version="~>0.2.2"
stringImportPaths "codegen"
2021-02-20 22:33:07 +00:00
+/
// 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";
2021-02-20 22:33:07 +00:00
import std.algorithm;
import std.array;
2021-03-24 19:04:03 +00:00
import std.conv;
2021-02-20 22:33:07 +00:00
import std.exception;
import std.file : mkdirRecurse;
import std.functional;
2021-02-20 22:33:07 +00:00
import std.path : buildPath, dirSeparator;
2021-03-24 19:04:03 +00:00
import std.parallelism : parallel;
2021-02-20 22:33:07 +00:00
import std.range;
import std.regex;
2021-02-20 22:33:07 +00:00
import std.string;
import std.stdio;
import std.uni;
import dyaml;
import handlebars.tpl;
2021-02-20 22:33:07 +00:00
/*
* 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";
}
2021-02-20 22:33:07 +00:00
// 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 = "";
2021-03-24 19:04:03 +00:00
string MODEL_FOLDER = "dto";
string SUPPORT_FOLDER = "support";
2021-03-24 19:04:03 +00:00
string LOADER_FOLDER = "loader";
string HTTP_LOADER_FOLDER = buildPath("loader", "http");
string[string] compatAliases;
string[string] memberAliases;
static this() {
memberAliases["id"] = "jellyfinId";
2021-03-24 19:04:03 +00:00
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"];
2021-03-24 19:04:03 +00:00
static immutable string[2] CPP_NAMESPACE_LOADER = ["Jellyfin", "Loader"];
static immutable string[3] CPP_NAMESPACE_LOADER_HTTP = ["Jellyfin", "Loader", "HTTP"];
2021-02-20 22:33:07 +00:00
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
2021-02-20 22:33:07 +00:00
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 <openapi scheme>";
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));
2021-03-24 19:04:03 +00:00
mkdirRecurse(buildPath(outputDirectory, "include", INCLUDE_PREFIX, HTTP_LOADER_FOLDER));
mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER));
2021-03-24 19:04:03 +00:00
mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX, HTTP_LOADER_FOLDER));
2021-02-20 22:33:07 +00:00
Node root = Loader.fromFile(schemeFile).load();
Appender!(string[]) headerFiles, implementationFiles;
2021-03-24 19:04:03 +00:00
foreach(string key, ref const Node scheme; root["components"]["schemas"]) {
string fileBase = key.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING);
2021-03-24 19:04:03 +00:00
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);
}
2021-03-24 19:04:03 +00:00
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";
}
2021-03-24 19:04:03 +00:00
if (tag in tags) {
tags[tag] ~= endpoint;
} else {
tags[tag] = [endpoint];
}
2021-03-24 19:04:03 +00:00
}
}
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);
}
2021-03-24 19:04:03 +00:00
string typesHeaderPath = buildPath(outputDirectory, "include", INCLUDE_PREFIX, LOADER_FOLDER, "requesttypes.h");
string typesImplementationPath = buildPath(outputDirectory, "src", SRC_PREFIX, LOADER_FOLDER, "requesttypes.cpp");
2021-03-24 19:04:03 +00:00
File typesHeader = File(typesHeaderPath, "w+");
File typesImplementation = File(typesImplementationPath, "w+");
headerFiles ~= [typesHeaderPath];
implementationFiles ~= [typesImplementationPath];
2021-03-24 19:04:03 +00:00
writeRequestTypesFile(typesHeader, typesImplementation, endpoints[].sort!((a, b) => a.name < b.name));
2021-03-24 19:04:03 +00:00
writeCMakeFile(headerFiles[], implementationFiles[]);
}
2021-03-24 19:04:03 +00:00
/**
* 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);
2021-02-20 22:33:07 +00:00
}
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)) {
2021-03-24 19:04:03 +00:00
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)
2021-03-24 19:04:03 +00:00
.array
.array;
}
string[] systemImports = collectImports(endpoints, e => e.needsSystemImport)
2021-03-26 20:27:35 +00:00
~ ["QList", "QStringList", "optional"];
2021-03-24 19:04:03 +00:00
string[] userImports = collectImports(endpoints, e => e.needsLocalImport)
.map!(e => buildPath(MODEL_FOLDER, e.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h"))
.array;
2021-03-24 19:04:03 +00:00
struct EndpointController {
string name;
RequestParameter[] requiredPathParameters = [];
RequestParameter[] requiredQueryParameters = [];
RequestParameter[] optionalQueryParameters = [];
RequestParameter[] requiredParameters = [];
RequestParameter[] optionalParameters = [];
RequestParameter[] parameters = [];
RequestParameter[] bodyParameters = [];
2021-03-24 19:04:03 +00:00
}
2021-03-24 19:04:03 +00:00
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));
2021-03-24 19:04:03 +00:00
with (endpointController) {
parameters = requiredPathParameters ~ requiredQueryParameters ~ optionalQueryParameters ~ bodyParameters;
2021-03-24 19:04:03 +00:00
requiredParameters = requiredPathParameters ~ requiredQueryParameters;
optionalParameters = optionalQueryParameters;
2021-03-24 19:04:03 +00:00
}
controller.endpoints ~= [endpointController];
}
headerFile.writeHeaderPreamble(CPP_NAMESPACE_LOADER, "RequestTypes", systemImports, userImports);
2021-03-24 19:04:03 +00:00
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");
2021-02-20 22:33:07 +00:00
}
2021-03-24 19:04:03 +00:00
/**
* Generates files for endpoints in a category
2021-03-24 19:04:03 +00:00
* 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
2021-03-24 19:04:03 +00:00
*/
void generateFileForEndpoints(ref const Node[] endpointNodes,
2021-03-24 19:04:03 +00:00
ref const Node allSchemas, ref scope File headerFile, ref scope File implementationFile,
ref scope Appender!(Endpoint[]) endpoints, ref const string categoryName) {
2021-03-24 19:04:03 +00:00
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);
2021-03-24 19:04:03 +00:00
string[] systemImports = ["optional"];
string[] userImports = [
buildPath(SUPPORT_FOLDER, "jsonconv.h"),
buildPath(SUPPORT_FOLDER, "loader.h"),
buildPath(LOADER_FOLDER, "requesttypes.h")
];
2021-03-24 19:04:03 +00:00
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];
}
}
}
2021-03-24 19:04:03 +00:00
}
}
}
}
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];
2021-03-24 19:04:03 +00:00
}
}
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];
2021-03-24 19:04:03 +00:00
}
// Render templates
struct Controller {
EndpointController[] endpoints;
2021-03-24 19:04:03 +00:00
string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
string dtoNamespace = namespaceString!CPP_NAMESPACE_DTO;
}
Controller controller = Controller();
controller.endpoints = endpointControllers[];
2021-03-24 19:04:03 +00:00
writeHeaderPreamble(headerFile, CPP_NAMESPACE_LOADER_HTTP, categoryName, systemImports, userImports);
2021-03-24 19:04:03 +00:00
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);
2021-03-24 19:04:03 +00:00
}
/**
* Generates a file containing a class generated based on the given JSON Schema.
*/
2021-03-24 19:04:03 +00:00
void generateFileForSchema(ref const string name, ref const Node scheme, Node allSchemas,
ref scope File headerFile, ref scope File implementationFile) {
2021-02-20 22:33:07 +00:00
// Check if this JSON "thing" is an enum
2021-02-20 22:33:07 +00:00
if ("enum" in scheme) {
string[3] imports = ["QJsonValue", "QObject", "QString"];
string[1] userImports = [buildPath(SUPPORT_FOLDER, "jsonconv.h")];
2021-02-20 22:33:07 +00:00
Appender!(string[]) values;
foreach (string value; scheme["enum"]) {
values ~= value;
}
writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, imports, userImports);
2021-02-20 22:33:07 +00:00
writeEnumHeader(headerFile, name, values[]);
writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name);
2021-02-20 22:33:07 +00:00
writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name);
writeEnumImplementation(implementationFile, name, values[]);
writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name);
2021-02-20 22:33:07 +00:00
}
// Check if this is an object
2021-02-20 22:33:07 +00:00
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")];
2021-02-20 22:33:07 +00:00
MetaTypeInfo[] usedTypes = collectTypeInfo(scheme["properties"], allSchemas);
2021-03-24 19:04:03 +00:00
usedTypes[$-1].isLast = true;
2021-02-20 22:33:07 +00:00
bool importedContainers = false;
void collectImports(MetaTypeInfo type) {
if (type.needsPointer && !systemImports[].canFind("QSharedPointer")) {
systemImports ~= ["QSharedPointer"];
}
2021-02-20 22:33:07 +00:00
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;
}
2021-02-20 22:33:07 +00:00
}
}
foreach (type; usedTypes) {
collectImports(type);
}
foreach (ref type; usedTypes.retro) {
if (type.isNotNullable) {
type.isLastNonNullable = true;
break;
}
}
2021-02-20 22:33:07 +00:00
// Sort them for nicer reading
string[] sortedSystemImports = sort(systemImports[]).array;
string[] sortedUserImports = sort(userImports[]).array;
string[] sortedForwardDeclarations = sort(forwardDeclarations[]).array;
2021-02-20 22:33:07 +00:00
// Write implementation files
writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, sortedSystemImports, sortedUserImports);
writeObjectHeader(headerFile, name, usedTypes, sortedForwardDeclarations);
writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name);
2021-02-20 22:33:07 +00:00
writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name);
2021-02-20 22:33:07 +00:00
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);
2021-02-20 22:33:07 +00:00
}
}
// Object
2021-03-24 19:04:03 +00:00
// 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) {
2021-03-24 19:04:03 +00:00
MetaTypeInfo info = new MetaTypeInfo();
info.originalName = name;
info.name = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_MEMBER_CASING);
info.defaultValue = node.getOr!string("default", "");
2021-03-24 19:04:03 +00:00
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
2021-03-24 19:04:03 +00:00
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()";
}
2021-02-20 22:33:07 +00:00
}
2021-03-24 19:04:03 +00:00
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.
2021-03-24 19:04:03 +00:00
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);
2021-03-24 19:04:03 +00:00
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()";
2021-02-20 22:33:07 +00:00
return info;
2021-03-24 19:04:03 +00:00
/+case "uuid":
info.typeName = "QUuid";
info.needsSystemImport = true;
info.isTypeNullable = true;
info.typeNullableCheck = ".isNull()";
info.typeNullableSetter = "= QGuid()";
return info;+/
default:
break;
2021-02-20 22:33:07 +00:00
}
2021-03-24 19:04:03 +00:00
}
info.isTypeNullable = true;
info.typeName = "QString";
2021-03-24 19:04:03 +00:00
info.needsSystemImport = true;
info.typeNullableCheck = ".isNull()";
info.typeNullableSetter = ".clear()";
return info;
case "integer":
if ("format" in node) {
info.typeName= "q" ~ node["format"].as!string;
2021-02-20 22:33:07 +00:00
return info;
2021-03-24 19:04:03 +00:00
}
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;
2021-02-20 22:33:07 +00:00
}
}
2021-03-24 19:04:03 +00:00
goto default;
case "object":
info.typeName = "QJsonObject"; // This'll do for now
info.isTypeNullable = true;
info.typeNullableCheck = ".isEmpty()";
info.typeNullableSetter = "= QJsonObject()";
2021-03-24 19:04:03 +00:00
return info;
case "array":
string containedTypeName = "arrayItem";
MetaTypeInfo containedType = getType(containedTypeName, node["items"], allSchemas);
2021-03-26 20:27:35 +00:00
containedType.needsPointer = false;
2021-03-24 19:04:03 +00:00
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;
2021-02-20 22:33:07 +00:00
}
2021-03-24 19:04:03 +00:00
}
/**
* Given a list of JSON schemes, this will generate a list of MetaTypeInfo[]
*/
MetaTypeInfo[] collectTypeInfo(const ref Node properties, const ref Node allSchemas) {
2021-02-20 22:33:07 +00:00
Appender!(MetaTypeInfo[]) result;
2021-03-24 19:04:03 +00:00
foreach(ref string name, const ref Node node; properties) {
result ~= getType(name, node, allSchemas);
2021-02-20 22:33:07 +00:00
}
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;
2021-02-20 22:33:07 +00:00
}
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);
2021-02-20 22:33:07 +00:00
output.writeln(render!(import("object_header.hbs"), Controller)(controller));
2021-02-20 22:33:07 +00:00
}
void writeObjectImplementation(File output, string name, MetaTypeInfo[] properties) {
class Controller {
string className;
MetaTypeInfo[] properties;
string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
bool hasRequiredProperties;
2021-02-20 22:33:07 +00:00
}
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));
2021-02-20 22:33:07 +00:00
}
// Enum
void writeEnumHeader(File output, string name, string[] values, string doc = "") {
class Controller {
string className;
string[] values;
string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
2021-02-20 22:33:07 +00:00
}
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));
2021-02-20 22:33:07 +00:00
}
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));
2021-02-20 22:33:07 +00:00
}
// Common
void writeHeaderPreamble(File output, immutable string[] fileNamespace, string className, string[] imports = [], string[] userImports = []) {
output.writeln(COPYRIGHT);
string guard = guardName(fileNamespace, className);
2021-02-20 22:33:07 +00:00
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) {
2021-02-20 22:33:07 +00:00
output.writefln("namespace %s {", namespace);
}
output.writeln();
}
void writeHeaderPostamble(File output, immutable string[] fileNamespace, string className) {
2021-02-20 22:33:07 +00:00
output.writeln();
2021-03-24 19:04:03 +00:00
foreach_reverse(namespace; fileNamespace) {
2021-02-20 22:33:07 +00:00
output.writefln("} // NS %s", namespace);
}
output.writeln();
output.writefln("#endif // %s", guardName(fileNamespace, className));
2021-02-20 22:33:07 +00:00
}
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"));
2021-02-20 22:33:07 +00:00
output.writeln();
foreach(file; imports) {
output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, file));
2021-02-20 22:33:07 +00:00
}
if (imports.length > 0) output.writeln();
foreach (namespace; fileNamespace) {
2021-02-20 22:33:07 +00:00
output.writefln("namespace %s {", namespace);
}
output.writeln();
}
void writeImplementationPostamble(File output, immutable string[] fileNamespace, string className) {
2021-02-20 22:33:07 +00:00
output.writeln();
2021-03-24 19:04:03 +00:00
foreach_reverse(namespace; fileNamespace) {
2021-02-20 22:33:07 +00:00
output.writefln("} // NS %s", namespace);
}
}
// Helper functions
2021-03-24 19:04:03 +00:00
/**
* Transforsm the given string from the input casing system to the ouptut casing system.
*
* Params:
* source = The string to transform
*
*/
2021-02-20 22:33:07 +00:00
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");
}
}
2021-03-24 19:04:03 +00:00
unittest {
assert("fooBar".applyCasePolicy(CasePolicy.CAMEL, CasePolicy.SNAKE) == "foo_bar");
}
2021-02-20 22:33:07 +00:00
class MetaTypeInfo {
public:
string originalName = "";
2021-02-20 22:33:07 +00:00
string name = "";
string typeName = "";
/// Description of this property.
2021-02-20 22:33:07 +00:00
string description = "";
/// If this property is nullable according to the OpenAPI spec.
bool isNullable = false;
2021-02-20 22:33:07 +00:00
bool needsPointer = false;
/// If the type needs a system import (Such as Qt types)
2021-02-20 22:33:07 +00:00
bool needsSystemImport = false;
/// If the type needs a local import (such as types elsewhere in this project).
2021-02-20 22:33:07 +00:00
bool needsLocalImport = false;
/// If the type is a container type
2021-02-20 22:33:07 +00:00
bool isContainer = false;
/// If this type has a non-ambigious null state.
bool isTypeNullable = false;
/// If `isContainer` is true, the type of the container.
2021-02-20 22:33:07 +00:00
MetaTypeInfo containerType = null;
2021-03-24 19:04:03 +00:00
/// For use in templating
bool isLast = false;
bool isLastNonNullable = false;
string defaultValue = "";
bool hasDefaultValue() const {
return defaultValue.length > 0;
}
2021-03-24 19:04:03 +00:00
string writeName() const {
2021-02-20 22:33:07 +00:00
return name.applyCasePolicy(CPP_CLASS_MEMBER_CASING, CasePolicy.PASCAL);
}
string memberName() const {
2021-02-20 22:33:07 +00:00
return CPP_CLASS_MEMBER_PREFIX ~ name;
}
string typeNameWithQualifiers() const {
2021-02-20 22:33:07 +00:00
if (needsPointer) {
return "QSharedPointer<" ~ typeName ~ ">";
2021-03-24 19:04:03 +00:00
}
if (needsOptional) {
return "std::optional<" ~ typeName ~ ">";
2021-02-20 22:33:07 +00:00
} 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) {
2021-03-24 19:04:03 +00:00
return " = std::nullopt";
}
return typeNullableSetter;
}
string defaultInitializer() const {
if (needsPointer) return "QSharedPointer<" ~ typeName ~ ">()";
if (needsOptional) return "std::nullopt";
2021-02-20 22:33:07 +00:00
return "";
}
bool isNotNullable() const { return !isNullable; }
string fileName (){
return typeName.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h";
}
2021-02-20 22:33:07 +00:00
}
/**
* Represents an API endpoint.
*/
2021-03-24 19:04:03 +00:00
class Endpoint {
bool resultIsReference = false;
bool hasSuccessResponse = false;
string name;
/// The type of the
2021-03-24 19:04:03 +00:00
string resultType;
/// The name of the structure containing the parameters for this endpoint.
2021-03-24 19:04:03 +00:00
string parameterType = "void";
/// HTTP path for this endpoint.
string path;
/// Description/documentation for this endpoint
2021-03-24 19:04:03 +00:00
string description;
/// HTTP method for this endpoint
string operation;
2021-03-24 19:04:03 +00:00
/// List of all parameters for this request
2021-03-24 19:04:03 +00:00
RequestParameter[] parameters = [];
RequestParameter[] requiredPathParameters;
RequestParameter[] requiredQueryParameters;
RequestParameter[] optionalQueryParameters;
RequestParameter[] bodyParameters;
2021-03-24 19:04:03 +00:00
}
enum ParameterLocation {
PATH,
QUERY,
COOKIE,
HEADER,
BODY
2021-03-24 19:04:03 +00:00
}
class RequestParameter {
string name;
ParameterLocation location;
bool required;
string description;
MetaTypeInfo type;
// Only for body parameters.
string mimeType;
2021-03-24 19:04:03 +00:00
}
/**
* 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) {
2021-02-20 22:33:07 +00:00
return namespace.map!toUpper().join("_") ~ "_"
~ className.applyCasePolicy(OPENAPI_CASING, CasePolicy.UPPER)
~ "_H";
}
2021-03-24 19:04:03 +00:00
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;
}
2021-03-24 19:04:03 +00:00
/// 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<type>\w*)\/(?P<subtype>(([\w\-]+\.)+)?[\w\-\*]+)(\+(?P<suffix>[\w\-\.]+))?(; (?P<parameter>[\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;
}
}
2021-03-24 19:04:03 +00:00
/**
* 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);
2021-03-24 19:04:03 +00:00
return or;
}
}