WIP: extract loading logic from model

Loading logic should be extracted from the model, to make
loading the data from other sources, such as a local database for
synchronised items, possible.
This commit is contained in:
Chris Josten 2021-03-05 15:34:10 +01:00
parent 40d8793bd8
commit 17d4b2c24b
7 changed files with 168 additions and 21 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ rpm/*.spec
# Build folders
build/
build-*/
.dub/
# IDE files
*.user

View File

@ -4,6 +4,7 @@ set (CMAKE_CXX_STANDARD 17)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
set(CMAKE_AUTOMOC ON)
cmake_policy(SET CMP0048 NEW)
set(CMAKE_CXX_STANDARD 17)
# Options
option(PLATFORM_SAILFISHOS "Build SailfishOS version of application" OFF)

View File

@ -6,6 +6,7 @@ include(GeneratedSources.cmake)
set(jellyfin-qt_SOURCES
# src/DTO/dto.cpp
src/support/loader.cpp
src/apiclient.cpp
src/apimodel.cpp
src/credentialmanager.cpp
@ -21,6 +22,7 @@ list(APPEND jellyfin-qt_SOURCES ${openapi_SOURCES})
set(jellyfin-qt_HEADERS
# include/JellyfinQt/DTO/dto.h
include/JellyfinQt/support/loader.h
include/JellyfinQt/apiclient.h
include/JellyfinQt/apimodel.h
include/JellyfinQt/credentialmanager.h

View File

@ -159,6 +159,9 @@ protected:
}
}
ApiClient *m_apiClient = nullptr;
protected:
// Returns true if this class is instantiated within QML and is still being parsed.
bool isQmlParsing() const { return m_isParsing; }
private:
Status m_status = Uninitialised;
QNetworkReply::NetworkError m_error = QNetworkReply::NoError;

View File

@ -0,0 +1,90 @@
#ifndef JELLYFIN_SUPPORT_LOADER_H
#define JELLYFIN_SUPPORT_LOADER_H
#include <exception>
#include <stdexcept>
#include <QJsonDocument>
#include <QUrlQuery>
#include <QString>
#include <JellyfinQt/apiclient.h>
namespace Jellfyin {
namespace Support {
class LoadException : public std::runtime_error {
public:
explicit LoadException(const char *message)
: std::runtime_error(message) {}
};
static const int HTTP_TIMEOUT = 30000; // 30 seconds;
/**
* Interface describing a way to load items. Used to abstract away
* the difference between loading from a cache or loading over the network.
*
* @tparam R the type of data that should be fetched, R for result.
* @tparam P the type of paramaters given, to determine which resource should
* be loaded.
*/
template <typename R, typename P>
class Loader {
using ApiClient = Jellyfin::ApiClient;
public:
explicit Loader(ApiClient *apiClient) : m_apiClient(apiClient) {}
/**
* @brief load Loads the given resource.
* @param parameters Parameters to determine which resource should be loaded.
* @return The resource if successfull.
*/
virtual R load(const P &parameters) const;
/**
* @brief Heuristic to determine if this resource can be loaded via this loaded.
*
* For example, a Loader that requires the network to be available should return false
* if the network is not available.
* @return True if this loader is available, false otherwise.
*/
virtual bool isAvailable() const;
protected:
ApiClient *m_apiClient;
};
template <typename R, typename P>
class HttpLoader : public Loader<R, P> {
public:
R load(const P &parameters) const override {
QNetworkReply *reply = m_apiClient->get(url(parameters), query(parameters));
QByteArray array;
while (!reply->atEnd()) {
if (!reply->waitForReadyRead(HTTP_TIMEOUT)) {
if (reply->error() == QNetworkReply::NoError) {
reply->deleteLater();
throw LoadException("HTTP timeout");
}
reply->deleteLater();
throw LoadException("HTTP error");
}
array.append(reply->readAll());
}
reply->deleteLater();
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(array, &error);
if (error.error != QJsonParseError::NoError) {
throw LoadException(error.errorString().toLocal8Bit().constData());
}
return parse(document);
}
protected:
virtual const QString url(const P &parameters) const;
virtual const QUrlQuery query(const P &parameters) const;
virtual R parse(const QJsonObject &object) const;
};
} // NS Support
} // NS Jellyfin
#endif // JELLYFIN_SUPPORT_LOADER_H

View File

