Skip to content

Commit

Permalink
Merge pull request #969 from Geod24/queryParam
Browse files Browse the repository at this point in the history
REST enhancement #1.2: Implementation of @QueryParam & @bodyParam
  • Loading branch information
s-ludwig committed Feb 21, 2015
2 parents 5810257 + 9705fd4 commit d04b295
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 62 deletions.
77 changes: 74 additions & 3 deletions examples/rest/source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -336,25 +336,53 @@ unittest
* - body for POST requests;
*
* This is configurable by means of:
* - headerParam : Get a parameter from the query header;
* - @headerParam : Get a parameter from the query header;
* - @queryParam : Get a parameter from the query URL;
* - @bodyParam : Get a parameter from the body;
*/
@rootPathFromName
interface Example6API
{
// The first parameter of HeaderParam is the identifier (must match one of the parameter name).
// The first parameter of @headerParam is the identifier (must match one of the parameter name).
// The second is the name of the field in the header, such as "Accept", "Content-Type", "User-Agent"...
@headerParam("auth", "Authorization")
string getResponse(string auth);
// As with @headerParam, the first parameter of @queryParam is the identifier.
// The second being the field name, e.g for a query such as: 'GET /root/node?foo=bar', "foo" will be the second parameter.
@queryParam("fortyTwo", "qparam")
string postAnswer(string fortyTwo);
// Finally, there is @bodyParam. It works as you expect it to work,
// currently serializing passed data as Json and pass them through the body.
@bodyParam("myFoo", "parameter")
string getConcat(FooType myFoo);

struct FooType {
int a;
string s;
double d;
}
}

class Example6 : Example6API
{
override string getResponse(string auth) {
override:
string getResponse(string auth)
{
// If the user provided credentials Aladdin / 'open sesame'
if (auth == "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
return "The response is 42";
return "The cake is a lie";
}
string postAnswer(string fortyTwo)
{
if (fortyTwo == "Life_universe_and_the_rest")
return "True";
return "False";
}
string getConcat(FooType myFoo)
{
return to!string(myFoo.a)~myFoo.s~to!string(myFoo.d);
}
}

unittest
Expand All @@ -364,6 +392,8 @@ unittest
auto routes = router.getAllRoutes();

assert (routes[0].method == HTTPMethod.GET && routes[0].pattern == "/example6_api/response");
assert (routes[1].method == HTTPMethod.POST && routes[1].pattern == "/example6_api/answer");
assert (routes[0].method == HTTPMethod.GET && routes[2].pattern == "/example6_api/concat");
}


Expand Down Expand Up @@ -457,6 +487,47 @@ shared static this()
assert(answer == "The cake is a lie");
}

// Example 6 -- Query
{
import vibe.http.client : requestHTTP;
import vibe.stream.operations : readAllUTF8;

// First we make sure parameters are transmitted via query.
auto res = requestHTTP("http://127.0.0.1:8080/example6_api/answer?qparam=Life_universe_and_the_rest",
(scope r) { r.method = HTTPMethod.POST; });
assert(res.statusCode == 200);
assert(res.bodyReader.readAllUTF8() == `"True"`);
// Then we check that both can communicate together.
auto api = new RestInterfaceClient!Example6API("http://127.0.0.1:8080");
auto answer = api.postAnswer("IDK");
assert(answer == "False");
}

// Example 6 -- Body
{
import vibe.http.client : requestHTTP;
import vibe.stream.operations : readAllUTF8;

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.GET;
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.getConcat(Example6API.FooType(42, "fortySomething", 51.42));
assert(answer == expected);
}

logInfo("Success.");
});
}
95 changes: 45 additions & 50 deletions source/vibe/web/common.d
Original file line number Diff line number Diff line change
Expand Up @@ -470,30 +470,27 @@ package struct WebParamAttribute {
string field;
}

