Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement whole-body parameters for the REST interface generator #1723

Merged
merged 4 commits into from
Mar 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/rest/source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ unittest
* This is to be consistent with the way D 'out' and 'ref' works.
* However, it makes no sense to have 'ref' or 'out' parameters on
* body or query parameter, so those are treated as error at compile time.
*
* If no Json fieldname is passed to @bodyParam, the entire Json body is deserialized into the respective field.
*/
@rootPathFromName
interface Example6API
Expand All @@ -374,6 +376,13 @@ interface Example6API
@bodyParam("myFoo", "parameter")
string postConcat(FooType myFoo);

// If no field name is passed to @bodyParam the entire json object is
// serialized into the parameter.
// Moreover if only one bodyParameter is present, this is the default
// behavior.
@bodyParam("obj")
string postConcatBody(FooType obj);

struct FooType {
int a;
string s;
Expand Down Expand Up @@ -414,6 +423,11 @@ override:
import std.conv : to;
return to!string(myFoo.a)~myFoo.s~to!string(myFoo.d);
}

string postConcatBody(FooType obj)
{
return postConcat(obj);
}
}

unittest
Expand Down Expand Up @@ -558,6 +572,11 @@ shared static this()
auto api = new RestInterfaceClient!Example6API("http://127.0.0.1:8080");
auto answer = api.postAnswer("IDK");
assert(answer == "False");

Example6API.FooType fType = {a: 1, s: "str", d: 3.14};
auto expected = "1str3.14";
assert(api.postConcat(fType) == expected);
assert(api.postConcatBody(fType) == expected);
}

// Example 7 -- Custom JSON response
Expand Down
57 changes: 43 additions & 14 deletions tests/rest/source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ interface Example6API
@bodyParam("myFoo", "parameter")
string postConcat(FooType myFoo);

// expects the entire body
@bodyParam("obj")
string postConcatBody(FooType obj);

struct FooType {
int a;
string s;
Expand Down Expand Up @@ -413,6 +417,11 @@ override:
import std.conv : to;
return to!string(myFoo.a)~myFoo.s~to!string(myFoo.d);
}

string postConcatBody(FooType obj)
{
return postConcat(obj);
}
}

unittest
Expand Down Expand Up @@ -544,21 +553,41 @@ void runTests()
enum expected = "42fortySomething51.42"; // to!string(51.42) doesn't work at CT

auto api = new RestInterfaceClient!Example6API("http://127.0.0.1:8080");
// First we make sure parameters are transmitted via query.
auto res = requestHTTP("http://127.0.0.1:8080/example6_api/concat",
(scope r) {
import vibe.data.json;
r.method = HTTPMethod.POST;
Json obj = Json.emptyObject;
obj["parameter"] = serializeToJson(Example6API.FooType(42, "fortySomething", 51.42));
r.writeJsonBody(obj);
});
{
// First we make sure parameters are transmitted via query.
auto res = requestHTTP("http://127.0.0.1:8080/example6_api/concat",
(scope r) {
import vibe.data.json;
r.method = HTTPMethod.POST;
Json obj = Json.emptyObject;
obj["parameter"] = serializeToJson(Example6API.FooType(42, "fortySomething", 51.42));
r.writeJsonBody(obj);
});

assert(res.statusCode == 200);
assert(res.bodyReader.readAllUTF8() == `"`~expected~`"`);
// Then we check that both can communicate together.
auto answer = api.postConcat(Example6API.FooType(42, "fortySomething", 51.42));
assert(answer == expected);
}

