Skip to content

Commit

Permalink
Implement optional trailing underscore stripping for REST names.
Browse files Browse the repository at this point in the history
This is applied to method and parameter names to enable the use of reserved D words as REST names (similar to vibe.data.serialization).
  • Loading branch information
s-ludwig committed Sep 19, 2014
1 parent 5f1d9fb commit f2ffc4b
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 39 deletions.
47 changes: 30 additions & 17 deletions examples/rest/source/app.d
Expand Up @@ -25,7 +25,7 @@ interface Example1API
{
/* Default convention is based on camelCase
*/

/* Used HTTP method is "GET" because function name start with "get".
* Remaining part is converted to lower case with words separated by _
*
Expand Down Expand Up @@ -73,9 +73,9 @@ unittest
registerRestInterface(router, new Example1());
auto routes = router.getAllRoutes();

assert (routes[HTTPMethod.GET][0].pattern == "/example1_api/some_info");
assert (routes[HTTPMethod.GET][1].pattern == "/example1_api/getter");
assert (routes[HTTPMethod.POST][0].pattern == "/example1_api/sum");
assert (routes[0].method == HTTPMethod.GET && routes[0].pattern == "/example1_api/some_info");
assert (routes[1].method == HTTPMethod.POST && routes[1].pattern == "/example1_api/sum");
assert (routes[2].method == HTTPMethod.GET && routes[2].pattern == "/example1_api/getter");
}

/* --------- EXAMPLE 2 ---------- */
Expand Down Expand Up @@ -130,7 +130,7 @@ unittest
registerRestInterface(router, new Example2(), MethodStyle.upperUnderscored);
auto routes = router.getAllRoutes();

assert (routes[HTTPMethod.GET][0].pattern == "/EXAMPLE2_API/ACCUMULATE_ALL");
assert (routes[0].method == HTTPMethod.GET && routes[0].pattern == "/EXAMPLE2_API/ACCUMULATE_ALL");
}

/* --------- EXAMPLE 3 ---------- */
Expand Down Expand Up @@ -168,11 +168,11 @@ interface Example3APINested
class Example3 : Example3API
{
private:
Example3Nested m_nestedImpl;
Example3Nested m_nestedImpl;

public:
this()
{
{
m_nestedImpl = new Example3Nested();
}

Expand Down Expand Up @@ -203,8 +203,8 @@ unittest
registerRestInterface(router, new Example3());
auto routes = router.getAllRoutes();

assert (routes[HTTPMethod.GET][0].pattern == "/example3_api/nested_module/number");
assert (routes[HTTPMethod.GET][1].pattern == "/example3_api/:id/myid");
assert (routes[0].method == HTTPMethod.GET && routes[0].pattern == "/example3_api/nested_module/number");
assert (routes[1].method == HTTPMethod.GET && routes[1].pattern == "/example3_api/:id/myid");
}


Expand All @@ -228,6 +228,12 @@ interface Example4API
*/
@path(":param/:another_param/data")
int getParametersInURL(string _param, string _another_param);

/* The underscore at the end of each parameter will be dropped in the
* protocol, so that D keywords, such as "body" or "in" can be used as
* identifiers.
*/
int querySpecialParameterNames(int body_, bool in_);
}

class Example4 : Example4API
Expand All @@ -242,6 +248,11 @@ class Example4 : Example4API
import std.conv;
return to!int(_param) + to!int(_another_param);
}

int querySpecialParameterNames(int body_, bool in_)
{
return body_ * (in_ ? -1 : 1);
}
}

unittest
Expand All @@ -250,8 +261,9 @@ unittest
registerRestInterface(router, new Example4());
auto routes = router.getAllRoutes();

assert (routes[HTTPMethod.POST][0].pattern == "/example4_api/simple");
assert (routes[HTTPMethod.GET][0].pattern == "/example4_api/:param/:another_param/data");
assert (routes[0].method == HTTPMethod.POST && routes[0].pattern == "/example4_api/simple");
assert (routes[1].method == HTTPMethod.GET && routes[1].pattern == "/example4_api/:param/:another_param/data");
assert (routes[2].method == HTTPMethod.GET && routes[2].pattern == "/example4_api/special_parameter_names");
}

