1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-04 01:42:44 +00:00

WIP: logic rewrite

WIP: adding loaders
This commit is contained in:
Chris Josten 2021-03-24 20:04:03 +01:00
parent b9b08ab384
commit 2360b261f7
1769 changed files with 124903 additions and 1963 deletions

View file

@ -29,9 +29,11 @@ string COPYRIGHT = q"EOL
EOL";
import std.algorithm;
import std.array;
import std.conv;
import std.file : mkdirRecurse;
import std.exception;
import std.path : buildPath, dirSeparator;
import std.parallelism : parallel;
import std.range;
import std.string;
import std.stdio;
@ -65,24 +67,25 @@ string CMAKE_VAR_PREFIX = "openapi";
string INCLUDE_PREFIX = "JellyfinQt";
string SRC_PREFIX = "";
string MODEL_FOLDER = "DTO";
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() {
/*compatAliases["BaseItemDto"] = "Item";
compatAliases["UserDto"] = "User";
compatAliases["UserItemDataDto"] = "UserData";*/
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;
@ -129,23 +132,54 @@ void realMain(string[] args) {
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"]) {
generateFileForSchema(key, scheme, root["components"]["schemas"]);
string fileBase = key.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING);
headerFiles ~= [buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER, fileBase ~ ".h")];
implementationFiles ~= [buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER, fileBase ~ ".cpp")];
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);
}
foreach(string original, string compatAlias; compatAliases) {
writeCompatAliasFile(original, compatAlias);
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);
}
}
string typesHeaderPath = buildPath(outputDirectory, "include", INCLUDE_PREFIX, LOADER_FOLDER, "requesttypes.h");
File typesHeader = File(typesHeaderPath, "w+");
implementationFiles ~= [typesHeaderPath];
writeRequestTypesFile(typesHeader, endpoints[]);
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)");
@ -171,20 +205,188 @@ void writeCMakeFile(string[] headerFiles, string[] implementationFiles) {
output.writeln(")");
}
void writeCompatAliasFile(ref const string original, ref const string compatAlias) {
string fileBase = compatAlias.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING);
File headerFile = File(buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER, fileBase ~ ".h"), "w+");
File implementationFile = File(buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER, fileBase ~ ".cpp"), "w+");
void writeRequestTypesFile(R)(File headerFile, R endpoints) if(is(ElementType!R : Endpoint)) {
writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, compatAlias, [], [original]);
headerFile.writefln("using %s = %s;", compatAlias, original);
writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, compatAlias);
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", "optional"];
string[] userImports = collectImports(endpoints, e => e.needsLocalImport)
.map!(e => buildPath(MODEL_FOLDER, e.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h"))
.array;
headerFile.writeHeaderPreamble(CPP_NAMESPACE_LOADER, "RequestTypes", systemImports, userImports);
struct EndpointController {
string name;
RequestParameter[] requiredPathParameters = [];
RequestParameter[] optionalPathParameters = [];
RequestParameter[] requiredQueryParameters = [];
RequestParameter[] optionalQueryParameters = [];
RequestParameter[] requiredParameters = [];
RequestParameter[] optionalParameters = [];
RequestParameter[] parameters = [];
}
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.optionalPathParameters =
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) {
parameters = requiredPathParameters ~ requiredQueryParameters ~ optionalPathParameters ~ optionalQueryParameters;
requiredParameters = requiredPathParameters ~ requiredQueryParameters;
optionalParameters = optionalPathParameters ~ optionalQueryParameters;
}
controller.endpoints ~= [endpointController];
}
headerFile.writeln(render!(import("loader_types_header.hbs"), Controller)(controller));
headerFile.writeHeaderPostamble(CPP_NAMESPACE_LOADER, "RequestTypes");
}
void generateFileForSchema(ref string name, ref const Node scheme, Node allSchemas) {
string fileBase = name.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING);
File headerFile = File(buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER, fileBase ~ ".h"), "w+");
File implementationFile = File(buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER, fileBase ~ ".cpp"), "w+");
/**
* 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();
endpoint.parameterType = name ~ "Params";
endpoint.description = endpointNode.getOr!string("summary", "");
string[] systemImports = ["optional"];
string[] userImports = [buildPath(SUPPORT_FOLDER, "loader.h"), "apiclient.h", buildPath(LOADER_FOLDER, "requesttypes.h")];
// 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", "");
param.type = getType(param.name, yamlParameter["schema"], allSchemas);
switch(yamlParameter["in"].as!string.toLower) {
case "path":
param.location = ParameterLocation.PATH;
break;
case "query":
param.location = ParameterLocation.QUERY;
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;
}
Controller controller = new Controller();
controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
controller.endpoint = endpoint;
//controller.properties = properties;
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);
}
void generateFileForSchema(ref const string name, ref const Node scheme, Node allSchemas,
ref scope File headerFile, ref scope File implementationFile) {
if ("enum" in scheme) {
string[3] imports = ["QJsonValue", "QObject", "QString"];
@ -210,6 +412,7 @@ void generateFileForSchema(ref string name, ref const Node scheme, Node allSchem
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")) {
@ -256,119 +459,124 @@ void generateFileForSchema(ref string name, ref const Node scheme, Node allSchem
}
// Object
MetaTypeInfo[] collectTypeInfo(Node properties, Node allSchemas) {
// We need to recurse (sometimes)
MetaTypeInfo getType(string name, Node node) {
MetaTypeInfo info = new MetaTypeInfo();
info.originalName = name;
info.name = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_MEMBER_CASING);
if ("description" in node) {
info.description = node["description"].as!string;
}
// Special case for QML
info.name = memberAliases.get(info.name.toLower(), info.name);
if ("$ref" in node) {
string type = node["$ref"].as!string()["#/components/schemas/".length..$];
if (type in allSchemas&& "type" in allSchemas[type]
&& allSchemas[type]["type"].as!string == "object") {
info.needsPointer = true;
info.isTypeNullable = true;
info.typeNullableCheck = ".isNull()";
info.typeNullableSetter = ".clear()";
}
info.needsLocalImport = true;
info.typeName = type;
return info;
}
if (!("type" in node)) {
info.typeName = "QVariant";
info.isTypeNullable = true;
info.needsSystemImport = true;
info.typeNullableCheck = ".isNull()";
info.typeNullableSetter = ".clear()";
return info;
}
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
return info;
case "array":
MetaTypeInfo containedType = getType("arrayItem", node["items"]);
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;
}
// We need to recurse (sometimes)
MetaTypeInfo getType(ref 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);
if ("description" in node) {
info.description = node["description"].as!string;
}
// Special case for QML
info.name = memberAliases.get(info.name.toLower(), info.name);
info.isNullable = node.getOr("nullable", false);
if ("$ref" in node) {
string type = node["$ref"].as!string()["#/components/schemas/".length..$];
if (type in allSchemas&& "type" in allSchemas[type]
&& allSchemas[type]["type"].as!string == "object") {
info.needsPointer = true;
info.isTypeNullable = true;
info.typeNullableCheck = ".isNull()";
info.typeNullableSetter = ".clear()";
}
info.needsLocalImport = true;
info.typeName = type;
return info;
}
if (!("type" in node)) {
info.typeName = "QVariant";
info.isTypeNullable = true;
info.needsSystemImport = true;
info.typeNullableCheck = ".isNull()";
info.typeNullableSetter = ".clear()";
return info;
}
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
return info;
case "array":
string containedTypeName = "arrayItem";
MetaTypeInfo containedType = getType(containedTypeName, node["items"], allSchemas);
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(const ref string name, const ref Node node; properties) {
result ~= getType(name, node);
foreach(ref string name, const ref Node node; properties) {
result ~= getType(name, node, allSchemas);
}
return result[];
}
@ -454,7 +662,7 @@ void writeHeaderPreamble(File output, immutable string[] fileNamespace, string c
void writeHeaderPostamble(File output, immutable string[] fileNamespace, string className) {
output.writeln();
foreach(namespace; fileNamespace) {
foreach_reverse(namespace; fileNamespace) {
output.writefln("} // NS %s", namespace);
}
output.writeln();
@ -479,13 +687,21 @@ void writeImplementationPreamble(File output, immutable string[] fileNamespace,
void writeImplementationPostamble(File output, immutable string[] fileNamespace, string className) {
output.writeln();
foreach(namespace; fileNamespace) {
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) {
@ -543,6 +759,10 @@ string applyCasePolicy(string source, CasePolicy input, CasePolicy output) {
}
}
unittest {
assert("fooBar".applyCasePolicy(CasePolicy.CAMEL, CasePolicy.SNAKE) == "foo_bar");
}
class MetaTypeInfo {
public:
string originalName = "";
@ -564,6 +784,9 @@ public:
/// If `isContainer` is true, the type of the container.
MetaTypeInfo containerType = null;
/// For use in templating
bool isLast = false;
string writeName() {
return name.applyCasePolicy(CPP_CLASS_MEMBER_CASING, CasePolicy.PASCAL);
}
@ -575,6 +798,9 @@ public:
string typeNameWithQualifiers() {
if (needsPointer) {
return "QSharedPointer<" ~ typeName~ ">";
}
if (needsOptional) {
return "std::optional<" ~ typeName ~ ">";
} else {
return typeName;
}
@ -587,7 +813,7 @@ public:
string typeNullableCheck;
string nullableCheck() {
if (typeNullableCheck.length > 0) {
return memberName ~ "." ~ typeNullableCheck;
return memberName ~ typeNullableCheck;
}
if (needsOptional) {
return "!" ~ memberName ~ ".has_value()";
@ -598,10 +824,10 @@ public:
string typeNullableSetter = "";
string nullableSetter() {
if (needsOptional) {
return memberName ~ "= std::nullopt";
return " = std::nullopt";
}
if (typeNullableSetter.startsWith("=")) {
return memberName ~ typeNullableSetter;
return typeNullableSetter;
} else {
return typeNullableSetter;
}
@ -614,12 +840,52 @@ public:
}
}
class Endpoint {
bool resultIsReference = false;
bool hasSuccessResponse = false;
string resultType;
string parameterType = "void";
string description;
string method;
RequestParameter[] parameters = [];
}
enum ParameterLocation {
PATH,
QUERY
}
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).
*/
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) {
@ -630,3 +896,34 @@ string namespaceString(string[] name)() {
}
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");
}
/**
* 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 {
return or;
}
}