assert(res.statusCode == 200);
assert(res.bodyReader.readAllUTF8() == `"`~expected~`"`);
// Then we check that both can communicate together.
auto answer = api.postConcat(Example6API.FooType(42, "fortySomething", 51.42));
assert(answer == expected);
// suppling the whole body
{
// First we make sure parameters are transmitted via query.
auto res = requestHTTP("http://127.0.0.1:8080/example6_api/concat_body",
(scope r) {
import vibe.data.json;
r.method = HTTPMethod.POST;
Json obj = serializeToJson(Example6API.FooType(42, "fortySomething", 51.42));
r.writeJsonBody(obj);
});

assert(res.statusCode == 200);
assert(res.bodyReader.readAllUTF8() == `"`~expected~`"`);
// Then we check that both can communicate together.
auto answer = api.postConcatBody(Example6API.FooType(42, "fortySomething", 51.42));
assert(answer == expected);
}
}
}

Expand Down
19 changes: 17 additions & 2 deletions web/vibe/web/common.d
Original file line number Diff line number Diff line change
Expand Up @@ -463,11 +463,13 @@ package struct WebParamAttribute {
string field;
}


/**
* Declare that a parameter will be transmitted to the API through the body.
*
* It will be serialized as part of a JSON object.
* The serialization format is currently not customizable.
* If no fieldname is given, the entire body is serialized into the object.
*
* Params:
* - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch.
Expand All @@ -480,14 +482,27 @@ package struct WebParamAttribute {
* // { "package": 42 }
* ----
*/
WebParamAttribute bodyParam(string identifier, string field)
@safe {
WebParamAttribute bodyParam(string identifier, string field) @safe
in {
assert(field.length > 0, "fieldname can't be empty.");
}
body
{
import vibe.web.internal.rest.common : ParameterKind;
if (!__ctfe)
assert(false, onlyAsUda!__FUNCTION__);
return WebParamAttribute(ParameterKind.body_, identifier, field);
}

/// ditto
WebParamAttribute bodyParam(string identifier)
@safe {
import vibe.web.internal.rest.common : ParameterKind;
if (!__ctfe)
assert(false, onlyAsUda!__FUNCTION__);
return WebParamAttribute(ParameterKind.body_, identifier, "");
}

/**
* Declare that a parameter will be transmitted to the API through the headers.
*
Expand Down
23 changes: 22 additions & 1 deletion web/vibe/web/internal/rest/common.d
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ import std.meta : anySatisfy, Filter;
final switch (pi.kind) {
case ParameterKind.query: route.queryParameters ~= pi; break;
case ParameterKind.body_: route.bodyParameters ~= pi; break;
case ParameterKind.wholeBody: route.wholeBodyParameter = pi; break;
case ParameterKind.header: route.headerParameters ~= pi; break;
case ParameterKind.internal: route.internalParameters ~= pi; break;
case ParameterKind.attributed: route.attributedParameters ~= pi; break;
Expand Down Expand Up @@ -247,6 +248,7 @@ import std.meta : anySatisfy, Filter;
{
static import std.traits;
import vibe.web.auth : AuthInfo;
import std.algorithm.searching : any, count;

assert(__ctfe);

Expand Down Expand Up @@ -302,6 +304,8 @@ import std.meta : anySatisfy, Filter;
alias PWPAT = Filter!(mixin(CompareParamName.Name), WPAT);
pi.kind = PWPAT[0].origin;
pi.fieldName = PWPAT[0].field;
if (pi.kind == ParameterKind.body_ && pi.fieldName == "")
pi.kind = ParameterKind.wholeBody;
} else static if (pname.startsWith("_")) {
pi.kind = ParameterKind.internal;
pi.fieldName = parameterNames[i][1 .. $];
Expand All @@ -315,6 +319,11 @@ import std.meta : anySatisfy, Filter;
route.parameters ~= pi;
}

auto nhb = route.parameters.count!(p => p.kind == ParameterKind.wholeBody);
assert(nhb <= 1, "Multiple whole-body parameters defined for "~route.functionName~".");
assert(nhb == 0 || !route.parameters.any!(p => p.kind == ParameterKind.body_),
"Normal body parameters and a whole-body parameter defined at the same time for "~route.functionName~".");

ret[fi] = route;
}

Expand Down Expand Up @@ -417,6 +426,7 @@ struct Route {
PathPart[] pathParts; // path separated into text and placeholder parts
PathPart[] fullPathParts; // full path separated into text and placeholder parts
Parameter[] parameters;
Parameter wholeBodyParameter;
Parameter[] queryParameters;
Parameter[] bodyParameters;
Parameter[] headerParameters;
Expand Down Expand Up @@ -455,7 +465,8 @@ struct StaticParameter {

enum ParameterKind {
query, // req.query[]
body_, // JSON body
body_, // JSON body (single field)
wholeBody, // JSON body
header, // req.header[]
attributed, // @before
internal, // req.params[]
Expand Down Expand Up @@ -698,3 +709,13 @@ unittest { // #1648
}
alias RI = RestInterface!I;
}

unittest {
interface I1 { @bodyParam("foo") void a(int foo); }
alias RI = RestInterface!I1;
interface I2 { @bodyParam("foo") void a(int foo, int bar); }
interface I3 { @bodyParam("foo") @bodyParam("bar") void a(int foo, int bar); }
static assert(__traits(compiles, RestInterface!I1.init));
static assert(!__traits(compiles, RestInterface!I2.init));
static assert(!__traits(compiles, RestInterface!I3.init));
}
4 changes: 3 additions & 1 deletion web/vibe/web/internal/rest/jsclient.d
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ class JSRestClientSettings
}

// body parameters
if (route.bodyParameters.length) {
if (route.wholeBodyParameter.name.length) {
fout.formattedWrite("var bostbody = %s;\n", route.wholeBodyParameter.name);
} else if (route.bodyParameters.length) {
fout.put("var postbody = {\n");
foreach (p; route.bodyParameters)
fout.formattedWrite("%s: %s,\n", Json(p.fieldName), p.name);
Expand Down
16 changes: 10 additions & 6 deletions web/vibe/web/rest.d
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ private HTTPServerRequestDelegate jsonMethodHandler(alias Func, size_t ridx, T)(
enforceBadRequest(req.json.type != Json.Type.undefined,
"The request body does not contain a valid JSON value.");
enforceBadRequest(req.json.type == Json.Type.object,
"The request body must contain a JSON object with an entry for each parameter.");
"The request body must contain a JSON object.");
}

static if (isAuthenticated!(T, Func)) {
Expand All @@ -1050,13 +1050,15 @@ private HTTPServerRequestDelegate jsonMethodHandler(alias Func, size_t ridx, T)(
} else static if (sparam.kind == ParameterKind.query) {
if (auto pv = fieldname in req.query)
v = fromRestString!PT(*pv);
} else static if (sparam.kind == ParameterKind.wholeBody) {
try v = deserializeJson!PT(req.json);
catch (JSONException e) enforceBadRequest(false, e.msg);
} else static if (sparam.kind == ParameterKind.body_) {
if (auto pv = fieldname in req.json) {
try
try {
if (auto pv = fieldname in req.json)
v = deserializeJson!PT(*pv);
catch (JSONException e)
enforceBadRequest(false, e.msg);
}
} catch (JSONException e)
enforceBadRequest(false, e.msg);
} else static if (sparam.kind == ParameterKind.header) {
if (auto pv = fieldname in req.headers)
v = fromRestString!PT(*pv);
Expand Down Expand Up @@ -1333,6 +1335,8 @@ private auto executeClientMethod(I, size_t ridx, ARGS...)
auto fieldname = route.parameters[i].fieldName;
static if (sparam.kind == ParameterKind.query) {
addQueryParam!i(fieldname);
} else static if (sparam.kind == ParameterKind.wholeBody) {
jsonBody = serializeToJson(ARGS[i]);
} else static if (sparam.kind == ParameterKind.body_) {
jsonBody[fieldname] = serializeToJson(ARGS[i]);
} else static if (sparam.kind == ParameterKind.header) {
Expand Down