version (none) {
// It's not yet implemented in the REST and web interface.
/*
* 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.
*
* Params:
* - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch.
* - field: The name of the field in the JSON object.
*
* ----
* @bodyParam("pack", "package")
* void ship(int pack);
* // The server will receive the following body for a call to ship(42):
* // { "package": 42 }
* ----
*/
private WebParamAttribute bodyParam(string identifier, string field) {
if (!__ctfe)
assert(false, onlyAsUda!__FUNCTION__);
return WebParamAttribute(WebParamAttribute.Origin.Body, identifier, 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.
*
* Params:
* - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch.
* - field: The name of the field in the JSON object.
*
* ----
* @bodyParam("pack", "package")
* void ship(int pack);
* // The server will receive the following body for a call to ship(42):
* // { "package": 42 }
* ----
*/
WebParamAttribute bodyParam(string identifier, string field) {
if (!__ctfe)
assert(false, onlyAsUda!__FUNCTION__);
return WebParamAttribute(WebParamAttribute.Origin.Body, identifier, field);
}

/**
Expand All @@ -513,37 +510,35 @@ version (none) {
* void login(string auth);
* ----
*/
WebParamAttribute headerParam(string identifier, string field) {
WebParamAttribute headerParam(string identifier, string field)
{
if (!__ctfe)
assert(false, onlyAsUda!__FUNCTION__);
return WebParamAttribute(WebParamAttribute.Origin.Header, identifier, field);
}

version (none) {
// It's not yet implemented in the REST and web interface.
/*
* Declare that a parameter will be transmitted to the API through the query string.
*
* It will be serialized as part of a JSON object, and will go through URL serialization.
* The serialization format is not customizable.
*
* Params:
* - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch.
* - field: The field name to use.
*
* ----
* // For a call to postData("D is awesome"), the server will receive the query:
* // POST /data?test=%22D is awesome%22
* @queryParam("data", "test")
* void postData(string data);
* ----
*/
private WebParamAttribute queryParam(string identifier, string field) {

if (!__ctfe)
assert(false, onlyAsUda!__FUNCTION__);
return WebParamAttribute(WebParamAttribute.Origin.Query, identifier, field);
}
/**
* Declare that a parameter will be transmitted to the API through the query string.
*
* It will be serialized as part of a JSON object, and will go through URL serialization.
* The serialization format is not customizable.
*
* Params:
* - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch.
* - field: The field name to use.
*
* ----
* // For a call to postData("D is awesome"), the server will receive the query:
* // POST /data?test=%22D is awesome%22
* @queryParam("data", "test")
* void postData(string data);
* ----
*/
WebParamAttribute queryParam(string identifier, string field)
{
if (!__ctfe)
assert(false, onlyAsUda!__FUNCTION__);
return WebParamAttribute(WebParamAttribute.Origin.Query, identifier, field);
}

/**
Expand Down
60 changes: 51 additions & 9 deletions source/vibe/web/rest.d
Original file line number Diff line number Diff line change
Expand Up @@ -631,9 +631,48 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func
logDebug("Header param: %s <- %s", paramsArgList[0].identifier, *fld);
params[i] = fromRestString!P(*fld);
} else static if (paramsArgList[0].origin == WebParamAttribute.Origin.Query) {
static assert (0, "@QueryParam is not yet supported");
// Note: Doesn't work if HTTPServerOption.parseQueryString is disabled.
static if (is (ParamDefaults[i] == void)) {
auto fld = enforceBadRequest(paramsArgList[0].field in req.query,
format("Expected form field '%s' in query", paramsArgList[0].field));
} else {
auto fld = paramsArgList[0].field in req.query;
if (fld is null) {
params[i] = ParamDefaults[i];
logDebug("No query param %s, using default value", paramsArgList[0].identifier);
continue;
}
}
logDebug("Query param: %s <- %s", paramsArgList[0].identifier, *fld);
params[i] = fromRestString!P(*fld);
} else static if (paramsArgList[0].origin == WebParamAttribute.Origin.Body) {
static assert (0, "@BodyParam is not yet supported");
enforceBadRequest(
req.contentType == "application/json",
"The Content-Type header needs to be set to application/json."
);
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."
);

static if (is(ParamDefaults[i] == void)) {
auto par = req.json[paramsArgList[0].field];
enforceBadRequest(par.type != Json.Type.Undefined,
format("Missing parameter %s", paramsArgList[0].field)
);
logDebug("Body param: %s <- %s", paramsArgList[0].identifier, par);
params[i] = deserializeJson!P(par);
} else {
if (req.json[paramsArgList[0].field].type == Json.Type.Undefined) {
logDebug("No body param %s, using default value", paramsArgList[0].identifier);
params[i] = ParamDefaults[i];
continue;
}
}
} else static assert (false, "Internal error: Origin "~to!string(paramsArgList[0].origin)~" is not implemented.");
} else static if (ParamNames[i].startsWith("_")) {
// URL parameter
Expand Down Expand Up @@ -683,18 +722,17 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func
);

static if (is(DefVal == void)) {
enforceBadRequest(
req.json[pname].type != Json.Type.Undefined,
format("Missing parameter %s", pname)
);
auto par = req.json[pname];
enforceBadRequest(par.type != Json.Type.Undefined,
format("Missing parameter %s", pname)
);
params[i] = deserializeJson!P(par);
} else {
if (req.json[pname].type == Json.Type.Undefined) {
params[i] = DefVal;
continue;
}
}

params[i] = deserializeJson!P(req.json[pname]);
}
}
}
Expand Down Expand Up @@ -966,8 +1004,12 @@ private string genClientBody(alias Func)() {
static assert (paramsArgList.length == 1, "Multiple attribute for parameter '"~ParamNames[i]~"' in "~FuncId);
static if (paramsArgList[0].origin == WebParamAttribute.Origin.Header)
param_handling_str ~= format(q{headers__["%s"] = to!string(%s);}, paramsArgList[0].field, paramsArgList[0].identifier);
else static if (paramsArgList[0].origin == WebParamAttribute.Origin.Query)
queryParamCTMap[paramsArgList[0].field] = paramsArgList[0].identifier;
else static if (paramsArgList[0].origin == WebParamAttribute.Origin.Body)
bodyParamCTMap[paramsArgList[0].field] = paramsArgList[0].identifier;
else
static assert (0, "Only header parameter are currently supported client-side");
static assert (0, "Internal error: Unknown WebParamAttribute.Origin in REST client code generation.");
} else static if (!ParamNames[i].startsWith("_")
&& !IsAttributedParameter!(Func, ParamNames[i])) {
// underscore parameters are sourced from the HTTPServerRequest.params map or from url itself
Expand Down

0 comments on commit d04b295

Please sign in to comment.