1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-11-16 14:35:17 +00:00
harbour-sailfin/core/openapigenerator.d
Chris Josten 90db983c30 Replace not-fully-initializing DTO constructors
There were some constructors in the DTOs which allowed construction of
DTO which weren't fully initialized. These constructors have been made
private, as they are still used in the 'fromJson' methods. Additionally,
a constructor with all required parameters to fully initialize the
class has been added.

Additionally, the Loader class has been modified, since it no longer can
assume it is able to default construct the parameter type. The parameter
is now stored as an optional.

Closes #15
2021-09-25 17:07:12 +02:00

1284 lines
40 KiB
D
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <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));
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 ("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<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;
}
}
/**
* 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;
}
}