/* It is possible to attach function hooks to methods via User-Define Attributes.
Expand All @@ -260,9 +272,9 @@ unittest
* 1) accepts HTTPServerRequest and HTTPServerResponse
* 2) is attached to specific parameter of a method
* 3) has same return type as that parameter type
*
*
* REST API framework will call attached functions before actual
* method call and use their result as an input to method call.
* method call and use their result as an input to method call.
*
* There is also another attribute function type that can be called
* to post-process method return value.
Expand Down Expand Up @@ -303,7 +315,7 @@ class Example5 : Example5API

if (!user.authorized)
return "";

return format("secret #%s for %s", num, user.name);
}
}
Expand All @@ -314,7 +326,7 @@ unittest
registerRestInterface(router, new Example5());
auto routes = router.getAllRoutes();

assert (routes[HTTPMethod.GET][0].pattern == "/example5_api/secret");
assert (routes[0].method == HTTPMethod.GET && routes[0].pattern == "/example5_api/secret");
}

shared static this()
Expand All @@ -338,7 +350,7 @@ shared static this()
/* At this moment, server is prepared to process requests.
* After a small delay to let socket become ready, the very same D interfaces
* will be used to define some form of Remote Procedure Calling via HTTP in client code.
*
*
* It greatly simplifies writing client applications and gurantees that server and client API
* will always stay in sync. Care about method style naming convention mismatch though.
*/
Expand All @@ -358,7 +370,7 @@ shared static this()
{
auto api = new RestInterfaceClient!Example2API("http://127.0.0.1:8080", MethodStyle.upperUnderscored);
Example2API.Aggregate[] data = [
{ "one", 1, Example2API.Aggregate.Type.Type1 },
{ "one", 1, Example2API.Aggregate.Type.Type1 },
{ "two", 2, Example2API.Aggregate.Type.Type2 }
];
auto accumulated = api.queryAccumulateAll(data);
Expand All @@ -378,6 +390,7 @@ shared static this()
auto api = new RestInterfaceClient!Example4API("http://127.0.0.1:8080");
api.myNameDoesNotMatter();
assert(api.getParametersInURL("20", "30") == 50);
assert(api.querySpecialParameterNames(10, true) == -10);
}
// Example 5
{
Expand Down
92 changes: 70 additions & 22 deletions source/vibe/web/rest.d
Expand Up @@ -99,6 +99,12 @@ void registerRestInterface(TImpl)(URLRouter router, TImpl instance, RestInterfac
);
}

string strip(string name) {
if (settings.stripTrailingUnderscore && name.endsWith("_"))
return name[0 .. $-1];
else return name;
}

foreach (method; __traits(allMembers, I)) {
foreach (overload; MemberFunctionsTuple!(I, method)) {

Expand All @@ -115,7 +121,7 @@ void registerRestInterface(TImpl)(URLRouter router, TImpl instance, RestInterfac
" in the next release.");
}

string url = adjustMethodStyle(meta.url, settings.methodStyle);
string url = adjustMethodStyle(strip(meta.url), settings.methodStyle);
}

alias RT = ReturnType!overload;
Expand All @@ -133,7 +139,7 @@ void registerRestInterface(TImpl)(URLRouter router, TImpl instance, RestInterfac
);
} else {
// normal handler
auto handler = jsonMethodHandler!(I, method, overload)(instance);
auto handler = jsonMethodHandler!(I, method, overload)(instance, settings);

string[] params = [ ParameterIdentifierTuple!overload ];

Expand Down Expand Up @@ -264,6 +270,7 @@ class RestInterfaceClient(I) : I
URL m_baseURL;
MethodStyle m_methodStyle;
RequestFilter m_requestFilter;
RestInterfaceSettings m_settings;
}

/**
Expand All @@ -273,6 +280,13 @@ class RestInterfaceClient(I) : I
{
import vibe.internal.meta.uda : findFirstUDA;

m_settings = settings.dup;

if (!m_settings.baseURL.path.absolute) {
assert(m_settings.baseURL.path.empty, "Base URL path must be absolute.");
m_settings.baseURL.path = Path("/");
}

URL url = settings.baseURL;
enum uda = findFirstUDA!(RootPathAttribute, I);
static if (uda.found) {
Expand All @@ -286,6 +300,12 @@ class RestInterfaceClient(I) : I
m_baseURL = url;
m_methodStyle = settings.methodStyle;

string strip(string name) {
if (settings.stripTrailingUnderscore && name.endsWith("_"))
return name[0 .. $-1];
else return name;
}

mixin (generateRestInterfaceSubInterfaceInstances!I());
}

Expand Down Expand Up @@ -396,6 +416,13 @@ class RestInterfaceClient(I) : I
return ret;
}
}

private string _stripName(string name)
{
if (m_settings.stripTrailingUnderscore && name.endsWith("_"))
return name[0 .. $-1];
else return name;
}
}

///
Expand Down Expand Up @@ -440,21 +467,34 @@ unittest
Encapsulates settings used to customize the generated REST interface.
*/
class RestInterfaceSettings {
/** The public URL below which the REST interface is registered.
*/
URL baseURL;

/** Naming convention used for the generated URLs.
*/
MethodStyle methodStyle = MethodStyle.lowerUnderscored;

/** Ignores a trailing underscore in method and function names.
With this setting set to $(D true), it's possible to use names in the
REST interface that are reserved words in D.
*/
bool stripTrailingUnderscore = true;

@property RestInterfaceSettings dup()
const {
auto ret = new RestInterfaceSettings;
ret.baseURL = this.baseURL;
ret.methodStyle = this.methodStyle;
ret.stripTrailingUnderscore = this.stripTrailingUnderscore;
return ret;
}
}


