Skip to content

Commit

Permalink
Support response parsing code, optional parameters
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
KitsuneRal committed Oct 10, 2017
1 parent bf896f5 commit ccfe0d8
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 251 deletions.
107 changes: 50 additions & 57 deletions analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>& substitutions)
{
cout << "Loading from " << baseDir + fileName << endl;
Expand Down Expand Up @@ -166,92 +187,64 @@ Model Analyzer::loadModel(const pair_vector_t<string>& substitutions)
const string verb = yaml_call_pair.first.as<string>();
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<string>();
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;

for (const YamlMap yamlParam:
yamlCall["parameters"].asSequence())
{
auto name = yamlParam.get("name").as<string>();
const auto in = yamlParam.get("in").as<string>();
auto&& name = yamlParam.get("name").as<string>();
auto&& in = yamlParam.get("in").as<string>();
auto required =
in == "path" || yamlParam["required"].as<bool>(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);
}
}
Expand Down
2 changes: 2 additions & 0 deletions analyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
191 changes: 17 additions & 174 deletions model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
#include "exception.h"

enum {
CannotResolveClassName = InternalErrors,
ConflictingOverloads, UnknownInValue, UnbalancedBracesInPath
_Base = InternalErrors, UnknownParamBlock, UnbalancedBracesInPath
};

using namespace std;
Expand Down Expand Up @@ -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<string> splitPath(const string& path)
Expand All @@ -264,21 +114,15 @@ vector<string> 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
Expand All @@ -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)
Expand Down

0 comments on commit ccfe0d8

Please sign in to comment.