@ -58,15 +58,22 @@ EOS";
string CMAKE_INCLUDE_FILE = "GeneratedSources.cmake";
string CMAKE_VAR_PREFIX = "openapi";
string INCLUDE_PREFIX = "JellyfinQt/DTO";
string SRC_PREFIX = "DTO";
string INCLUDE_PREFIX = "JellyfinQt";
string SRC_PREFIX = "";
string MODEL_FOLDER = "model";
string SUPPORT_FOLDER = "support";
string[string] compatAliases;
string[string] memberAliases;
static this() {
compatAliases["BaseItemDto"] = "Item";
compatAliases["UserDto"] = "User";
compatAliases["UserItemDataDto"] = "UserData";
memberAliases["id"] = "jellyfinId";
}
CasePolicy OPENAPI_CASING = CasePolicy.PASCAL;
@ -115,8 +122,8 @@ void realMain(string[] args) {
string schemeFile = args[1];
if (args.length >= 3) outputDirectory = args[2];
mkdirRecurse(buildPath(outputDirectory, "include", INCLUDE_PREFIX));
mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX));
mkdirRecurse(buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER));
mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER));
Node root = Loader.fromFile(schemeFile).load();
Appender!(string[]) headerFiles, implementationFiles;
@ -124,8 +131,8 @@ void realMain(string[] args) {
generateFileForSchema(key, scheme, root["components"]["schemas"]);
string fileBase = key.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING);
headerFiles ~= [buildPath(outputDirectory, "include", INCLUDE_PREFIX, fileBase ~ ".h")];
implementationFiles ~= [buildPath(outputDirectory, "src", SRC_PREFIX, fileBase ~ ".cpp")];
headerFiles ~= [buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER, fileBase ~ ".h")];
implementationFiles ~= [buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER, fileBase ~ ".cpp")];
}
foreach(string original, string compatAlias; compatAliases) {
writeCompatAliasFile(original, compatAlias);
@ -160,8 +167,8 @@ void writeCMakeFile(string[] headerFiles, string[] implementationFiles) {
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, fileBase ~ ".h"), "w+");
File implementationFile = File(buildPath(outputDirectory, "src", SRC_PREFIX, fileBase ~ ".cpp"), "w+");
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+");
writeHeaderPreamble(headerFile, compatAlias, [], [original]);
headerFile.writefln("using %s = %s;", compatAlias, original);
@ -170,11 +177,11 @@ void writeCompatAliasFile(ref const string original, ref const string compatAlia
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, fileBase ~ ".h"), "w+");
File implementationFile = File(buildPath(outputDirectory, "src", SRC_PREFIX, fileBase ~ ".cpp"), "w+");
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+");
if ("enum" in scheme) {
string[1] imports = ["QObject"];
string[3] imports = ["QJsonValue", "QObject", "QString"];
writeHeaderPreamble(headerFile, name, imports);
Appender!(string[]) values;
@ -193,6 +200,7 @@ void generateFileForSchema(ref string name, ref const Node scheme, Node allSchem
Appender!(string[]) systemImports, userImports;
Appender!(string[]) forwardDeclarations;
systemImports ~= ["QObject", "QJsonObject"];
userImports ~= [buildPath(SUPPORT_FOLDER, "jsonconv.h")];
MetaTypeInfo[] usedTypes = collectTypeInfo(scheme["properties"], allSchemas);
bool importedContainers = false;
@ -213,7 +221,7 @@ void generateFileForSchema(ref string name, ref const Node scheme, Node allSchem
if (type.needsPointer) {
forwardDeclarations ~= type.typeName;
} else {
userImports ~= type.typeName;
userImports ~= buildPath(MODEL_FOLDER, type.typeName.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h");
}
}
}
@ -231,7 +239,7 @@ void generateFileForSchema(ref string name, ref const Node scheme, Node allSchem
writeObjectHeader(headerFile, name, usedTypes, sortedForwardDeclarations);
writeHeaderPostamble(headerFile, name);
writeImplementationPreamble(implementationFile, name, sortedUserImports);
writeImplementationPreamble(implementationFile, name);
writeObjectImplementation(implementationFile, name, usedTypes);
writeImplementationPostamble(implementationFile, name);
}
@ -243,13 +251,14 @@ 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
if (info.name.toLower() == "id") info.name = "jellyfinId";
info.name = memberAliases.get(info.name.toLower(), info.name);
if ("$ref" in node) {
string type = node["$ref"].as!string()["#/components/schemas/".length..$];
@ -342,7 +351,7 @@ void writeObjectHeader(File output, string name, MetaTypeInfo[] properties, stri
output.writefln("public:");
output.writefln("\texplicit %s(QObject *parent = nullptr);", className);
output.writefln("\tstatic %s *fromJSON(QJsonObject source, QObject *parent = nullptr);", className);
output.writefln("\tvoid updateFromJSON(QJsonObject source);");
output.writefln("\tvoid updateFromJSON(QJsonObject source, bool emitSignals = true);");
output.writefln("\tQJsonObject toJSON();");
output.writeln();
@ -399,18 +408,31 @@ void writeObjectImplementation(File output, string name, MetaTypeInfo[] properti
output.writefln("%s *%s::fromJSON(QJsonObject source, QObject *parent) {", className, className);
output.writefln("\t%s *instance = new %s(parent);", className, className);
output.writefln("\tinstance->updateFromJSON(source);", className);
output.writefln("\tinstance->updateFromJSON(source, false);", className);
output.writefln("\treturn instance;");
output.writefln("}");
output.writeln();
output.writefln("void %s::updateFromJSON(QJsonObject source) {", className, className);
output.writefln("void %s::updateFromJSON(QJsonObject source, bool emitSignals) {", className, className);
output.writefln("\tQ_UNIMPLEMENTED();");
foreach (property; properties) {
output.writefln("\t%s = fromJsonValue<%s>(source[\"%s\");", property.memberName, property.typeNameWithQualifiers,
property.originalName);
}
output.writeln();
output.writefln("\tif (emitSignals) {");
foreach (property; properties) {
output.writefln("\t\temit %sChanged(%s);", property.name, property.memberName);
}
output.writefln("\t}");
output.writefln("}");
output.writefln("QJsonObject %s::toJSON() {", className);
output.writefln("\tQ_UNIMPLEMENTED();");
output.writefln("\tQJsonObject result;");
foreach (property; properties) {
output.writefln("\tresult[\"%s\"] = toJsonValue<%s>(%s);", property.originalName, property.typeNameWithQualifiers,
property.memberName);
}
output.writefln("\treturn result;");
output.writefln("}");
@ -434,6 +456,7 @@ void writeEnumHeader(File output, string name, string[] values, string doc = "")
output.writefln("\tQ_GADGET");
output.writefln("public:");
output.writefln("\tenum Value {");
output.writefln("\t\tEnumNotSet,");
foreach (value; values) {
output.writefln("\t\t%s,", value);
}
@ -443,6 +466,26 @@ void writeEnumHeader(File output, string name, string[] values, string doc = "")
output.writefln("\texplicit %sClass();", className);
output.writefln("};");
output.writefln("typedef %sClass::Value %s;", className, className);
output.writeln();
output.writefln("template <>");
output.writefln("%s fromJsonValue<%s>(QJsonValue source) {", className, className);
output.writefln("\tif (!source.isString()) return %sClass::EnumNotSet;", className);
output.writeln();
output.writefln("\tQString str = source.toString();");
if (values.length > 0) {
output.writefln("\tif (str == QStringLiteral(\"%s\")) {", values[0]);
output.writefln("\t\treturn %sClass::%s;", className, values[0]);
output.write("\t}");
foreach(value; drop(values, 1)) {
output.writefln(" else if (str == QStringLiteral(\"%s\")) {", value);
output.writefln("\t\treturn %sClass::%s;", className, value);
output.write("\t}");
}
output.writeln();
}
output.writeln();
output.writefln("\treturn %sClass::EnumNotSet;", className);
output.writefln("}");
}
void writeEnumImplementation(File output, string name) {
@ -466,7 +509,7 @@ void writeHeaderPreamble(File output, string className, string[] imports = [], s
if (imports.length > 0) output.writeln();
foreach(file; userImports) {
output.writefln("#include \"%s\"", buildPath(INCLUDE_PREFIX, file.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h"));
output.writefln("#include \"%s\"", buildPath(INCLUDE_PREFIX, file));
}
if (userImports.length > 0) output.writeln();
@ -487,11 +530,11 @@ void writeHeaderPostamble(File output, string className) {
void writeImplementationPreamble(File output, string className, string[] imports = []) {
output.writeln(COPYRIGHT);
output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, className.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h"));
output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, MODEL_FOLDER, className.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h"));
output.writeln();
foreach(file; imports) {
output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, file.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h"));
output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, file));
}
if (imports.length > 0) output.writeln();
@ -569,6 +612,7 @@ string applyCasePolicy(string source, CasePolicy input, CasePolicy output) {
class MetaTypeInfo {
public:
string originalName = "";
string name = "";
string typeName = "";
string description = "";

View File

@ -0,0 +1,6 @@
#include <JellyfinQt/support/loader.h>
Loader::Loader()
{
}