2021-02-20 22:33:07 +00:00
|
|
|
|
#!/usr/bin/env dub
|
|
|
|
|
/+ dub.sdl:
|
|
|
|
|
name "openapigenerator.d"
|
|
|
|
|
dependency "dyaml" version="~>0.8.0"
|
2021-03-19 22:01:29 +00:00
|
|
|
|
dependency "handlebars" version="~>0.2.2"
|
|
|
|
|
stringImportPaths "codegen"
|
2021-02-20 22:33:07 +00:00
|
|
|
|
+/
|
2021-02-20 23:26:18 +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;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
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.string;
|
|
|
|
|
import std.stdio;
|
|
|
|
|
import std.uni;
|
|
|
|
|
|
|
|
|
|
import dyaml;
|
2021-03-19 22:01:29 +00:00
|
|
|
|
import handlebars.tpl;
|
2021-02-20 22:33:07 +00:00
|
|
|
|
|
2021-03-25 16:32:00 +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×]
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
2021-02-20 23:26:18 +00:00
|
|
|
|
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
|
|
|
|
|
|
2021-02-20 23:26:18 +00:00
|
|
|
|
// File name of the CMake file this generated should generate.
|
|
|
|
|
string CMAKE_INCLUDE_FILE = "GeneratedSources.cmake";
|
|
|
|
|
string CMAKE_VAR_PREFIX = "openapi";
|
|
|
|
|
|
2021-03-05 14:34:10 +00:00
|
|
|
|
|
|
|
|
|
string INCLUDE_PREFIX = "JellyfinQt";
|
|
|
|
|
string SRC_PREFIX = "";
|
|
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
|
string MODEL_FOLDER = "dto";
|
2021-03-05 14:34:10 +00:00
|
|
|
|
string SUPPORT_FOLDER = "support";
|
2021-03-24 19:04:03 +00:00
|
|
|
|
string LOADER_FOLDER = "loader";
|
|
|
|
|
string HTTP_LOADER_FOLDER = buildPath("loader", "http");
|
2021-02-20 23:26:18 +00:00
|
|
|
|
|
2021-02-21 04:02:05 +00:00
|
|
|
|
string[string] compatAliases;
|
2021-03-05 14:34:10 +00:00
|
|
|
|
string[string] memberAliases;
|
2021-02-21 04:02:05 +00:00
|
|
|
|
|
|
|
|
|
static this() {
|
2021-03-05 14:34:10 +00:00
|
|
|
|
memberAliases["id"] = "jellyfinId";
|
2021-03-24 19:04:03 +00:00
|
|
|
|
memberAliases["static"] = "staticStreaming";
|
2021-02-21 04:02:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-02-20 23:26:18 +00:00
|
|
|
|
CasePolicy OPENAPI_CASING = CasePolicy.PASCAL;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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-03-20 02:30:50 +00:00
|
|
|
|
|
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";
|
2021-02-20 23:26:18 +00:00
|
|
|
|
// END CODE GENERATION SETTINGS.
|
|
|
|
|
|
|
|
|
|
// 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];
|
2021-03-05 14:34:10 +00:00
|
|
|
|
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));
|
2021-03-05 14:34:10 +00:00
|
|
|
|
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();
|
2021-02-20 23:26:18 +00:00
|
|
|
|
Appender!(string[]) headerFiles, implementationFiles;
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
2021-02-21 04:02:05 +00:00
|
|
|
|
foreach(string key, ref const Node scheme; root["components"]["schemas"]) {
|
2021-02-20 23:26:18 +00:00
|
|
|
|
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-02-20 23:26:18 +00:00
|
|
|
|
}
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
|
|
|
|
Appender!(Endpoint[]) endpoints;
|
|
|
|
|
foreach(string path, ref const Node operations; root["paths"]) {
|
|
|
|
|
foreach (string operation, ref const Node endpoint; operations) {
|
|
|
|
|
string fileBase = endpoint["operationId"].as!string.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+");
|
|
|
|
|
generateFileForEndpoint(path, operation, endpoint, root["components"]["schemas"], headerFile, implementationFile, endpoints);
|
|
|
|
|
}
|
2021-02-21 04:02:05 +00:00
|
|
|
|
}
|
2021-03-24 19:04:03 +00:00
|
|
|
|
string typesHeaderPath = buildPath(outputDirectory, "include", INCLUDE_PREFIX, LOADER_FOLDER, "requesttypes.h");
|
2021-03-25 16:32:00 +00:00
|
|
|
|
string typesImplementationPath = buildPath(outputDirectory, "src", SRC_PREFIX, LOADER_FOLDER, "requesttypes.cpp");
|
2021-03-24 19:04:03 +00:00
|
|
|
|
File typesHeader = File(typesHeaderPath, "w+");
|
2021-03-25 16:32:00 +00:00
|
|
|
|
File typesImplementation = File(typesImplementationPath, "w+");
|
|
|
|
|
headerFiles ~= [typesHeaderPath];
|
|
|
|
|
implementationFiles ~= [typesImplementationPath];
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
writeRequestTypesFile(typesHeader, typesImplementation, endpoints[].sort!((a, b) => a.name < b.name));
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
2021-02-20 23:26:18 +00:00
|
|
|
|
writeCMakeFile(headerFiles[], implementationFiles[]);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
|
/**
|
|
|
|
|
* Writes a CMake file that includes the specified files.
|
|
|
|
|
*/
|
2021-02-20 23:26:18 +00:00
|
|
|
|
void writeCMakeFile(string[] headerFiles, string[] implementationFiles) {
|
2021-02-21 04:02:05 +00:00
|
|
|
|
File output = File(buildPath(outputDirectory, CMAKE_INCLUDE_FILE), "w+");
|
2021-02-20 23:26:18 +00:00
|
|
|
|
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
|
|
|
|
}
|
2021-02-21 04:02:05 +00:00
|
|
|
|
output.writeln(")");
|
2021-02-20 23:26:18 +00:00
|
|
|
|
output.writeln();
|
|
|
|
|
|
|
|
|
|
output.writef("set(%s_SOURCES", CMAKE_VAR_PREFIX);
|
|
|
|
|
foreach (implementationFile; implementationFiles) {
|
|
|
|
|
output.writeln();
|
|
|
|
|
output.writef("\t%s", implementationFile);
|
|
|
|
|
}
|
2021-02-21 04:02:05 +00:00
|
|
|
|
output.writeln(")");
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
/**
|
|
|
|
|
* 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
|
2021-03-25 16:32:00 +00:00
|
|
|
|
.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-25 16:32:00 +00:00
|
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
|
|
|
|
struct EndpointController {
|
|
|
|
|
string name;
|
|
|
|
|
RequestParameter[] requiredPathParameters = [];
|
|
|
|
|
RequestParameter[] requiredQueryParameters = [];
|
|
|
|
|
RequestParameter[] optionalQueryParameters = [];
|
|
|
|
|
RequestParameter[] requiredParameters = [];
|
|
|
|
|
RequestParameter[] optionalParameters = [];
|
|
|
|
|
RequestParameter[] parameters = [];
|
|
|
|
|
}
|
2021-02-21 04:02:05 +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));
|
|
|
|
|
with (endpointController) {
|
2021-03-25 16:32:00 +00:00
|
|
|
|
parameters = requiredPathParameters ~ requiredQueryParameters ~ optionalQueryParameters;
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
|
|
|
|
requiredParameters = requiredPathParameters ~ requiredQueryParameters;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
optionalParameters = optionalQueryParameters;
|
2021-03-24 19:04:03 +00:00
|
|
|
|
}
|
|
|
|
|
controller.endpoints ~= [endpointController];
|
|
|
|
|
}
|
2021-03-25 16:32:00 +00:00
|
|
|
|
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");
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
|
|
|
|
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 endpoins
|
|
|
|
|
* Params:
|
|
|
|
|
* path = Path of the endpoint, for example "/foo/{barId}/baz";
|
|
|
|
|
* operation = HTTP method, like "get", "post", ...
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
void generateFileForEndpoint(ref const string path, ref const string operation, ref const Node endpointNode,
|
|
|
|
|
ref const Node allSchemas, ref scope File headerFile, ref scope File implementationFile,
|
|
|
|
|
ref scope Appender!(Endpoint[]) endpoints) {
|
|
|
|
|
|
|
|
|
|
string name = endpointNode["operationId"].as!string;
|
|
|
|
|
|
|
|
|
|
Endpoint endpoint = new Endpoint();
|
2021-03-25 16:32:00 +00:00
|
|
|
|
endpoint.name = name;
|
2021-03-24 19:04:03 +00:00
|
|
|
|
endpoint.parameterType = name ~ "Params";
|
|
|
|
|
endpoint.description = endpointNode.getOr!string("summary", "");
|
2021-03-25 16:32:00 +00:00
|
|
|
|
endpoint.path = path;
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
|
|
|
|
string[] systemImports = ["optional"];
|
2021-03-25 16:32:00 +00:00
|
|
|
|
string[] userImports = [
|
|
|
|
|
buildPath(SUPPORT_FOLDER, "jsonconv.h"),
|
|
|
|
|
buildPath(SUPPORT_FOLDER, "loader.h"),
|
|
|
|
|
buildPath(LOADER_FOLDER, "requesttypes.h"),
|
|
|
|
|
"apiclient.h"
|
|
|
|
|
];
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
|
|
|
|
// 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") {
|
|
|
|
|
if ("$ref" in content["schema"]) {
|
|
|
|
|
endpoint.hasSuccessResponse = true;
|
|
|
|
|
string reference = content["schema"]["$ref"].as!string.chompPrefix("#/components/schemas/");
|
|
|
|
|
endpoint.resultIsReference = true;
|
|
|
|
|
endpoint.resultType = reference;
|
|
|
|
|
string importFile = reference.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h";
|
|
|
|
|
userImports ~= [buildPath(MODEL_FOLDER, importFile)];
|
|
|
|
|
} else {
|
|
|
|
|
endpoint.resultIsReference = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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", "");
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
|
param.type = getType(param.name, yamlParameter["schema"], allSchemas);
|
2021-03-25 16:32:00 +00:00
|
|
|
|
if (!param.type.isNullable && !param.required && !param.type.hasDefaultValue) {
|
|
|
|
|
param.type.isNullable = true;
|
|
|
|
|
}
|
2021-03-24 19:04:03 +00:00
|
|
|
|
switch(yamlParameter["in"].as!string.toLower) {
|
|
|
|
|
case "path":
|
|
|
|
|
param.location = ParameterLocation.PATH;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
endpoint.requiredPathParameters ~= [param];
|
2021-03-24 19:04:03 +00:00
|
|
|
|
break;
|
|
|
|
|
case "query":
|
|
|
|
|
param.location = ParameterLocation.QUERY;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
if (param.required) {
|
|
|
|
|
endpoint.requiredQueryParameters ~= [param];
|
|
|
|
|
} else {
|
|
|
|
|
endpoint.optionalQueryParameters ~= [param];
|
|
|
|
|
}
|
2021-03-24 19:04:03 +00:00
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
assert(false);
|
|
|
|
|
}
|
|
|
|
|
endpoint.parameters ~= [param];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
endpoints ~= [endpoint];
|
|
|
|
|
|
|
|
|
|
// Render templates
|
|
|
|
|
class Controller {
|
|
|
|
|
string className;
|
|
|
|
|
//MetaTypeInfo[] properties;
|
|
|
|
|
string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
|
|
|
|
|
string dtoNamespace = namespaceString!CPP_NAMESPACE_DTO;
|
|
|
|
|
string responseType = "void";
|
|
|
|
|
string parameterType = "void";
|
|
|
|
|
Endpoint endpoint;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
|
|
|
|
string pathStringInterpolation() {
|
|
|
|
|
string result = "QStringLiteral(\"" ~ endpoint.path ~ "\")";
|
|
|
|
|
foreach(p; endpoint.parameters.filter!(p => p.location == ParameterLocation.PATH)) {
|
|
|
|
|
result = result.replace("{" ~ p.name ~ "}", "\") + Support::toString(params." ~ p.type.name ~ "()) + QStringLiteral(\"");
|
|
|
|
|
}
|
|
|
|
|
result = result.replace(`+ QStringLiteral("")`, "");
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2021-03-24 19:04:03 +00:00
|
|
|
|
}
|
|
|
|
|
Controller controller = new Controller();
|
|
|
|
|
controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
|
|
|
|
|
controller.endpoint = endpoint;
|
|
|
|
|
|
|
|
|
|
writeHeaderPreamble(headerFile, CPP_NAMESPACE_LOADER_HTTP, name, systemImports, userImports);
|
|
|
|
|
headerFile.writeln(render!(import("loader_header.hbs"), Controller)(controller));
|
|
|
|
|
writeHeaderPostamble(headerFile, CPP_NAMESPACE_LOADER_HTTP, name);
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
|
|
|
|
writeImplementationPreamble(implementationFile, CPP_NAMESPACE_LOADER_HTTP, HTTP_LOADER_FOLDER, name);
|
|
|
|
|
implementationFile.writeln(render!(import("loader_implementation.hbs"), Controller)(controller));
|
|
|
|
|
writeImplementationPostamble(implementationFile, CPP_NAMESPACE_LOADER_HTTP, name);
|
2021-03-24 19:04:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 16:32:00 +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
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
// Check if this JSON "thing" is an enum
|
2021-02-20 22:33:07 +00:00
|
|
|
|
if ("enum" in scheme) {
|
2021-03-05 14:34:10 +00:00
|
|
|
|
string[3] imports = ["QJsonValue", "QObject", "QString"];
|
2021-03-20 02:30:50 +00:00
|
|
|
|
string[1] userImports = [buildPath(SUPPORT_FOLDER, "jsonconv.h")];
|
|
|
|
|
writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, imports, userImports);
|
2021-02-20 22:33:07 +00:00
|
|
|
|
|
|
|
|
|
Appender!(string[]) values;
|
|
|
|
|
foreach (string value; scheme["enum"]) {
|
|
|
|
|
values ~= value;
|
|
|
|
|
}
|
|
|
|
|
writeEnumHeader(headerFile, name, values[]);
|
2021-03-20 02:30:50 +00:00
|
|
|
|
writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name);
|
2021-02-20 22:33:07 +00:00
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name);
|
2021-03-20 15:29:31 +00:00
|
|
|
|
writeEnumImplementation(implementationFile, name, values[]);
|
2021-03-20 02:30:50 +00:00
|
|
|
|
writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name);
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
2021-03-25 16:32:00 +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;
|
2021-02-21 04:02:05 +00:00
|
|
|
|
Appender!(string[]) forwardDeclarations;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
systemImports ~= ["optional", "QJsonObject", "QJsonValue"];
|
2021-03-05 14:34:10 +00:00
|
|
|
|
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) {
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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];
|
|
|
|
|
}
|
2021-03-20 02:30:50 +00:00
|
|
|
|
} 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-21 04:02:05 +00:00
|
|
|
|
}
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
foreach (type; usedTypes) {
|
|
|
|
|
collectImports(type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort them for nicer reading
|
|
|
|
|
string[] sortedSystemImports = sort(systemImports[]).array;
|
|
|
|
|
string[] sortedUserImports = sort(userImports[]).array;
|
2021-02-21 04:02:05 +00:00
|
|
|
|
string[] sortedForwardDeclarations = sort(forwardDeclarations[]).array;
|
2021-02-20 22:33:07 +00:00
|
|
|
|
|
|
|
|
|
// Write implementation files
|
2021-03-20 02:30:50 +00:00
|
|
|
|
writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, sortedSystemImports, sortedUserImports);
|
2021-02-21 04:02:05 +00:00
|
|
|
|
writeObjectHeader(headerFile, name, usedTypes, sortedForwardDeclarations);
|
2021-03-20 02:30:50 +00:00
|
|
|
|
writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name);
|
2021-02-20 22:33:07 +00:00
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name);
|
2021-02-20 22:33:07 +00:00
|
|
|
|
writeObjectImplementation(implementationFile, name, usedTypes);
|
2021-03-20 02:30:50 +00:00
|
|
|
|
writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name);
|
2021-03-25 16:32:00 +00:00
|
|
|
|
} 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-20 02:30:50 +00:00
|
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
|
// We need to recurse (sometimes)
|
2021-03-25 16:32:00 +00:00
|
|
|
|
/**
|
|
|
|
|
* 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);
|
2021-03-25 16:32:00 +00:00
|
|
|
|
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);
|
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
// 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..$];
|
2021-03-25 16:32:00 +00:00
|
|
|
|
info.needsLocalImport = true;
|
|
|
|
|
info.typeName = type;
|
|
|
|
|
if (type in allSchemas) {
|
|
|
|
|
if ("type" in allSchemas[type]
|
2021-03-24 19:04:03 +00:00
|
|
|
|
&& allSchemas[type]["type"].as!string == "object") {
|
2021-03-25 16:32:00 +00:00
|
|
|
|
info.needsPointer = true;
|
|
|
|
|
info.isTypeNullable = true;
|
|
|
|
|
info.typeNullableCheck = ".isNull()";
|
|
|
|
|
info.typeNullableSetter = ".clear()";
|
|
|
|
|
} else if ("enum" in allSchemas[type]) {
|
|
|
|
|
info.isTypeNullable = true;
|
|
|
|
|
info.typeNullableCheck = "== " ~ info.typeName ~ "::EnumNotSet";
|
|
|
|
|
info.typeNullableSetter = "= " ~ info.typeName ~ "::EnumNotSet";
|
|
|
|
|
}
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
2021-03-24 19:04:03 +00:00
|
|
|
|
return info;
|
|
|
|
|
}
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
|
|
|
|
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;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
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
|
|
|
|
|
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) {
|
2021-03-19 22:01:29 +00:00
|
|
|
|
class Controller {
|
|
|
|
|
string className;
|
|
|
|
|
MetaTypeInfo[] properties;
|
|
|
|
|
string[] userImports;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
2021-03-19 22:01:29 +00:00
|
|
|
|
Controller controller = new Controller();
|
|
|
|
|
controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
|
|
|
|
|
controller.properties = properties;
|
|
|
|
|
controller.userImports = userImports;
|
2021-02-20 22:33:07 +00:00
|
|
|
|
|
2021-03-19 22:01:29 +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) {
|
2021-03-19 22:01:29 +00:00
|
|
|
|
class Controller {
|
|
|
|
|
string className;
|
|
|
|
|
MetaTypeInfo[] properties;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
2021-03-19 22:01:29 +00:00
|
|
|
|
Controller controller = new Controller();
|
|
|
|
|
controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
|
|
|
|
|
controller.properties = properties;
|
|
|
|
|
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 = "") {
|
2021-03-19 22:01:29 +00:00
|
|
|
|
class Controller {
|
|
|
|
|
string className;
|
|
|
|
|
string[] values;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
2021-03-19 22:01:29 +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
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 15:29:31 +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
|
2021-03-20 02:30:50 +00:00
|
|
|
|
void writeHeaderPreamble(File output, immutable string[] fileNamespace, string className, string[] imports = [], string[] userImports = []) {
|
2021-02-20 23:26:18 +00:00
|
|
|
|
output.writeln(COPYRIGHT);
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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();
|
|
|
|
|
|
2021-02-21 04:02:05 +00:00
|
|
|
|
foreach(file; userImports) {
|
2021-03-05 14:34:10 +00:00
|
|
|
|
output.writefln("#include \"%s\"", buildPath(INCLUDE_PREFIX, file));
|
2021-02-21 04:02:05 +00:00
|
|
|
|
}
|
|
|
|
|
if (userImports.length > 0) output.writeln();
|
|
|
|
|
|
2021-03-20 02:30:50 +00:00
|
|
|
|
foreach (namespace; fileNamespace) {
|
2021-02-20 22:33:07 +00:00
|
|
|
|
output.writefln("namespace %s {", namespace);
|
|
|
|
|
}
|
|
|
|
|
output.writeln();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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();
|
2021-03-20 02:30:50 +00:00
|
|
|
|
output.writefln("#endif // %s", guardName(fileNamespace, className));
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
void writeImplementationPreamble(File output, immutable string[] fileNamespace, string folder, string className, string[] imports = []) {
|
2021-02-20 23:26:18 +00:00
|
|
|
|
output.writeln(COPYRIGHT);
|
2021-03-25 16:32:00 +00:00
|
|
|
|
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) {
|
2021-03-05 14:34:10 +00:00
|
|
|
|
output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, file));
|
2021-02-20 22:33:07 +00:00
|
|
|
|
}
|
|
|
|
|
if (imports.length > 0) output.writeln();
|
|
|
|
|
|
2021-03-20 02:30:50 +00:00
|
|
|
|
foreach (namespace; fileNamespace) {
|
2021-02-20 22:33:07 +00:00
|
|
|
|
output.writefln("namespace %s {", namespace);
|
|
|
|
|
}
|
|
|
|
|
output.writeln();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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) {
|
|
|
|
|
char[] mutableSource = source.dup;
|
|
|
|
|
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 if (!isLower(c) && !isLower(n)) {
|
|
|
|
|
result ~= toUpper(c);
|
|
|
|
|
} 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:
|
2021-03-05 14:34:10 +00:00
|
|
|
|
string originalName = "";
|
2021-02-20 22:33:07 +00:00
|
|
|
|
string name = "";
|
|
|
|
|
string typeName = "";
|
2021-03-20 02:30:50 +00:00
|
|
|
|
/// Description of this property.
|
2021-02-20 22:33:07 +00:00
|
|
|
|
string description = "";
|
2021-03-20 02:30:50 +00:00
|
|
|
|
/// If this property is nullable according to the OpenAPI spec.
|
|
|
|
|
bool isNullable = false;
|
2021-02-20 22:33:07 +00:00
|
|
|
|
bool needsPointer = false;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
/// If the type needs a system import (Such as Qt types)
|
2021-02-20 22:33:07 +00:00
|
|
|
|
bool needsSystemImport = false;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
/// 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;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
/// If the type is a container type
|
2021-02-20 22:33:07 +00:00
|
|
|
|
bool isContainer = false;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
/// 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;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
string defaultValue = "";
|
|
|
|
|
|
|
|
|
|
bool hasDefaultValue() {
|
|
|
|
|
return defaultValue.length > 0;
|
|
|
|
|
}
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
2021-02-20 22:33:07 +00:00
|
|
|
|
string writeName() {
|
|
|
|
|
return name.applyCasePolicy(CPP_CLASS_MEMBER_CASING, CasePolicy.PASCAL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string memberName() {
|
|
|
|
|
return CPP_CLASS_MEMBER_PREFIX ~ name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string typeNameWithQualifiers() {
|
|
|
|
|
if (needsPointer) {
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 02:30:50 +00:00
|
|
|
|
bool needsOptional() {
|
2021-03-25 16:32:00 +00:00
|
|
|
|
return (isNullable || hasDefaultValue) && !isTypeNullable;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string typeNullableCheck;
|
|
|
|
|
string nullableCheck() {
|
|
|
|
|
if (needsOptional) {
|
|
|
|
|
return "!" ~ memberName ~ ".has_value()";
|
2021-03-25 16:32:00 +00:00
|
|
|
|
} else if (typeNullableCheck.length > 0) {
|
|
|
|
|
return memberName ~ typeNullableCheck;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
}
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
2021-03-20 02:30:50 +00:00
|
|
|
|
return "Q_ASSERT(false)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string typeNullableSetter = "";
|
|
|
|
|
string nullableSetter() {
|
|
|
|
|
if (needsOptional) {
|
2021-03-24 19:04:03 +00:00
|
|
|
|
return " = std::nullopt";
|
2021-03-20 02:30:50 +00:00
|
|
|
|
}
|
|
|
|
|
if (typeNullableSetter.startsWith("=")) {
|
2021-03-24 19:04:03 +00:00
|
|
|
|
return typeNullableSetter;
|
2021-03-20 02:30:50 +00:00
|
|
|
|
} else {
|
|
|
|
|
return typeNullableSetter;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-20 22:33:07 +00:00
|
|
|
|
string defaultInitializer() {
|
|
|
|
|
if (needsPointer) return "nullptr";
|
2021-03-20 02:30:50 +00:00
|
|
|
|
if (needsOptional) return "std::nullopt";
|
2021-02-20 22:33:07 +00:00
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
/**
|
|
|
|
|
* Represents an API endpoint.
|
|
|
|
|
*/
|
2021-03-24 19:04:03 +00:00
|
|
|
|
class Endpoint {
|
|
|
|
|
bool resultIsReference = false;
|
|
|
|
|
bool hasSuccessResponse = false;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
string name;
|
|
|
|
|
|
|
|
|
|
/// The type of the
|
2021-03-24 19:04:03 +00:00
|
|
|
|
string resultType;
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
|
|
|
|
/// The name of the structure containing the parameters for this endpoint.
|
2021-03-24 19:04:03 +00:00
|
|
|
|
string parameterType = "void";
|
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
/// HTTP path for this endpoint.
|
|
|
|
|
string path;
|
|
|
|
|
|
|
|
|
|
/// Description/documentation for this endpoint
|
2021-03-24 19:04:03 +00:00
|
|
|
|
string description;
|
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
/// HTTP method for this endpoint
|
2021-03-24 19:04:03 +00:00
|
|
|
|
string method;
|
|
|
|
|
|
2021-03-25 16:32:00 +00:00
|
|
|
|
/// List of all parameters for this request
|
2021-03-24 19:04:03 +00:00
|
|
|
|
RequestParameter[] parameters = [];
|
2021-03-25 16:32:00 +00:00
|
|
|
|
|
|
|
|
|
RequestParameter[] requiredPathParameters;
|
|
|
|
|
RequestParameter[] requiredQueryParameters;
|
|
|
|
|
RequestParameter[] optionalQueryParameters;
|
2021-03-24 19:04:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum ParameterLocation {
|
|
|
|
|
PATH,
|
2021-03-25 16:32:00 +00:00
|
|
|
|
QUERY,
|
|
|
|
|
COOKIE,
|
|
|
|
|
HEADER
|
2021-03-24 19:04:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class RequestParameter {
|
|
|
|
|
string name;
|
|
|
|
|
ParameterLocation location;
|
|
|
|
|
bool required;
|
|
|
|
|
string description;
|
|
|
|
|
MetaTypeInfo type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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).
|
|
|
|
|
*/
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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-20 02:30:50 +00:00
|
|
|
|
|
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.
|
|
|
|
|
*/
|
2021-03-20 02:30:50 +00:00
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 {
|
2021-03-25 16:32:00 +00:00
|
|
|
|
//stdout.writefln("Could not find %s", key);
|
2021-03-24 19:04:03 +00:00
|
|
|
|
return or;
|
|
|
|
|
}
|
|
|
|
|
}
|