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

REST enhancement #1.2: Implementation of @queryParam & @bodyParam #969

Merged
merged 5 commits into from
Feb 21, 2015
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
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