From f2ffc4b2ff6b25e1562fb4ee9c8255f9ab5fc3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 19 Sep 2014 10:58:38 +0200 Subject: [PATCH] Implement optional trailing underscore stripping for REST names. This is applied to method and parameter names to enable the use of reserved D words as REST names (similar to vibe.data.serialization). --- examples/rest/source/app.d | 47 +++++++++++------- source/vibe/web/rest.d | 92 ++++++++++++++++++++++++++--------- tests/restclient/source/app.d | 3 ++ 3 files changed, 103 insertions(+), 39 deletions(-) diff --git a/examples/rest/source/app.d b/examples/rest/source/app.d index e4441f6821..a7a26ac381 100644 --- a/examples/rest/source/app.d +++ b/examples/rest/source/app.d @@ -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 _ * @@ -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 ---------- */ @@ -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 ---------- */ @@ -168,11 +168,11 @@ interface Example3APINested class Example3 : Example3API { private: - Example3Nested m_nestedImpl; + Example3Nested m_nestedImpl; public: this() - { + { m_nestedImpl = new Example3Nested(); } @@ -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"); } @@ -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 @@ -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 @@ -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. @@ -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. @@ -303,7 +315,7 @@ class Example5 : Example5API if (!user.authorized) return ""; - + return format("secret #%s for %s", num, user.name); } } @@ -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() @@ -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. */ @@ -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); @@ -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 { diff --git a/source/vibe/web/rest.d b/source/vibe/web/rest.d index 115a48512c..c6e1525404 100644 --- a/source/vibe/web/rest.d +++ b/source/vibe/web/rest.d @@ -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)) { @@ -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; @@ -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 ]; @@ -264,6 +270,7 @@ class RestInterfaceClient(I) : I URL m_baseURL; MethodStyle m_methodStyle; RequestFilter m_requestFilter; + RestInterfaceSettings m_settings; } /** @@ -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) { @@ -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()); } @@ -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; + } } /// @@ -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; @@ -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, @@ -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", @@ -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]); } } } @@ -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"; } @@ -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], @@ -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 diff --git a/tests/restclient/source/app.d b/tests/restclient/source/app.d index 8dde79173f..5aa6ab4136 100644 --- a/tests/restclient/source/app.d +++ b/tests/restclient/source/app.d @@ -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 @@ -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() @@ -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); }