/// private
private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func)(T inst)
private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func)(T inst, RestInterfaceSettings settings)
{
import std.traits : ParameterTypeTuple, ReturnType,
ParameterDefaultValueTuple, ParameterIdentifierTuple;
Expand All @@ -476,6 +516,12 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func
{
PT params;

string strip(string name) {
if (settings.stripTrailingUnderscore && name.endsWith("_"))
return name[0 .. $-1];
else return name;
}

foreach (i, P; PT) {
static assert (
ParamNames[i].length,
Expand Down Expand Up @@ -505,24 +551,26 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func
} else {
// normal parameter
alias DefVal = ParamDefaults[i];
auto pname = strip(ParamNames[i]);

if (req.method == HTTPMethod.GET) {
logDebug("query %s of %s" ,ParamNames[i], req.query);
logDebug("query %s of %s", pname, req.query);

static if (is (DefVal == void)) {
enforce(
ParamNames[i] in req.query,
format("Missing query parameter '%s'", ParamNames[i])
pname in req.query,
format("Missing query parameter '%s'", pname)
);
} else {
if (ParamNames[i] !in req.query) {
if (pname !in req.query) {
params[i] = DefVal;
continue;
}
}

params[i] = fromRestString!P(req.query[ParamNames[i]]);
params[i] = fromRestString!P(req.query[pname]);
} else {
logDebug("%s %s", method, ParamNames[i]);
logDebug("%s %s", method, pname);

enforce(
req.contentType == "application/json",
Expand All @@ -539,17 +587,17 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func

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

params[i] = deserializeJson!P(req.json[ParamNames[i]]);
params[i] = deserializeJson!P(req.json[pname]);
}
}
}
Expand Down Expand Up @@ -671,14 +719,12 @@ private string generateRestInterfaceSubInterfaceInstances(I)()

ret ~= format(
q{
if (%s)
m_%s = new %s(m_baseURL.toString() ~ PathEntry("%s").toString() ~ "/", m_methodStyle);
else
m_%s = new %s(m_baseURL.toString() ~ adjustMethodStyle(PathEntry("%s").toString() ~ "/", m_methodStyle), m_methodStyle);
auto settings_%1$s = m_settings.dup;
settings_%1$s.baseURL.path = m_baseURL.path ~
(%3$s ? "%2$s/" : adjustMethodStyle(strip("%2$s"), m_methodStyle) ~ "/");
m_%1$s = new %1$s(settings_%1$s);
},
meta.hadPathUDA,
implname, implname, meta.url,
implname, implname, meta.url
implname, meta.url, meta.hadPathUDA
);
ret ~= "\n";
}
Expand Down Expand Up @@ -803,8 +849,8 @@ private string generateRestInterfaceMethods(I)()
// underscore parameters are sourced from the HTTPServerRequest.params map or from url itself
param_handling_str ~= format(
q{
jparams__["%s"] = serializeToJson(%s);
jparamsj__["%s"] = %s;
jparams__[_stripName("%s")] = serializeToJson(%s);
jparamsj__[_stripName("%s")] = %s;
},
ParamNames[i],
ParamNames[i],
Expand All @@ -820,6 +866,8 @@ private string generateRestInterfaceMethods(I)()
static if (!meta.hadPathUDA) {
request_str = format(
q{
if (m_settings.stripTrailingUnderscore && url__.endsWith("_"))
url__ = url__[0 .. $-1];
url__ = %s ~ adjustMethodStyle(url__, m_methodStyle);
},
url_prefix
Expand Down
3 changes: 3 additions & 0 deletions tests/restclient/source/app.d
Expand Up @@ -15,6 +15,7 @@ interface ITestAPI
int testID1(int _id);
@path("idtest2")
int testID2(int id); // the special "id" parameter
int testKeyword(int body_, int const_);
}

class TestAPI : ITestAPI
Expand All @@ -25,6 +26,7 @@ class TestAPI : ITestAPI
int customParameters2(int _param, bool _param2) { return _param2 ? _param : -_param; }
int testID1(int _id) { return _id; }
int testID2(int id) { return id; }
int testKeyword(int body_, int const_) { return body_ + const_; }
}

void runTest()
Expand All @@ -45,6 +47,7 @@ void runTest()
assert(api.customParameters2(10, true) == 10);
assert(api.testID1(2) == 2);
assert(api.testID2(3) == 3);
assert(api.testKeyword(3, 4) == 7);
exitEventLoop(true);
}

Expand Down

0 comments on commit f2ffc4b

Please sign in to comment.