From ccfe0d83085e5db57d3ba956943515fd3b6ddc62 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 10 Oct 2017 13:41:01 +0900 Subject: [PATCH] Support response parsing code, optional parameters This closes #10, addressing the 3 GTAD-related bullets. libqmatrixclient commit will follow separately in its repo. As a result, CallClass becomes just a thin layer (but probably will be loaded with different functions later). In theory, default values for parameters should also work now but I did not check. --- analyzer.cpp | 107 ++++++++++++++--------------- analyzer.h | 2 + model.cpp | 191 +++++---------------------------------------------- model.h | 34 ++++----- printer.cpp | 22 ++++++ 5 files changed, 105 insertions(+), 251 deletions(-) diff --git a/analyzer.cpp b/analyzer.cpp index b60f0f5..4ac15c5 100644 --- a/analyzer.cpp +++ b/analyzer.cpp @@ -137,6 +137,27 @@ ObjectSchema Analyzer::analyzeSchema(const YamlMap& yamlSchema) return s; } +void Analyzer::addParamsFromSchema(VarDecls& varList, + std::string name, bool required, ObjectSchema paramSchema) +{ + if (paramSchema.parentTypes.empty()) + { + for (const auto & param: paramSchema.fields) + model.addVarDecl(varList, param); + } else if (paramSchema.trivial()) + { + // Bare reference to another file where the type is defined. + model.addVarDecl(varList, + VarDecl(paramSchema.parentTypes.front(), move(name), required)); + } else + { + const auto typeName = camelCase(name); + model.types.emplace(typeName, move(paramSchema)); + model.addVarDecl(varList, + VarDecl(TypeUsage(typeName), move(name), required)); + } +} + Model Analyzer::loadModel(const pair_vector_t& substitutions) { cout << "Loading from " << baseDir + fileName << endl; @@ -166,35 +187,12 @@ Model Analyzer::loadModel(const pair_vector_t& substitutions) const string verb = yaml_call_pair.first.as(); const YamlMap yamlCall { yaml_call_pair.second }; - const auto yamlResponses = yamlCall.get("responses").asMap(); - if (auto normalResponse = yamlResponses["200"].asMap()) - { - if (auto respSchema = normalResponse["schema"].asMap()) - { - auto schema = analyzeSchema(respSchema); - if (!schema.fields.empty()) - { - cerr << "Not implemented: skipping " << path << " - " << verb - << ": non-trivial '200' response" << endl; - continue; - } - } - } - else - { - cerr << "Not implemented: skipping " << path << " - " << verb - << ": no '200' response" << endl; - continue; - } - auto operationId = yamlCall.get("operationId").as(); bool needsToken = false; if (const auto security = yamlCall["security"].asSequence()) needsToken = security[0]["accessToken"].IsDefined(); - // TODO: Pass the response type - Call& call = model.addCall(path, verb, operationId, - needsToken, {}); + Call& call = model.addCall(path, verb, operationId, needsToken); cout << "Loading " << operationId << ": " << path << " - " << verb << endl; @@ -202,56 +200,51 @@ Model Analyzer::loadModel(const pair_vector_t& substitutions) for (const YamlMap yamlParam: yamlCall["parameters"].asSequence()) { - auto name = yamlParam.get("name").as(); - const auto in = yamlParam.get("in").as(); + auto&& name = yamlParam.get("name").as(); + auto&& in = yamlParam.get("in").as(); auto required = in == "path" || yamlParam["required"].as(false); if (in != "body") { - model.addCallParam(call, - VarDecl(analyzeType(yamlParam, In), name, required), - in); + model.addVarDecl(call.getParamsBlock(in), + VarDecl(analyzeType(yamlParam, In), name, required)); continue; } - auto bodyParamSchema = analyzeSchema(yamlParam.get("schema")); - if (bodyParamSchema.parentTypes.empty()) + auto&& bodySchema = analyzeSchema(yamlParam.get("schema")); + if (bodySchema.empty()) { - // Special case: an empty schema for a body - // parameter means a freeform object - if (bodyParamSchema.fields.empty()) - model.addCallParam(call, - VarDecl(translator.mapType("object"), name, - false)); - else - for (const auto & param: bodyParamSchema.fields) - model.addCallParam(call, param); - } else if (bodyParamSchema.parentTypes.size() == 1 && - bodyParamSchema.fields.empty()) - { - // Bare reference to another file where the type is - // defined. - model.addCallParam(call, - VarDecl(bodyParamSchema.parentTypes.front(), - name, required)); - } else + // Special case: an empty schema for a body parameter + // means a freeform object. + model.addVarDecl(call.bodyParams, + VarDecl(translator.mapType("object"), name, false)); + } + else + addParamsFromSchema(call.getParamsBlock("body"), + name, required, bodySchema); + } + const auto yamlResponses = yamlCall.get("responses").asMap(); + if (const auto yamlResponse = yamlResponses["200"].asMap()) + { + Response response { "200", {} }; + if (auto yamlSchema = yamlResponse["schema"]) { - const auto typeName = camelCase(name); - model.types.emplace(typeName, - std::move(bodyParamSchema)); - model.addCallParam(call, - VarDecl(TypeUsage(typeName), name, required)); + auto&& responseSchema = analyzeSchema(yamlSchema); + if (!responseSchema.empty()) + addParamsFromSchema(response.properties, + "result", true, responseSchema); } + call.responses.emplace_back(move(response)); } } } } else { model.types.emplace(camelCase(model.filename), analyzeSchema(yaml)); - for (auto type: model.types) + for (const auto& type: model.types) { - for (auto parentType: type.second.parentTypes) + for (const auto& parentType: type.second.parentTypes) model.addImports(parentType); - for (auto field: type.second.fields) + for (const auto& field: type.second.fields) model.addImports(field.type); } } diff --git a/analyzer.h b/analyzer.h index 1295dcc..efae524 100644 --- a/analyzer.h +++ b/analyzer.h @@ -46,4 +46,6 @@ class Analyzer ObjectSchema analyzeSchema(const YamlMap& yamlSchema); TypeUsage tryResolveParentTypes(const YamlMap& yamlSchema); + void addParamsFromSchema(VarDecls& varList, std::string name, + bool required, ObjectSchema bodyParamSchema); }; diff --git a/model.cpp b/model.cpp index 97ebe29..52dc42e 100644 --- a/model.cpp +++ b/model.cpp @@ -7,8 +7,7 @@ #include "exception.h" enum { - CannotResolveClassName = InternalErrors, - ConflictingOverloads, UnknownInValue, UnbalancedBracesInPath + _Base = InternalErrors, UnknownParamBlock, UnbalancedBracesInPath }; using namespace std; @@ -78,166 +77,17 @@ string dropSuffix(string path, const string& suffix) string(path.begin(), path.end() - suffix.size()) : std::move(path); } -regex makeRegex(const string& pattern) +Call& Model::addCall(std::string path, std::string verb, std::string operationId, + bool needsToken) { - // Prepare a regex using regexes. - static const regex braces_re("{}", regex::basic); - static const regex pound_re("#(\\?)?"); // # with optional ? (non-greediness) - return regex( - regex_replace( - // Escape and expand double-brace to {\w+} ({value} etc.) - regex_replace(pattern, braces_re, string("\\{\\w+\\}")), - // Then replace # with word-matching sub-expr; if it's #? then - // insert ? so that we have a non-greedy matching - pound_re, string("(\\w+$1)")) - ); -} - -string makeClassName(const string& path, const string& verb) -{ - using namespace std; - - // Special cases - if (path == "/account/password") - return "ChangePassword"; - if (path == "/account/deactivate") - return "DeactivateAccount"; - if (path == "/pushers/set") - return "PostPusher"; - if (path == "/sync") - return "Sync"; - if (path == "/publicRooms" && verb == "post") - return "SearchPublicRooms"; - if (path.find("/initialSync") != string::npos) - { - cerr << "Warning: initialSync endpoints are deprecated" << endl; - return "InitialSync"; - } - if (regex_match(path, makeRegex("/join/{}"))) - return "JoinByAlias"; - if (regex_match(path, makeRegex("/download/{}/{}(/{})?"))) - return "Download"; - if (regex_match(path, makeRegex("/sendToDevice/{}/{}"))) - return "SendToDevice"; - if (regex_match(path, makeRegex("/admin/whois/{}"))) - return "WhoIs"; - if (regex_match(path, makeRegex("/presence/{}/status"))) - return "SetPresence"; - if (regex_match(path, makeRegex("/rooms/{}/receipt/{}/{}"))) - return "PostReceipt"; - if (regex_search(path, regex("/invite$"))) // /rooms/{id}/invite - return "InviteUser"; - - std::smatch m; - - // /smth1[/smth2]/email/requestToken -> RequestTokenToSmth - // /account/3pid/email/requestToken -> RequestTokenToAccount3pid - // /register/email/requestToken -> RequestTokenToRegister - // /account/password/email/requestToken -> RequestTokenToAccountPassword - if (regex_match(path, m, makeRegex("/(#(?:/#)?)/email/requestToken"))) - return "RequestTokenTo" + camelCase(m[1]); - - // /login/cas/smth -> VerbCasSmth (GetCasTicket|Redirect) (should it be in the API at all?) - if (regex_search(path, m, makeRegex("^/login/cas/#"))) - return "GetCas" + capitalizedCopy(m[1]); - - // [...]/smth1/{}/{txnId} -> Smth1Event - // /rooms/{id}/redact/{}/{txnId} -> RedactEvent - // /rooms/{id}/send/{}/{txnId} -> SendEvent - if (regex_search(path, m, makeRegex("/#/{}/\\{txnId\\}"))) - return capitalizedCopy(m[1]) + "Event"; - - // The following conversions will use altered verbs - string adjustedVerb = capitalizedCopy(verb); - - // /smth1/smth2[/{}] -> VerbSmth1Smth2 - // /presence/list/{userId}, get|post -> Get|ModifyPresenceList - // /account/3pid|password, get|post -> Get|PostAccount3pid|Password - if (regex_search(path, m, makeRegex("^/#/#(?:/{})?"))) - { - if (m[1] == "presence" && verb == "post") - return "ModifyPresenceList"; - return adjustedVerb + capitalizedCopy(m[1]) + capitalizedCopy(m[2]); - } - - if (adjustedVerb == "Put") - adjustedVerb = "Set"; - - // /user/{val1}/[/smth1.5/{val1.5}]/smth2[s] -> VerbSmth1Smth2s - // /user/{val1}/[/smth1.5/{val1.5}]/smth2[s]/{val2} -> VerbSmth1Smth2 - // /user/{id}/rooms/{id}/tags -> GetUserTags - // /user/{id}/rooms/{id}/account_data/{} -> SetUserAccountData - // /user/{id}/account_data/{} -> SetUserAccountData (overload) - // /user/{id}/rooms/{id}/tags/{tag} -> Set|DeleteUserTag - // /user/{id}/filter/{id} -> GetUserFilter - // /user/{id}/filter -> PostUserFilter - if (regex_match(path, m, makeRegex("/user/{}(?:/#/{})?/#?(s?/{})?"))) - return adjustedVerb + "User" + camelCase(m[2]); - - if (adjustedVerb == "Post") - adjustedVerb.clear(); - - if (regex_match(path, makeRegex("/room/{}"))) - { - if (verb == "get") - return "GetRoomIdByAlias"; - return adjustedVerb + "RoomAlias"; - } - // /rooms/{id}/join, post; |messages|state, get -> Join, GetMessages, GetState - if (regex_match(path, m, makeRegex("/rooms/{}/#"))) - return adjustedVerb + capitalizedCopy(m[1]) + "Room"; - - // /smth[s/[{}]] - note non-greedy matching before the smth's "s" - // all -> VerbSmth: - // /upload, /createRoom, /register -> Upload, CreateRoom, Register - // /devices, /publicRooms -> GetDevices, GetPublicRooms - // /devices/{deviceId} -> Get|Set|DeleteDevice - if (regex_match(path, m, makeRegex("/#?(s?/{})?"))) - { - if (m[1] == "device" && verb == "set") - return "UpdateDevice"; - return adjustedVerb + capitalizedCopy(m[1]); - } - - // /smth1s/{}/{}/[{}[/smth2]] -> VerbSmth1[Smth2] - // /thumbnail/{}/{} - // /pushrules/{}/{}/{id}[/...] -> Get|Set|DeletePushrule|... - if (regex_match(path, m, makeRegex("/#?s?/{}/{}(?:/{}(?:/#)?)?"))) - return adjustedVerb + capitalizedCopy(m[1]) + capitalizedCopy(m[2]); - - // /smth1/{val1}/smth2[/{val2}[/{}]] -> VerbSmth2 - // VerbSmth2 - // /rooms/{id}/invite|join, post; |messages|state, get -> Invite, Join, GetMessages, GetState - // /rooms/{id}/smth/{} -> Get|SetSmth - // /profile/{}/display_name|avatar_url -> Get|SetDisplayName - if (regex_match(path, m, makeRegex("/#/{}/#(/{}){0,2}"))) - return adjustedVerb + camelCase(m[2]); - - cerr << "Couldn't create a class name for path " << path << ", verb: " << verb; - fail(CannotResolveClassName); -} - -Call& Model::addCall(string path, string verb, string operationId, bool needsToken, - ResponseType responseType) -{ -// string className = makeClassName(path, verb); -// if (className != capitalizedCopy(operationId)) -// cout << "Warning: className/operationId mismatch: " -// << className << " != " << capitalizedCopy(operationId) << endl; - if (callClasses.empty() || operationId != callClasses.back().operationId) - { -// if (!callClasses.empty() && -// callClasses.back().responseType.name != responseTypename) -// fail(ConflictingOverloads, "Call overloads return different types"); - - callClasses.emplace_back(operationId, std::move(responseType)); - } - transform(verb.begin(), verb.end(), verb.begin(), [] (char c) { return toupper(c, locale::classic()); }); - return callClasses.back() - .addCall(std::move(path), std::move(verb), - std::move(operationId), needsToken); + + callClasses.emplace_back(); + auto& cc = callClasses.back(); + cc.callOverloads.emplace_back(std::move(path), std::move(verb), + std::move(operationId), needsToken); + return cc.callOverloads.back(); } vector splitPath(const string& path) @@ -264,21 +114,15 @@ vector splitPath(const string& path) return parts; } -void Call::addParam(const VarDecl& param, const string& in) +Call::params_type& Call::getParamsBlock(const string& name) { static const char* const map[] { "path", "query", "header", "body" }; for (params_type::size_type i = 0; i < 4; ++i) - if (map[i] == in) - { - allParams[i].push_back(param); - cout << "Added input parameter in " << in << ": " - << param.toString(true) << endl; - return; - } + if (map[i] == name) + return allParams[i]; - cerr << "Parameter " << param.toString() - << " has unknown 'in' value: "<< in << endl; - fail(UnknownInValue); + cerr << "Unknown params block value: "<< name << endl; + fail(UnknownParamBlock); } Call::params_type Call::collateParams() const @@ -292,11 +136,10 @@ Call::params_type Call::collateParams() const return allCollated; } -void Model::addCallParam(Call& call, const VarDecl& param, const string& in) +void Model::addVarDecl(VarDecls& varList, VarDecl var) { - call.addParam(param, in); - - addImports(param.type); + addImports(var.type); + varList.emplace_back(move(var)); } void Model::addImports(const TypeUsage& type) diff --git a/model.h b/model.h index 8bef695..005a55d 100644 --- a/model.h +++ b/model.h @@ -81,16 +81,23 @@ struct ObjectSchema std::vector parentTypes; std::vector fields; - bool isTrivial() const { return parentTypes.size() == 1 && fields.empty(); } + bool empty() const { return parentTypes.empty() && fields.empty(); } + bool trivial() const { return parentTypes.size() == 1 && fields.empty(); } }; -using ResponseType = ObjectSchema; +using VarDecls = std::vector; + +struct Response +{ + std::string code; + VarDecls properties; +}; std::vector splitPath(const std::string& path); struct Call { - using params_type = std::vector; + using params_type = VarDecls; Call(std::string callPath, std::string callVerb, std::string callName, bool callNeedsToken) @@ -107,7 +114,7 @@ struct Call { } Call operator=(Call&&) = delete; - void addParam(const VarDecl& param, const std::string& in); + Call::params_type& getParamsBlock(const std::string& name); params_type collateParams() const; std::string path; @@ -122,24 +129,12 @@ struct Call // FIXME: This is Matrix-specific, should be replaced with proper // securityDefinitions representation. bool needsToken; + std::vector responses; }; struct CallClass { - std::string operationId; std::vector callOverloads; - ResponseType responseType; - - CallClass(std::string operationId, ResponseType responseType) - : operationId(std::move(operationId)) - , responseType(std::move(responseType)) - { } - Call& addCall(std::string path, std::string verb, std::string name, - bool needsToken) - { - callOverloads.emplace_back(path, verb, name, needsToken); - return callOverloads.back(); - } }; struct Model @@ -164,9 +159,8 @@ struct Model Model(Model&&) = default; Model& operator=(Model&&) = delete; Call& addCall(std::string path, std::string verb, std::string operationId, - bool needsToken, ResponseType responseType); - void addCallParam(Call& call, const VarDecl& param, - const std::string& in = "body"); + bool needsToken); + void addVarDecl(VarDecls& varList, VarDecl var); void addImports(const TypeUsage& type); }; diff --git a/printer.cpp b/printer.cpp index ddc2f6e..a9c7776 100644 --- a/printer.cpp +++ b/printer.cpp @@ -221,6 +221,28 @@ vector Printer::print(const Model& model) const } setList(&mClass, pp.first, move(mParams)); } + { + list mResponses; + for (const auto& response: call.responses) + { + object mResponse { { "code", response.code } + , { "normalResponse?", + response.code == "200" } + }; + list mProperties; + for (const auto& p: response.properties) + { + object mProperty { { "dataType", renderType(p.type) } + , { "paramName", p.name } + }; + dumpFieldAttrs(p, mProperty); + mProperties.emplace_back(move(mProperty)); + } + setList(&mResponse, "properties", move(mProperties)); + mResponses.emplace_back(move(mResponse)); + } + setList(&mClass, "responses", move(mResponses)); + } mClasses.emplace_back(move(mClass)); } }