From 109cfd752bd22c89de2f2d23285c094abac9f6a4 Mon Sep 17 00:00:00 2001 From: Dicebot Date: Thu, 17 Oct 2013 02:09:19 +0200 Subject: [PATCH 1/9] vibe.http.rest : example + URL Change DDOC example for registerRestInterface into documented unit-test, reformat it for better readability. Add method for URLRouter to check registered routes. Implement standard way for URL formatting using stand-alone function --- source/vibe/http/rest.d | 188 ++++++++++++++++++++++---------------- source/vibe/http/router.d | 6 ++ 2 files changed, 117 insertions(+), 77 deletions(-) diff --git a/source/vibe/http/rest.d b/source/vibe/http/rest.d index 7c0df50507..8173cd5a7a 100644 --- a/source/vibe/http/rest.d +++ b/source/vibe/http/rest.d @@ -26,7 +26,7 @@ import std.typecons; import std.typetuple; /** - Generates registers a REST interface and connects it the the given instance. + Registers a REST interface and connects it the the given instance. Each method is mapped to the corresponing HTTP verb. Property methods are mapped to GET/PUT and all other methods are mapped according to their prefix verb. If the method has no known prefix, @@ -51,80 +51,105 @@ import std.typetuple; Any interface that you return from a getter will be made available with the base url and its name appended. - Examples: + See_Also: + + RestInterfaceClient class for a seamless way to acces such a generated API - The following example makes MyApi available using HTTP requests. Valid requests are: - - - --- - import vibe.d; - - interface IMyItemsApi { - string getText(); - int getIndex(int id); - } +*/ - interface IMyApi { - string getStatus(); +/// example +unittest +{ + // This is a veru limited example of REST interface + // features. Please refer to `rest` project in vibe.d `examples` + // folder for full overview - it is a very long list. - @property string greeting(); - @property void greeting(string text); + // all details related to HTTP are inferred from + // interface declaration - void addNewUser(string name); - @property string[] users(); - string getName(int id); - - @property IMyItemsApi items(); + interface IMyAPI + { + // GET /api/greeting + @property string greeting(); + + // PUT /api/greeting + @property void greeting(string text); + + // POST /api/users + @path("/users") + void addNewUser(string name); + + // GET /api/users + @property string[] users(); + + // GET /api/:id/name + string getName(int id); + } + + // vibe.d takes care of all JSON encoding/decoding + // and actual API implementation can work directly + // with native types + + class API : IMyAPI + { + private { + string m_greeting; + string[] m_users; } - class MyItemsApiImpl : IMyItemsApi { - string getText() { return "Hello, World"; } - int getIndex(int id) { return id; } + @property string greeting() + { + return m_greeting; } - class MyApiImpl : IMyApi { - private string m_greeting; - private string[] m_users; - private MyItemsApiImpl m_items; - - this() { m_items = new MyItemsApiImpl; } - - string getStatus() { return "OK"; } + @property void greeting(string text) + { + m_greeting = text; + } - @property string greeting() { return m_greeting; } - @property void greeting(string text) { m_greeting = text; } + void addNewUser(string name) + { + m_users ~= name; + } - void addNewUser(string name) { m_users ~= name; } - @property string[] users() { return m_users; } - string getName(int id) { return m_users[id]; } - - @property MyItemsApiImpl items() { return m_items; } + @property string[] users() + { + return m_users; } - static this() + string getName(int id) { - auto routes = new URLRouter; + return m_users[id]; + } + } - registerRestInterface(routes, new MyApiImpl, "/api/"); + // actual usage, this is usually done in app.d module + // constructor - listenHTTP(new HTTPServerSettings, routes); - } - --- + void static_this() + { + auto router = new URLRouter(); + registerRestInterface(router, new API()); + listenHTTP(new HTTPServerSettings(), router); + } +} + +// concatenates two URL parts avoiding any duplicate slashes +// in resulting URL. `trailing` defines of result URL must +// end with slash +private string concatUrl(string prefix, string url, bool trailing = false) +{ + // "/" is ASCII, so can just slice + auto pre = prefix.endsWith("/") ? prefix[0..$-1] : prefix; + auto post = url.startsWith("/") ? url : ( "/" ~ url ); + if (trailing) { + return post.endsWith("/") ? (pre ~ post) : (pre ~ post ~ "/"); + } + else { + return post.endsWith("/") ? (pre ~ post[0..$-1]) : (pre ~ post); + } +} - See_Also: - - RestInterfaceClient class for a seamless way to acces such a generated API -*/ void registerRestInterface(TImpl)(URLRouter router, TImpl instance, string urlPrefix, MethodStyle style = MethodStyle.lowerUnderscored) { @@ -144,31 +169,38 @@ void registerRestInterface(TImpl)(URLRouter router, TImpl instance, string urlPr enum meta = extractHTTPMethodAndName!(overload)(); enum pathOverriden = meta[0]; HTTPMethod httpVerb = meta[1]; - static if (pathOverriden) + + static if (pathOverriden) { string url = meta[2]; - else - { - static if (__traits(identifier, overload) == "index") - pragma(msg, "Processing interface " ~ T.stringof ~ ": please use @path(\"/\") to define '/' path instead of 'index' method." - " Special behavior will be removed in the next release."); + } + else { + static if (__traits(identifier, overload) == "index") { + pragma(msg, "Processing interface " ~ T.stringof ~ + ": please use @path(\"/\") to define '/' path" ~ + " instead of 'index' method. Special behavior will be removed" ~ + " in the next release."); + } string url = adjustMethodStyle(meta[2], style); - } - + } + static if( is(RetType == interface) ) { static assert(ParameterTypeTuple!overload.length == 0, "Interfaces may only be returned from parameter-less functions!"); - registerRestInterface!RetType(router, __traits(getMember, instance, method)(), urlPrefix ~ url ~ "/"); + registerRestInterface!RetType(router, __traits(getMember, instance, method)(), concatUrl(urlPrefix, url, true)); } else { auto handler = jsonMethodHandler!(T, method, overload)(instance); string id_supplement; size_t skip = 0; // legacy special case for :id, left for backwards-compatibility reasons - if( paramNames.length && paramNames[0] == "id" ) { - addRoute(httpVerb, urlPrefix ~ ":id/" ~ url, handler, paramNames); - if( url.length == 0 ) - addRoute(httpVerb, urlPrefix ~ ":id", handler, paramNames); - } else - addRoute(httpVerb, urlPrefix ~ url, handler, paramNames); + if (paramNames.length && paramNames[0] == "id") { + auto combinedUrl = concatUrl( + concatUrl(urlPrefix, ":id"), + url + ); + addRoute(httpVerb, combinedUrl, handler, paramNames); + } else { + addRoute(httpVerb, concatUrl(urlPrefix, url), handler, paramNames); + } } } } @@ -304,8 +336,7 @@ class RestInterfaceClient(I) : I #line 1 "restinterface" mixin(generateRestInterfaceMethods!I()); -#line 307 "source/vibe/http/rest.d" -static assert(__LINE__ == 307); +#line 354 "source/vibe/http/rest.d" protected Json request(string verb, string name, Json params, bool[string] paramIsJson) const { URL url = m_baseURL; @@ -379,7 +410,10 @@ unittest */ string adjustMethodStyle(string name, MethodStyle style) { - if (name.length == 0) return null; + if (!name.length) { + return ""; + } + import std.uni; final switch(style){ diff --git a/source/vibe/http/router.d b/source/vibe/http/router.d index 28eff189f3..cfa54bac98 100644 --- a/source/vibe/http/router.d +++ b/source/vibe/http/router.d @@ -168,6 +168,12 @@ class URLRouter : HTTPRouter { logTrace("no route match: %s %s", req.method, req.requestURL); } + + /// Returns all registered routes as const AA + const(typeof(m_routes)) getAllRoutes() + { + return m_routes; + } } /// Deprecated compatibility alias From 206ae73a70f79b5feb6e67a9228a7415cb16a44b Mon Sep 17 00:00:00 2001 From: Dicebot Date: Thu, 17 Oct 2013 15:16:19 +0200 Subject: [PATCH 2/9] rest example: add termination event --- examples/rest/source/app.d | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/rest/source/app.d b/examples/rest/source/app.d index ad654246fe..fdbd96dfd9 100644 --- a/examples/rest/source/app.d +++ b/examples/rest/source/app.d @@ -267,5 +267,6 @@ shared static this() assert(api.getParametersInURL("20", "30") == 50); } logInfo("Success."); + exitEventLoop(true); }); } From eb7620ba56613c74f2c5e076b0cdc1be03eacbca Mon Sep 17 00:00:00 2001 From: Dicebot Date: Thu, 17 Oct 2013 16:54:39 +0200 Subject: [PATCH 3/9] rest example: add unittests to verify route paths --- examples/rest/source/app.d | 46 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/examples/rest/source/app.d b/examples/rest/source/app.d index fdbd96dfd9..95a0c16bad 100644 --- a/examples/rest/source/app.d +++ b/examples/rest/source/app.d @@ -49,7 +49,7 @@ interface Example1API class Example1 : Example1API { - override: // use of this handy D feature is highly recommended + override: // usage of this handy D feature is highly recommended string getSomeInfo() { return "Some Info!"; @@ -67,6 +67,17 @@ class Example1 : Example1API } } +unittest +{ + auto router = new URLRouter; + 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"); +} + /* --------- EXAMPLE 2 ---------- */ /* Step forward. Using some compound types and query parameters. @@ -107,10 +118,21 @@ class Example2 : Example2API { import std.algorithm; // Some sweet functional D - return reduce!( (a, b) => Aggregate(a.name ~ b.name, a.count + b.count, Aggregate.Type.Type3) )(Aggregate.init, input); + return reduce!( + (a, b) => Aggregate(a.name ~ b.name, a.count + b.count, Aggregate.Type.Type3) + )(Aggregate.init, input); } } +unittest +{ + auto router = new URLRouter; + registerRestInterface(router, new Example2(), MethodStyle.upperUnderscored); + auto routes = router.getAllRoutes(); + + assert (routes[HTTPMethod.GET][0].pattern == "/EXAMPLE2_API/ACCUMULATE_ALL"); +} + /* --------- EXAMPLE 3 ---------- */ /* Nested REST interfaces may be used to better match your D code structure with URL paths. @@ -172,6 +194,17 @@ class Example3Nested : Example3APINested } } +unittest +{ + auto router = new URLRouter; + 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"); +} + + /* If pre-defined conventions do not suit your needs, you can configure url and method * precisely via User Defined Attributes. */ @@ -208,6 +241,15 @@ class Example4 : Example4API } } +unittest +{ + auto router = new URLRouter; + 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"); +} shared static this() { From 43b9828e618ff4dd0355a0675bd49189448859cf Mon Sep 17 00:00:00 2001 From: Dicebot Date: Thu, 17 Oct 2013 20:34:27 +0200 Subject: [PATCH 4/9] vibe.http.rest : full restyle Adapt everything to vibe.d standard code style Localize imports Improve formatting where it helps readability --- examples/rest/source/app.d | 10 +- source/vibe/http/rest.d | 995 +++++++++++++++++++++++-------------- 2 files changed, 621 insertions(+), 384 deletions(-) diff --git a/examples/rest/source/app.d b/examples/rest/source/app.d index 95a0c16bad..c0860aadcf 100644 --- a/examples/rest/source/app.d +++ b/examples/rest/source/app.d @@ -158,8 +158,11 @@ interface Example3APINested { /* In this example it will be available under "GET /nested_module/number" * But this interface does't really know it, it does not care about exact path + * + * Default parameter values work as expected - they get used if there are no data + & for that parameter in request. */ - int getNumber(); + int getNumber(int def_arg = 42); } class Example3 : Example3API @@ -188,9 +191,9 @@ class Example3 : Example3API class Example3Nested : Example3APINested { override: - int getNumber() + int getNumber(int def_arg) { - return 42; + return def_arg; } } @@ -301,6 +304,7 @@ shared static this() auto api = new RestInterfaceClient!Example3API("http://127.0.0.1:8080"); assert(api.getMyID(9000) == 9000); assert(api.nestedModule.getNumber() == 42); + assert(api.nestedModule.getNumber(1) == 1); } // Example 4 { diff --git a/source/vibe/http/rest.d b/source/vibe/http/rest.d index 8173cd5a7a..0abd8e8569 100644 --- a/source/vibe/http/rest.d +++ b/source/vibe/http/rest.d @@ -7,23 +7,11 @@ */ module vibe.http.rest; +import vibe.http.router : URLRouter; +import vibe.http.common : HTTPMethod; +import vibe.http.server : HTTPServerRequestDelegate; + import vibe.core.log; -import vibe.data.json; -import vibe.http.client; -import vibe.http.router; -import vibe.inet.url; -import vibe.textfilter.urlencode; -import vibe.utils.string; -import vibe.utils.meta.all; - -import std.algorithm : filter; -import std.array; -import std.conv; -import std.exception; -import std.string; -import std.traits; -import std.typecons; -import std.typetuple; /** Registers a REST interface and connects it the the given instance. @@ -56,6 +44,115 @@ import std.typetuple; RestInterfaceClient class for a seamless way to acces such a generated API */ +void registerRestInterface(TImpl)(URLRouter router, TImpl instance, string url_prefix, + MethodStyle style = MethodStyle.lowerUnderscored) +{ + import vibe.utils.meta.uda : extractUda; + import vibe.utils.meta.traits : baseInterface; + import std.traits : MemberFunctionsTuple, ParameterIdentifierTuple, + ParameterTypeTuple, ReturnType; + + void addRoute(HTTPMethod httpVerb, string url, HTTPServerRequestDelegate handler, string[] params) + { + import std.algorithm : filter, startsWith; + import std.array : array; + + router.match(httpVerb, url, handler); + logDiagnostic( + "REST route: %s %s %s", + httpVerb, + url, + params.filter!(p => !p.startsWith("_") && p != "id")().array() + ); + } + + alias I = baseInterface!TImpl; + + foreach (method; __traits(allMembers, I)) { + foreach (overload; MemberFunctionsTuple!(I, method)) { + + enum meta = extractHTTPMethodAndName!overload(); + + static if (meta.hadPathUDA) { + string url = meta.url; + } + else { + static if (__traits(identifier, overload) == "index") { + pragma(msg, "Processing interface " ~ T.stringof ~ + ": please use @path(\"/\") to define '/' path" ~ + " instead of 'index' method. Special behavior will be removed" ~ + " in the next release."); + } + + string url = adjustMethodStyle(meta.url, style); + } + + alias RT = ReturnType!overload; + + static if (is(RT == interface)) { + // nested API + static assert( + ParameterTypeTuple!overload.length == 0, + "Interfaces may only be returned from parameter-less functions!" + ); + registerRestInterface!RT( + router, + __traits(getMember, instance, method)(), + concatURL(url_prefix, url, true) + ); + } else { + // normal handler + auto handler = jsonMethodHandler!(I, method, overload)(instance); + + string[] params = [ ParameterIdentifierTuple!overload ]; + + // legacy special case for :id, left for backwards-compatibility reasons + if (params.length && params[0] == "id") { + auto combined_url = concatURL( + concatURL(url_prefix, ":id"), + url + ); + addRoute(meta.method, combined_url, handler, params); + } else { + addRoute(meta.method, concatURL(url_prefix, url), handler, params); + } + } + } + } +} + +/// ditto +void registerRestInterface(TImpl)(URLRouter router, TImpl instance, MethodStyle style = MethodStyle.lowerUnderscored) +{ + // this shorter overload tries to deduce root path automatically + + import vibe.utils.meta.uda : extractUda; + import vibe.utils.meta.traits : baseInterface; + + alias I = baseInterface!TImpl; + enum uda = extractUda!(RootPath, I); + + static if (is(typeof(uda) == typeof(null))) + registerRestInterface!I(router, instance, "/", style); + else + { + static if (uda.data == "") + { + auto path = "/" ~ adjustMethodStyle(I.stringof, style); + registerRestInterface!I(router, instance, path, style); + } + else + { + auto path = uda.data; + registerRestInterface!I( + router, + instance, + concatURL("/", uda.data), + style + ); + } + } +} /// example unittest @@ -128,6 +225,8 @@ unittest void static_this() { + import vibe.http.server, vibe.http.router; + auto router = new URLRouter(); registerRestInterface(router, new API()); listenHTTP(new HTTPServerSettings(), router); @@ -139,6 +238,8 @@ unittest // end with slash private string concatUrl(string prefix, string url, bool trailing = false) { + import std.string : startsWith, endsWith; + // "/" is ASCII, so can just slice auto pre = prefix.endsWith("/") ? prefix[0..$-1] : prefix; auto post = url.startsWith("/") ? url : ( "/" ~ url ); @@ -150,88 +251,6 @@ private string concatUrl(string prefix, string url, bool trailing = false) } } -void registerRestInterface(TImpl)(URLRouter router, TImpl instance, string urlPrefix, - MethodStyle style = MethodStyle.lowerUnderscored) -{ - void addRoute(HTTPMethod httpVerb, string url, HTTPServerRequestDelegate handler, string[] params) - { - router.match(httpVerb, url, handler); - logDiagnostic("REST route: %s %s %s", httpVerb, url, params.filter!(p => !p.startsWith("_") && p != "id")().array()); - } - - alias T = baseInterface!TImpl; - - foreach( method; __traits(allMembers, T) ) { - foreach( overload; MemberFunctionsTuple!(T, method) ) { - alias ReturnType!overload RetType; - string[] paramNames = [ParameterIdentifierTuple!overload]; - - enum meta = extractHTTPMethodAndName!(overload)(); - enum pathOverriden = meta[0]; - HTTPMethod httpVerb = meta[1]; - - static if (pathOverriden) { - string url = meta[2]; - } - else { - static if (__traits(identifier, overload) == "index") { - pragma(msg, "Processing interface " ~ T.stringof ~ - ": please use @path(\"/\") to define '/' path" ~ - " instead of 'index' method. Special behavior will be removed" ~ - " in the next release."); - } - - string url = adjustMethodStyle(meta[2], style); - } - - static if( is(RetType == interface) ) { - static assert(ParameterTypeTuple!overload.length == 0, "Interfaces may only be returned from parameter-less functions!"); - registerRestInterface!RetType(router, __traits(getMember, instance, method)(), concatUrl(urlPrefix, url, true)); - } else { - auto handler = jsonMethodHandler!(T, method, overload)(instance); - string id_supplement; - size_t skip = 0; - // legacy special case for :id, left for backwards-compatibility reasons - if (paramNames.length && paramNames[0] == "id") { - auto combinedUrl = concatUrl( - concatUrl(urlPrefix, ":id"), - url - ); - addRoute(httpVerb, combinedUrl, handler, paramNames); - } else { - addRoute(httpVerb, concatUrl(urlPrefix, url), handler, paramNames); - } - } - } - } -} -/// ditto -void registerRestInterface(TImpl)(URLRouter router, TImpl instance, MethodStyle style = MethodStyle.lowerUnderscored) -{ - alias T = baseInterface!TImpl; - enum uda = extractUda!(RootPath, T); - static if (is(typeof(uda) == typeof(null))) - registerRestInterface!T(router, instance, "/", style); - else - { - static if (uda.data == "") - { - auto path = "/" ~ adjustMethodStyle(T.stringof, style) ~ "/"; - registerRestInterface!T(router, instance, path, style); - } - else - { - auto path = uda.data; - if (!path.startsWith("/")) - path = "/" ~ path; - if (!path.endsWith("/")) - path = path ~ "/"; - registerRestInterface!T(router, instance, path, style); - } - } -} - - /** Implements the given interface by forwarding all public methods to a REST server. @@ -239,167 +258,210 @@ void registerRestInterface(TImpl)(URLRouter router, TImpl instance, MethodStyle the matching method style for this. The RestInterfaceClient class will derive from the interface that is passed as a template argument. It can be used as a drop-in replacement of the real implementation of the API this way. - - Examples: - - An example client that accesses the API defined in the registerRestInterface() example: - - --- - import vibe.d; - - interface IMyApi { - string getStatus(); - - @property string greeting(); - @property void greeting(string text); - - void addNewUser(string name); - @property string[] users(); - string getName(int id); - } - - static this() - { - auto api = new RestInterfaceClient!IMyApi("http://127.0.0.1/api/"); - - logInfo("Status: %s", api.getStatus()); - api.greeting = "Hello, World!"; - logInfo("Greeting message: %s", api.greeting); - api.addNewUser("Peter"); - api.addNewUser("Igor"); - logInfo("Users: %s", api.users); - } - --- */ class RestInterfaceClient(I) : I { //pragma(msg, "imports for "~I.stringof~":"); //pragma(msg, generateModuleImports!(I)()); +#line 1 "module imports" mixin(generateModuleImports!I()); +#line 243 + + import vibe.inet.url : URL, PathEntry; + import vibe.http.client : HTTPClientRequest; - alias void delegate(HTTPClientRequest req) RequestFilter; + alias RequestFilter = void delegate(HTTPClientRequest req); + private { URL m_baseURL; MethodStyle m_methodStyle; RequestFilter m_requestFilter; } - alias I BaseInterface; - - /** Creates a new REST implementation of I + /** + Creates a new REST implementation of I */ - this(string base_url, MethodStyle style = MethodStyle.lowerUnderscored) + this (string base_url, MethodStyle style = MethodStyle.lowerUnderscored) { - enum uda = extractUda!(RootPath, I); - static if (is(typeof(uda) == typeof(null))) - m_baseURL = URL.parse(base_url); - else - { - static if (uda.data == "") - m_baseURL = URL.parse(base_url ~ "/" ~ adjustMethodStyle(I.stringof, style) ~ "/"); - else - { - auto path = uda.data; - if (!path.startsWith("/")) - path = "/" ~ path; - if (!path.endsWith("/")) - path = path ~ "/"; - m_baseURL = URL.parse(base_url ~ adjustMethodStyle(uda.data, style)); + import vibe.utils.meta.uda : extractUda; + + URL url; + enum uda = extractUda!(RootPath, I); + static if (is(typeof(uda) == typeof(null))) { + url = URL.parse(base_url); + } + else + { + static if (uda.data == "") { + url = URL.parse( + concatURL(base_url, adjustMethodStyle(I.stringof, style), true) + ); } - } - m_methodStyle = style; - mixin(generateRestInterfaceSubInterfaceInstances!I()); + else { + m_baseURL = URL.parse( + concatURL(base_url, uda.data, true) + ); + } + } + + this(url, style); } + /// ditto this(URL base_url, MethodStyle style = MethodStyle.lowerUnderscored) { m_baseURL = base_url; m_methodStyle = style; - mixin(generateRestInterfaceSubInterfaceInstances!I()); + +#line 1 "subinterface instances" + mixin (generateRestInterfaceSubInterfaceInstances!I()); +#line 293 } - /** An optional request filter that allows to modify each request before it is made. + /** + An optional request filter that allows to modify each request before it is made. */ - @property RequestFilter requestFilter() { return m_requestFilter; } + @property RequestFilter requestFilter() + { + return m_requestFilter; + } + /// ditto @property void requestFilter(RequestFilter v) { m_requestFilter = v; - mixin(generateRestInterfaceSubInterfaceRequestFilter!I()); +#line 1 "request filter" + mixin (generateRestInterfaceSubInterfaceRequestFilter!I()); +#line 309 } + //pragma(msg, "subinterfaces:"); //pragma(msg, generateRestInterfaceSubInterfaces!(I)()); #line 1 "subinterfaces" - mixin(generateRestInterfaceSubInterfaces!I()); + mixin (generateRestInterfaceSubInterfaces!I()); //pragma(msg, "restinterface:"); //pragma(msg, generateRestInterfaceMethods!(I)()); #line 1 "restinterface" - mixin(generateRestInterfaceMethods!I()); + mixin (generateRestInterfaceMethods!I()); +#line 337 "source/vibe/http/rest.d" + + protected { + import vibe.data.json : Json; + import vibe.textfilter.urlencode; -#line 354 "source/vibe/http/rest.d" - protected Json request(string verb, string name, Json params, bool[string] paramIsJson) - const { - URL url = m_baseURL; - if( name.length ) url ~= Path(name); - else if( !url.path.endsWithSlash ){ - auto p = url.path; - p.endsWithSlash = true; - url.path = p; - } - - if( (verb == "GET" || verb == "HEAD") && params.length > 0 ){ - auto queryString = appender!string(); - bool first = true; - foreach( string pname, p; params ){ - if( !first ) queryString.put('&'); - else first = false; - filterURLEncode(queryString, pname); - queryString.put('='); - filterURLEncode(queryString, paramIsJson[pname] ? p.toString() : toRestString(p)); + Json request(string verb, string name, Json params, bool[string] param_is_json) const + { + import vibe.http.client : HTTPClientRequest, HTTPClientResponse, + requestHTTP; + import vibe.http.common : HTTPStatusException, HTTPStatus, + httpMethodFromString, httpStatusText; + import vibe.inet.url : Path; + import std.string : appender; + + URL url = m_baseURL; + + if (name.length) { + url ~= Path(name); } - url.queryString = queryString.data(); - } - - Json ret; + else if (url.path.endsWithSlash) { + auto p = url.path; + p.endsWithSlash = false; + url.path = p; + } + + if ((verb == "GET" || verb == "HEAD") && params.length > 0) { + auto query = appender!string(); + bool first = true; + + foreach (string pname, p; params) { + if (!first) { + query.put('&'); + } + else { + first = false; + } + filterURLEncode(query, pname); + query.put('='); + filterURLEncode(query, param_is_json[pname] ? p.toString() : toRestString(p)); + } + + url.queryString = query.data(); + } + + Json ret; - requestHTTP(url, - (scope req){ + auto reqdg = (scope HTTPClientRequest req) { req.method = httpMethodFromString(verb); - if( m_requestFilter ) m_requestFilter(req); - if( verb != "GET" && verb != "HEAD" ) + + if (m_requestFilter) { + m_requestFilter(req); + } + + if (verb != "GET" && verb != "HEAD") { req.writeJsonBody(params); - }, - (scope res){ + } + }; + + auto resdg = (scope HTTPClientResponse res) { ret = res.readJson(); - logDebug("REST call: %s %s -> %d, %s", verb, url.toString(), res.statusCode, ret.toString()); - if( res.statusCode != HTTPStatus.OK ){ - if( ret.type == Json.Type.Object && ret.statusMessage.type == Json.Type.String ) + + logDebug( + "REST call: %s %s -> %d, %s", + verb, + url.toString(), + res.statusCode, + ret.toString() + ); + + if (res.statusCode != HTTPStatus.OK) { + if (ret.type == Json.Type.Object && ret.statusMessage.type == Json.Type.String) { throw new HTTPStatusException(res.statusCode, ret.statusMessage.get!string); - else throw new HTTPStatusException(res.statusCode, httpStatusText(res.statusCode)); + } + else { + throw new HTTPStatusException(res.statusCode, httpStatusText(res.statusCode)); + } } - } - ); - - return ret; + }; + + requestHTTP(url, reqdg, resdg); + + return ret; + } } } +/// unittest { - // checking that rest client actually instantiates - interface TestAPI - { - string getInfo(); - - @method(HTTPMethod.DELETE) - double[] setSomething(int num); + interface IMyApi + { + // GET /status + string getStatus(); + + // GET /greeting + @property string greeting(); + // PUT /greeting + @property void greeting(string text); - @path("/process/:param/:param2/please") - void readOnly(string _param, string _param2); + // POST /new_user + void addNewUser(string name); + // GET /users + @property string[] users(); + // GET /:id/name + string getName(int id); + } + + void application() + { + auto api = new RestInterfaceClient!IMyApi("http://127.0.0.1/api/"); + + logInfo("Status: %s", api.getStatus()); + api.greeting = "Hello, World!"; + logInfo("Greeting message: %s", api.greeting); + api.addNewUser("Peter"); + api.addNewUser("Igor"); + logInfo("Users: %s", api.users); } - - auto api = new RestInterfaceClient!TestAPI("http://127.0.0.1"); - assert(api); } /** @@ -416,21 +478,28 @@ string adjustMethodStyle(string name, MethodStyle style) import std.uni; - final switch(style){ + final switch(style) { case MethodStyle.unaltered: return name; case MethodStyle.camelCase: size_t i = 0; - foreach (idx, dchar ch; name) - if (isUpper(ch)) i = idx; + foreach (idx, dchar ch; name) { + if (isUpper(ch)) { + i = idx; + } else break; + } if (i == 0) { std.utf.decode(name, i); return std.string.toLower(name[0 .. i]) ~ name[i .. $]; } else { std.utf.decode(name, i); - if (i < name.length) return std.string.toLower(name[0 .. i-1]) ~ name[i-1 .. $]; - else return std.string.toLower(name); + if (i < name.length) { + return std.string.toLower(name[0 .. i-1]) ~ name[i-1 .. $]; + } + else { + return std.string.toLower(name); + } } case MethodStyle.pascalCase: size_t idx = 0; @@ -446,21 +515,32 @@ string adjustMethodStyle(string name, MethodStyle style) size_t start = 0, i = 0; while (i < name.length) { // skip acronyms - while (i < name.length && (i+1 >= name.length || (name[i+1] >= 'A' && name[i+1] <= 'Z'))) std.utf.decode(name, i); + while (i < name.length && (i+1 >= name.length || (name[i+1] >= 'A' && name[i+1] <= 'Z'))) { + std.utf.decode(name, i); + } // skip the main (lowercase) part of a word - while (i < name.length && !(name[i] >= 'A' && name[i] <= 'Z')) std.utf.decode(name, i); + while (i < name.length && !(name[i] >= 'A' && name[i] <= 'Z')) { + std.utf.decode(name, i); + } // add a single word - if( ret.length > 0 ) ret ~= "_"; + if( ret.length > 0 ) { + ret ~= "_"; + } ret ~= name[start .. i]; // quick skip the capital and remember the start of the next word start = i; - if (i < name.length) std.utf.decode(name, i); + if (i < name.length) { + std.utf.decode(name, i); + } } - if (i < name.length) ret ~= "_" ~ name[start .. $]; - return style == MethodStyle.lowerUnderscored ? std.string.toLower(ret) : std.string.toUpper(ret); + if (i < name.length) { + ret ~= "_" ~ name[start .. $]; + } + return style == MethodStyle.lowerUnderscored ? + std.string.toLower(ret) : std.string.toUpper(ret); } } @@ -491,11 +571,11 @@ unittest assert(adjustMethodStyle("IDTest", MethodStyle.camelCase) == "idTest"); } - /** Determines the naming convention of an identifier. */ -enum MethodStyle { +enum MethodStyle +{ /// Special value for free-style conventions unaltered, /// camelCaseNaming @@ -530,70 +610,116 @@ enum MethodStyle { /// private private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func)(T inst) { - alias ParameterTypeTuple!Func ParameterTypes; - alias ReturnType!Func RetType; - alias ParameterDefaultValueTuple!Func DefaultValues; - enum paramNames = [ParameterIdentifierTuple!Func]; + import std.traits : ParameterTypeTuple, ReturnType, + ParameterDefaultValueTuple, ParameterIdentifierTuple; + import std.string : format; + import std.algorithm : startsWith; + import std.exception : enforce; + + import vibe.http.server : HTTPServerRequest, HTTPServerResponse; + import vibe.http.common : HTTPStatusException, HTTPStatus; + import vibe.utils.string : sanitizeUTF8; + + alias PT = ParameterTypeTuple!Func; + alias RT = ReturnType!Func; + alias ParamDefaults = ParameterDefaultValueTuple!Func; + enum ParamNames = [ ParameterIdentifierTuple!Func ]; void handler(HTTPServerRequest req, HTTPServerResponse res) { - ParameterTypes params; + PT params; - foreach( i, P; ParameterTypes ){ - static assert(paramNames[i].length, "Parameter "~i.stringof~" of "~method~" has no name"); - static if( i == 0 && paramNames[i] == "id" ){ + foreach (i, P; PT) { + static assert ( + ParamNames[i].length, + format( + "Parameter %s of %s has no name", + i.stringof, + method + ) + ); + + static if (i == 0 && ParamNames[i] == "id") { + // legacy special case for :id, backwards-compatibility logDebug("id %s", req.params["id"]); params[i] = fromRestString!P(req.params["id"]); - } else static if( paramNames[i].startsWith("_") ){ - static if( paramNames[i] != "_dummy"){ - enforce(paramNames[i][1 .. $] in req.params, "req.param[\""~paramNames[i][1 .. $]~"\"] was not set!"); - logDebug("param %s %s", paramNames[i], req.params[paramNames[i][1 .. $]]); - params[i] = fromRestString!P(req.params[paramNames[i][1 .. $]]); + } else static if (ParamNames[i].startsWith("_")) { + // URL parameter + static if (ParamNames[i] != "_dummy") { + enforce( + ParamNames[i][1 .. $] in req.params, + format("req.param[%s] was not set!", ParamNames[i][1 .. $]) + ); + logDebug("param %s %s", ParamNames[i], req.params[ParamNames[i][1 .. $]]); + params[i] = fromRestString!P(req.params[ParamNames[i][1 .. $]]); } } else { - alias DefaultValues[i] DefVal; - if( req.method == HTTPMethod.GET ){ - logDebug("query %s of %s" ,paramNames[i], req.query); - static if( is(DefVal == void) ){ - enforce(paramNames[i] in req.query, "Missing query parameter '"~paramNames[i]~"'"); + // normal parameter + alias DefVal = ParamDefaults[i]; + if (req.method == HTTPMethod.GET ) { + logDebug("query %s of %s" ,ParamNames[i], req.query); + + static if (is (DefVal == void)) { + enforce( + ParamNames[i] in req.query, + format("Missing query parameter '%s'", ParamNames[i]) + ); } else { - if( paramNames[i] !in req.query ){ + if (ParamNames[i] !in req.query) { params[i] = DefVal; continue; } } - params[i] = fromRestString!P(req.query[paramNames[i]]); + params[i] = fromRestString!P(req.query[ParamNames[i]]); } else { - logDebug("%s %s", method, paramNames[i]); - enforce(req.contentType == "application/json", "The Content-Type header needs to be set to application/json."); - enforce(req.json.type != Json.Type.Undefined, "The request body does not contain a valid JSON value."); - enforce(req.json.type == Json.Type.Object, "The request body must contain a JSON object with an entry for each parameter."); - static if( is(DefVal == void) ){ - enforce(req.json[paramNames[i]].type != Json.Type.Undefined, "Missing parameter "~paramNames[i]~"."); + logDebug("%s %s", method, ParamNames[i]); + + enforce( + req.contentType == "application/json", + "The Content-Type header needs to be set to application/json." + ); + enforce( + req.json.type != Json.Type.Undefined, + "The request body does not contain a valid JSON value." + ); + enforce( + req.json.type == Json.Type.Object, + "The request body must contain a JSON object with an entry for each parameter." + ); + + static if (is(DefVal == void)) { + enforce( + req.json[ParamNames[i]].type != Json.Type.Undefined, + format("Missing parameter %s", ParamNames[i]) + ); } else { - if( req.json[paramNames[i]].type == Json.Type.Undefined ){ + if (req.json[ParamNames[i]].type == Json.Type.Undefined) { params[i] = DefVal; continue; } } - params[i] = deserializeJson!P(req.json[paramNames[i]]); + + params[i] = deserializeJson!P(req.json[ParamNames[i]]); } } } try { - static if( is(RetType == void) ){ + static if (is(RT == void)) { __traits(getMember, inst, method)(params); res.writeJsonBody(Json.emptyObject); } else { auto ret = __traits(getMember, inst, method)(params); res.writeJsonBody(serializeToJson(ret)); } - } catch( HTTPStatusException e) { - res.writeJsonBody(["statusMessage": e.msg], e.status); - } catch( Exception e ){ + } catch (HTTPStatusException e) { + res.writeJsonBody([ "statusMessage": e.msg ], e.status); + } catch (Exception e) { // TODO: better error description! - res.writeJsonBody(["statusMessage": e.msg, "statusDebugMessage": sanitizeUTF8(cast(ubyte[])e.toString())], HTTPStatus.internalServerError); + res.writeJsonBody( + [ "statusMessage": e.msg, "statusDebugMessage": sanitizeUTF8(cast(ubyte[])e.toString()) ], + HTTPStatus.internalServerError + ); } } @@ -605,27 +731,42 @@ private string generateRestInterfaceSubInterfaces(I)() { if (!__ctfe) assert(false); + + import std.traits : MemberFunctionsTuple, FunctionTypeOf, + ReturnType, ParameterTypeTuple, fullyQualifiedName; + import std.algorithm : canFind; + import std.string : format; string ret; - string[] tps; - foreach( method; __traits(allMembers, I) ){ - foreach( overload; MemberFunctionsTuple!(I, method) ){ - alias FunctionTypeOf!overload FT; - alias ParameterTypeTuple!FT PTypes; - alias ReturnType!FT RT; - static if( is(RT == interface) ){ - static assert(PTypes.length == 0, "Interface getters may not have parameters."); + string[] tps; // list of already processed interface types + + foreach (method; __traits(allMembers, I)) { + foreach (overload; MemberFunctionsTuple!(I, method)) { + + alias FT = FunctionTypeOf!overload; + alias PTT = ParameterTypeTuple!FT; + alias RT = ReturnType!FT; + + static if (is(RT == interface)) { + static assert ( + PTT.length == 0, + "Interface getters may not have parameters." + ); + if (!tps.canFind(RT.stringof)) { tps ~= RT.stringof; - string implname = RT.stringof~"Impl"; + string implname = RT.stringof ~ "Impl"; ret ~= format( - q{alias RestInterfaceClient!(%s) %s;}, + q{ + alias RestInterfaceClient!(%s) %s; + }, fullyQualifiedName!RT, implname ); - ret ~= "\n"; ret ~= format( - q{private %s m_%s;}, + q{ + private %s m_%s; + }, implname, implname ); @@ -642,24 +783,33 @@ private string generateRestInterfaceSubInterfaceInstances(I)() { if (!__ctfe) assert(false); + + import std.traits : MemberFunctionsTuple, FunctionTypeOf, + ReturnType, ParameterTypeTuple; + import std.string : format; + import std.algorithm : canFind; string ret; - string[] tps; - foreach( method; __traits(allMembers, I) ){ - foreach( overload; MemberFunctionsTuple!(I, method) ){ - alias FunctionTypeOf!overload FT; - alias ParameterTypeTuple!FT PTypes; - alias ReturnType!FT RT; - static if( is(RT == interface) ){ - static assert(PTypes.length == 0, "Interface getters may not have parameters."); + string[] tps; // list of of already processed interface types + + foreach (method; __traits(allMembers, I)) { + foreach (overload; MemberFunctionsTuple!(I, method)) { + + alias FT = FunctionTypeOf!overload; + alias PTT = ParameterTypeTuple!FT; + alias RT = ReturnType!FT; + + static if (is(RT == interface)) { + static assert ( + PTT.length == 0, + "Interface getters may not have parameters." + ); + if (!tps.canFind(RT.stringof)) { tps ~= RT.stringof; - string implname = RT.stringof~"Impl"; + string implname = RT.stringof ~ "Impl"; enum meta = extractHTTPMethodAndName!overload(); - bool pathOverriden = meta[0]; - HTTPMethod http_verb = meta[1]; - string url = meta[2]; ret ~= format( q{ @@ -668,15 +818,16 @@ private string generateRestInterfaceSubInterfaceInstances(I)() else m_%s = new %s(m_baseURL.toString() ~ adjustMethodStyle(PathEntry("%s").toString(), m_methodStyle), m_methodStyle); }, - pathOverriden, - implname, implname, url, - implname, implname, url + meta.hadPathUDA, + implname, implname, meta.url, + implname, implname, meta.url ); ret ~= "\n"; } } } } + return ret; } @@ -685,22 +836,36 @@ private string generateRestInterfaceSubInterfaceRequestFilter(I)() { if (!__ctfe) assert(false); + + import std.traits : MemberFunctionsTuple, FunctionTypeOf, + ReturnType, ParameterTypeTuple; + import std.string : format; + import std.algorithm : canFind; string ret; - string[] tps; - foreach( method; __traits(allMembers, I) ){ - foreach( overload; MemberFunctionsTuple!(I, method) ){ - alias FunctionTypeOf!overload FT; - alias ParameterTypeTuple!FT PTypes; - alias ReturnType!FT RT; - static if( is(RT == interface) ){ - static assert(PTypes.length == 0, "Interface getters may not have parameters."); + string[] tps; // list of already processed interface types + + foreach (method; __traits(allMembers, I)) { + foreach (overload; MemberFunctionsTuple!(I, method)) { + + alias FT = FunctionTypeOf!overload; + alias PTT = ParameterTypeTuple!FT; + alias RT = ReturnType!FT; + + static if (is(RT == interface)) { + static assert ( + PTT.length == 0, + "Interface getters may not have parameters." + ); + if (!tps.canFind(RT.stringof)) { tps ~= RT.stringof; - string implname = RT.stringof~"Impl"; + string implname = RT.stringof ~ "Impl"; ret ~= format( - q{m_%s.requestFilter = m_requestFilter;}, + q{ + m_%s.requestFilter = m_requestFilter; + }, implname ); ret ~= "\n"; @@ -716,23 +881,31 @@ private string generateRestInterfaceMethods(I)() { if (!__ctfe) assert(false); + + import std.traits : MemberFunctionsTuple, FunctionTypeOf, + ReturnType, ParameterTypeTuple, ParameterIdentifierTuple; + import std.string : format; + import std.algorithm : canFind, startsWith; + import std.array : split; + + import vibe.utils.meta.codegen : cloneFunction; + import vibe.http.server : httpMethodString; string ret; - foreach( method; __traits(allMembers, I) ){ - foreach( overload; MemberFunctionsTuple!(I, method) ){ - alias FunctionTypeOf!overload FT; - alias ReturnType!FT RT; - alias ParameterTypeTuple!overload PTypes; - alias ParameterIdentifierTuple!overload ParamNames; - - enum meta = extractHTTPMethodAndName!(overload)(); - enum pathOverriden = meta[0]; - HTTPMethod httpVerb = meta[1]; - string url = meta[2]; + + foreach (method; __traits(allMembers, I)) { + foreach (overload; MemberFunctionsTuple!(I, method)) { + + alias FT = FunctionTypeOf!overload; + alias RT = ReturnType!FT; + alias PTT = ParameterTypeTuple!overload; + alias ParamNames = ParameterIdentifierTuple!overload; + + enum meta = extractHTTPMethodAndName!overload(); // NB: block formatting is coded in dependency order, not in 1-to-1 code flow order - static if( is(RT == interface) ){ + static if (is(RT == interface)) { ret ~= format( q{ override %s { @@ -743,23 +916,30 @@ private string generateRestInterfaceMethods(I)() RT.stringof ); } else { - string paramHandlingStr; - string urlPrefix = `""`; + string param_handling_str; + string url_prefix = `""`; // Block 2 - foreach( i, PT; PTypes ){ - static assert(ParamNames[i].length, format("Parameter %s of %s has no name.", i, method)); + foreach (i, PT; PTT){ + static assert ( + ParamNames[i].length, + format( + "Parameter %s of %s has no name.", + i, + method + ) + ); // legacy :id special case, left for backwards-compatibility reasons - static if( i == 0 && ParamNames[0] == "id" ){ - static if( is(PT == Json) ) - urlPrefix = q{urlEncode(id.toString())~"/"}; + static if (i == 0 && ParamNames[0] == "id") { + static if (is(PT == Json)) + url_prefix = q{urlEncode(id.toString())~"/"}; else - urlPrefix = q{urlEncode(toRestString(serializeToJson(id)))~"/"}; + url_prefix = q{urlEncode(toRestString(serializeToJson(id)))~"/"}; } - else static if( !ParamNames[i].startsWith("_") ){ + else static if (!ParamNames[i].startsWith("_")) { // underscore parameters are sourced from the HTTPServerRequest.params map or from url itself - paramHandlingStr ~= format( + param_handling_str ~= format( q{ jparams__["%s"] = serializeToJson(%s); jparamsj__["%s"] = %s; @@ -773,45 +953,53 @@ private string generateRestInterfaceMethods(I)() } // Block 3 - string requestStr; + string request_str; - static if( !pathOverriden ){ - requestStr = format( + static if (!meta.hadPathUDA) { + request_str = format( q{ url__ = %s ~ adjustMethodStyle(url__, m_methodStyle); }, - urlPrefix + url_prefix ); } else { - auto parts = url.split("/"); - requestStr ~= `url__ = ""`; + auto parts = meta.url.split("/"); + request_str ~= `url__ = ""`; foreach (i, p; parts) { - if (i > 0) requestStr ~= `~"/"`; + if (i > 0) { + request_str ~= `~ "/"`; + } bool match = false; - if( p.startsWith(":") ){ + if (p.startsWith(":")) { foreach (pn; ParamNames) { if (pn.startsWith("_") && p[1 .. $] == pn[1 .. $]) { - requestStr ~= `~urlEncode(toRestString(serializeToJson(`~pn~`)))`; + request_str ~= format( + q{ ~ urlEncode(toRestString(serializeToJson(%s)))}, + pn + ); match = true; break; } } } - if (!match) requestStr ~= `~"`~p~`"`; + + if (!match) { + request_str ~= `~ "` ~ p ~ `"`; + } } - requestStr ~= ";\n"; + request_str ~= ";\n"; } - requestStr ~= format( + request_str ~= format( q{ auto jret__ = request("%s", url__ , jparams__, jparamsj__); }, - httpMethodString(httpVerb) + httpMethodString(meta.method) ); - static if (!is(ReturnType!overload == void)){ - requestStr ~= q{ + static if (!is(ReturnType!overload == void)) { + request_str ~= q{ typeof(return) ret__; deserializeJson(ret__, jret__); return ret__; @@ -830,9 +1018,9 @@ private string generateRestInterfaceMethods(I)() } }, cloneFunction!overload, - url, - paramHandlingStr, - requestStr + meta.url, + param_handling_str, + request_str ); } } @@ -841,49 +1029,76 @@ private string generateRestInterfaceMethods(I)() return ret; } -private string toRestString(Json value) -{ - switch( value.type ){ - default: return value.toString(); - case Json.Type.Bool: return value.get!bool ? "true" : "false"; - case Json.Type.Int: return to!string(value.get!long); - case Json.Type.Float: return to!string(value.get!double); - case Json.Type.String: return value.get!string; +private { + import vibe.data.json; + import std.conv : to; + + string toRestString(Json value) + { + switch( value.type ){ + default: return value.toString(); + case Json.Type.Bool: return value.get!bool ? "true" : "false"; + case Json.Type.Int: return to!string(value.get!long); + case Json.Type.Float: return to!string(value.get!double); + case Json.Type.String: return value.get!string; + } + } + + T fromRestString(T)(string value) + { + static if( is(T == bool) ) return value == "true"; + else static if( is(T : int) ) return to!T(value); + else static if( is(T : double) ) return to!T(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); + else static if( is(T : string) ) return value; + else static if( __traits(compiles, T.fromString("hello")) ) return T.fromString(value); + else return deserializeJson!T(parseJson(value)); } } -private T fromRestString(T)(string value) +// concatenates two URL parts avoiding any duplicate slashes +// in resulting URL. `trailing` defines of result URL must +// end with slash +private string concatURL(string prefix, string url, bool trailing = false) { - static if( is(T == bool) ) return value == "true"; - else static if( is(T : int) ) return to!T(value); - else static if( is(T : double) ) return to!T(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); - else static if( is(T : string) ) return value; - else static if( __traits(compiles, T.fromString("hello")) ) return T.fromString(value); - else return deserializeJson!T(parseJson(value)); + import std.algorithm : startsWith, endsWith; + + // "/" is ASCII, so can just slice + auto pre = prefix.endsWith("/") ? prefix[0..$-1] : prefix; + auto post = url.startsWith("/") ? url : ( "/" ~ url ); + if (trailing) { + return post.endsWith("/") ? (pre ~ post) : (pre ~ post ~ "/"); + } + else { + return post.endsWith("/") ? (pre ~ post[0..$-1]) : (pre ~ post); + } } private string generateModuleImports(I)() { if( !__ctfe ) assert(false); - auto modules = getRequiredImports!I(); - import std.algorithm; + + import vibe.utils.meta.codegen : getRequiredImports; + import std.algorithm : map; + import std.array : join; + + auto modules = getRequiredImports!I(); return join(map!(a => "static import " ~ a ~ ";")(modules), "\n"); } version(unittest) { - private struct Aggregate { } - private interface Interface - { - Aggregate[] foo(); - } + private struct Aggregate { } + private interface Interface + { + Aggregate[] foo(); + } } unittest { - enum imports = generateModuleImports!Interface; - static assert(imports == "static import vibe.http.rest;"); + enum imports = generateModuleImports!Interface; + static assert(imports == "static import vibe.http.rest;"); } /** @@ -960,10 +1175,23 @@ struct OverridenPath * HTTPMethod extracted * url path extracted */ -private Tuple!(bool, HTTPMethod, string) extractHTTPMethodAndName(alias Func)() +private auto extractHTTPMethodAndName(alias Func)() { if (!__ctfe) assert(false); + + struct HandlerMeta + { + bool hadPathUDA; + HTTPMethod method; + string url; + } + + import vibe.utils.meta.uda : extractUda; + import vibe.utils.meta.traits : isPropertySetter, + isPropertyGetter; + import std.algorithm : startsWith; + import std.typecons : Nullable; immutable httpMethodPrefixes = [ HTTPMethod.GET : [ "get", "query" ], @@ -985,32 +1213,37 @@ private Tuple!(bool, HTTPMethod, string) extractHTTPMethodAndName(alias Func)() enum uda1 = extractUda!(vibe.http.rest.OverridenMethod, Func); enum uda2 = extractUda!(vibe.http.rest.OverridenPath, Func); - static if (!is(typeof(uda1) == typeof(null))) + static if (!is(typeof(uda1) == typeof(null))) { udmethod = uda1; - static if (!is(typeof(uda2) == typeof(null))) + } + static if (!is(typeof(uda2) == typeof(null))) { udurl = uda2; + } // Everything is overriden, no further analysis needed - if (!udmethod.isNull() && !udurl.isNull()) - return tuple(true, udmethod.get(), udurl.get()); + if (!udmethod.isNull() && !udurl.isNull()) { + return HandlerMeta(true, udmethod.get(), udurl.get()); + } // Anti-copy-paste delegate typeof(return) udaOverride( HTTPMethod method, string url ){ - return tuple( + return HandlerMeta( !udurl.isNull(), udmethod.isNull() ? method : udmethod.get(), udurl.isNull() ? url : udurl.get() ); } - if (isPropertyGetter!T) + if (isPropertyGetter!T) { return udaOverride(HTTPMethod.GET, name); - else if(isPropertySetter!T) + } + else if(isPropertySetter!T) { return udaOverride(HTTPMethod.PUT, name); + } else { - foreach( method, prefixes; httpMethodPrefixes ){ - foreach (prefix; prefixes){ - if( name.startsWith(prefix) ){ + foreach (method, prefixes; httpMethodPrefixes) { + foreach (prefix; prefixes) { + if (name.startsWith(prefix)) { string tmp = name[prefix.length..$]; return udaOverride(method, tmp); } @@ -1042,25 +1275,25 @@ unittest } enum ret1 = extractHTTPMethodAndName!(Sample.getInfo); - static assert (ret1[0] == false); - static assert (ret1[1] == HTTPMethod.GET); - static assert (ret1[2] == "Info"); + static assert (ret1.hadPathUDA == false); + static assert (ret1.method == HTTPMethod.GET); + static assert (ret1.url == "Info"); enum ret2 = extractHTTPMethodAndName!(Sample.updateDescription); - static assert (ret2[0] == false); - static assert (ret2[1] == HTTPMethod.PATCH); - static assert (ret2[2] == "Description"); + static assert (ret2.hadPathUDA == false); + static assert (ret2.method == HTTPMethod.PATCH); + static assert (ret2.url == "Description"); enum ret3 = extractHTTPMethodAndName!(Sample.putInfo); - static assert (ret3[0] == false); - static assert (ret3[1] == HTTPMethod.DELETE); - static assert (ret3[2] == "Info"); + static assert (ret3.hadPathUDA == false); + static assert (ret3.method == HTTPMethod.DELETE); + static assert (ret3.url == "Info"); enum ret4 = extractHTTPMethodAndName!(Sample.getMattersnot); - static assert (ret4[0] == true); - static assert (ret4[1] == HTTPMethod.GET); - static assert (ret4[2] == "matters"); + static assert (ret4.hadPathUDA == true); + static assert (ret4.method == HTTPMethod.GET); + static assert (ret4.url == "matters"); enum ret5 = extractHTTPMethodAndName!(Sample.mattersnot); - static assert (ret5[0] == true); - static assert (ret5[1] == HTTPMethod.POST); - static assert (ret5[2] == "compound/path"); + static assert (ret5.hadPathUDA == true); + static assert (ret5.method == HTTPMethod.POST); + static assert (ret5.url == "compound/path"); } struct RootPath From 0afdcba73854f61fac3c431225101f5e2ad13d92 Mon Sep 17 00:00:00 2001 From: Dicebot Date: Thu, 17 Oct 2013 23:04:55 +0200 Subject: [PATCH 5/9] vibe.utils.meta.funcattr : add IsAttributedParameter --- source/vibe/utils/meta/funcattr.d | 60 +++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/source/vibe/utils/meta/funcattr.d b/source/vibe/utils/meta/funcattr.d index 222d0c5d37..c83cac38ac 100644 --- a/source/vibe/utils/meta/funcattr.d +++ b/source/vibe/utils/meta/funcattr.d @@ -110,6 +110,62 @@ unittest @after!filter() int foo() { return 42; } } +/** + Checks if parameter is calculated by one of attached + functions. + + Params: + Function = function symbol to query for attributes + name = parameter name to check + + Returns: + `true` if it is calculated +*/ +template IsAttributedParameter(alias Function, string name) +{ + import std.traits : FunctionTypeOf; + + static assert (is(FunctionTypeOf!Function)); + + private { + alias Data = AttributedParameterMetadata!Function; + + template Impl(T...) + { + static if (T.length == 0) { + enum Impl = false; + } + else { + static if (T[0].name == name) { + enum Impl = true; + } + else { + enum Impl = Impl!(T[1..$]); + } + } + } + } + + enum IsAttributedParameter = Impl!Data; +} + +/// +unittest +{ + int foo() + { + return 42; + } + + @before!foo("name1") + void bar(int name1, double name2) + { + } + + static assert (IsAttributedParameter!(bar, "name1")); + static assert (!IsAttributedParameter!(bar, "name2")); + static assert (!IsAttributedParameter!(bar, "oops")); +} // internal attribute definitions private { @@ -390,11 +446,11 @@ private { */ struct AttributedFunction(alias Function, alias StoredArgTypes) { - import std.traits : isSomeFunction, ReturnType; + import std.traits : isSomeFunction, ReturnType, FunctionTypeOf; import vibe.utils.meta.typetuple : Group, isGroup; static assert (isGroup!StoredArgTypes); - static assert (isSomeFunction!(typeof(Function))); + static assert (is(FunctionTypeOf!Function)); /** Stores argument tuple for attached function calls From 75ccc90b814a566b397163a866e1e4362579580c Mon Sep 17 00:00:00 2001 From: Dicebot Date: Fri, 18 Oct 2013 17:27:10 +0200 Subject: [PATCH 6/9] `funcattr` adjustments for usage with rest module 1) Add new calling overload in AttributedFunction so that it won't try merging parameter and attribute type tuples when incoming and target parameter type tuples match exactly. In that cases relevant parameters are simply overwritten by attached attribute functions. 2) Add diagnostic that all attached InputAttribute functions conform to stored argument list. 3) static imports added for qualified symbols --- source/vibe/utils/meta/funcattr.d | 101 ++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/source/vibe/utils/meta/funcattr.d b/source/vibe/utils/meta/funcattr.d index c83cac38ac..6dab01dc92 100644 --- a/source/vibe/utils/meta/funcattr.d +++ b/source/vibe/utils/meta/funcattr.d @@ -226,6 +226,8 @@ private { int index; // fully qualified return type of attached function string type; + // for non-basic types - module to import + string origin; } /** @@ -243,7 +245,7 @@ private { { import std.typetuple : Filter, staticMap, staticIndexOf; import std.traits : ParameterIdentifierTuple, ReturnType, - fullyQualifiedName; + fullyQualifiedName, moduleName; private alias attributes = Filter!( isInputAttribute, @@ -265,11 +267,19 @@ private { "hook functions attached for usage with `AttributedFunction` " ~ "must have a return type" ); + + static if (is(typeof(moduleName!(ReturnType!(attribute.evaluator))))) { + enum origin = moduleName!(ReturnType!(attribute.evaluator)); + } + else { + enum origin = ""; + } enum BuildParameter = Parameter( name, staticIndexOf!(name, parameter_names), - fullyQualifiedName!(ReturnType!(attribute.evaluator)) + fullyQualifiedName!(ReturnType!(attribute.evaluator)), + origin ); import std.string : format; @@ -354,6 +364,9 @@ private { enum Parameter meta = ParameterMeta.expand[0]; static assert (meta.index <= ParameterList.expand.length); + static if (meta.origin.length) { + mixin("static import " ~ meta.origin ~ ";"); + } mixin("alias type = " ~ meta.type ~ ";"); alias PartialResult = Group!( @@ -446,8 +459,11 @@ private { */ struct AttributedFunction(alias Function, alias StoredArgTypes) { - import std.traits : isSomeFunction, ReturnType, FunctionTypeOf; - import vibe.utils.meta.typetuple : Group, isGroup; + import std.traits : isSomeFunction, ReturnType, FunctionTypeOf, + ParameterTypeTuple, ParameterIdentifierTuple; + import vibe.utils.meta.typetuple : Group, isGroup, Compare; + import std.functional : toDelegate; + import std.typetuple : Filter; static assert (isGroup!StoredArgTypes); static assert (is(FunctionTypeOf!Function)); @@ -486,6 +502,23 @@ struct AttributedFunction(alias Function, alias StoredArgTypes) ReturnType!Function result; } + // check that all attached functions have conforming argument lists + foreach (uda; input_attributes) { + static assert ( + Compare!( + Group!(ParameterTypeTuple!(uda.evaluator)), + StoredArgTypes + ), + format( + "Input attribute function '%s%s' argument list " ~ + "does not match provided argument list %s", + fullyQualifiedName!(uda.evaluator), + ParameterTypeTuple!(uda.evaluator).stringof, + StoredArgTypes.expand.stringof + ) + ); + } + static if (hasReturnType) { result = prepareInputAndCall(dg, args); } @@ -499,8 +532,7 @@ struct AttributedFunction(alias Function, alias StoredArgTypes) ); static if (output_attributes.length) { - import vibe.utils.meta.typetuple : Compare; - import std.traits : fullyQualifiedName, ParameterTypeTuple; + import std.traits : fullyQualifiedName; import std.string : format; import std.typetuple : TypeTuple; @@ -544,10 +576,6 @@ struct AttributedFunction(alias Function, alias StoredArgTypes) } private { - import std.functional : toDelegate; - import std.traits : ParameterTypeTuple, ParameterIdentifierTuple; - import std.typetuple : Filter; - // used as an argument tuple when function attached // to InputAttribute is called StoredArgTypes.expand m_storedArgs; @@ -582,6 +610,7 @@ struct AttributedFunction(alias Function, alias StoredArgTypes) proxies return value of dg */ ReturnType!Function prepareInputAndCall(T...)(FunctionDg dg, T args) + if (!Compare!(Group!T, Group!(ParameterTypeTuple!Function))) { alias attributed_parameters = AttributedParameterMetadata!Function; // calculated combined input type list @@ -590,7 +619,6 @@ struct AttributedFunction(alias Function, alias StoredArgTypes) Group!T ); - import vibe.utils.meta.typetuple : Compare; import std.traits : fullyQualifiedName; import std.string : format; @@ -608,8 +636,7 @@ struct AttributedFunction(alias Function, alias StoredArgTypes) // this value tuple will be used to assemble argument list Input input; - foreach (i, uda; input_attributes) - { + foreach (i, uda; input_attributes) { // each iteration cycle is responsible for initialising `input` // tuple from previous spot to current attributed parameter index // (including) @@ -650,6 +677,24 @@ struct AttributedFunction(alias Function, alias StoredArgTypes) return dg(input); } + + /** + `prepareInputAndCall` overload that operates on argument tuple that exactly + matches attributed function argument list and thus gets updated by + attached function instead of being merged with it + */ + ReturnType!Function prepareInputAndCall(T...)(FunctionDg dg, T args) + if (Compare!(Group!T, Group!(ParameterTypeTuple!Function))) + { + alias attributed_parameters = AttributedParameterMetadata!Function; + + foreach (i, uda; input_attributes) { + enum index = attributed_parameters[i].index; + args[index] = uda.evaluator(m_storedArgs); + } + + return dg(args); + } } } @@ -685,6 +730,36 @@ unittest assert(result == (1020 + 42 + 1020 + to!int(13.5)) * 2); } +// testing other prepareInputAndCall overload +unittest +{ + import std.conv; + + static string evaluator(string left, string right) + { + return left ~ right; + } + + // all attribute function must accept same stored parameters + static int modificator(int result, string unused1, string unused2) + { + return result * 2; + } + + @before!evaluator("a") @before!evaluator("c") @after!modificator() + static int sum(string a, int b, string c, double d) + { + return to!int(a) + to!int(b) + to!int(c) + to!int(d); + } + + auto funcattr = createAttributedFunction!sum("10", "20"); + + // `a` and `c` are expected to be simply overwritten + int result = funcattr("1000", 42, "1000", 13.5); + + assert(result == (1020 + 42 + 1020 + to!int(13.5)) * 2); +} + /** Syntax sugar in top of AttributedFunction From 19c4d3520f6e883f1583720a2e61f2d18bd2df96 Mon Sep 17 00:00:00 2001 From: Dicebot Date: Fri, 18 Oct 2013 17:34:28 +0200 Subject: [PATCH 7/9] vibe.http.rest : add `funcattr` support --- source/vibe/http/rest.d | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/source/vibe/http/rest.d b/source/vibe/http/rest.d index 0abd8e8569..3e154030e9 100644 --- a/source/vibe/http/rest.d +++ b/source/vibe/http/rest.d @@ -265,7 +265,7 @@ class RestInterfaceClient(I) : I //pragma(msg, generateModuleImports!(I)()); #line 1 "module imports" mixin(generateModuleImports!I()); -#line 243 +#line 244 import vibe.inet.url : URL, PathEntry; import vibe.http.client : HTTPClientRequest; @@ -315,7 +315,7 @@ class RestInterfaceClient(I) : I #line 1 "subinterface instances" mixin (generateRestInterfaceSubInterfaceInstances!I()); -#line 293 +#line 294 } /** @@ -331,7 +331,7 @@ class RestInterfaceClient(I) : I m_requestFilter = v; #line 1 "request filter" mixin (generateRestInterfaceSubInterfaceRequestFilter!I()); -#line 309 +#line 310 } //pragma(msg, "subinterfaces:"); @@ -343,7 +343,7 @@ class RestInterfaceClient(I) : I //pragma(msg, generateRestInterfaceMethods!(I)()); #line 1 "restinterface" mixin (generateRestInterfaceMethods!I()); -#line 337 "source/vibe/http/rest.d" +#line 322 "source/vibe/http/rest.d" protected { import vibe.data.json : Json; @@ -619,6 +619,7 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func import vibe.http.server : HTTPServerRequest, HTTPServerResponse; import vibe.http.common : HTTPStatusException, HTTPStatus; import vibe.utils.string : sanitizeUTF8; + import vibe.utils.meta.funcattr : IsAttributedParameter; alias PT = ParameterTypeTuple!Func; alias RT = ReturnType!Func; @@ -639,6 +640,11 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func ) ); + // will be re-written by UDA function anyway + static if (IsAttributedParameter!(Func, ParamNames[i])) { + continue; + } + static if (i == 0 && ParamNames[i] == "id") { // legacy special case for :id, backwards-compatibility logDebug("id %s", req.params["id"]); @@ -658,7 +664,7 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func alias DefVal = ParamDefaults[i]; if (req.method == HTTPMethod.GET ) { logDebug("query %s of %s" ,ParamNames[i], req.query); - + static if (is (DefVal == void)) { enforce( ParamNames[i] in req.query, @@ -670,6 +676,7 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func continue; } } + params[i] = fromRestString!P(req.query[ParamNames[i]]); } else { logDebug("%s %s", method, ParamNames[i]); @@ -705,11 +712,15 @@ private HTTPServerRequestDelegate jsonMethodHandler(T, string method, alias Func } try { + import vibe.utils.meta.funcattr; + + auto handler = createAttributedFunction!Func(req, res); + static if (is(RT == void)) { - __traits(getMember, inst, method)(params); + handler(&__traits(getMember, inst, method), params); res.writeJsonBody(Json.emptyObject); } else { - auto ret = __traits(getMember, inst, method)(params); + auto ret = handler(&__traits(getMember, inst, method), params); res.writeJsonBody(serializeToJson(ret)); } } catch (HTTPStatusException e) { @@ -889,6 +900,7 @@ private string generateRestInterfaceMethods(I)() import std.array : split; import vibe.utils.meta.codegen : cloneFunction; + import vibe.utils.meta.funcattr : IsAttributedParameter; import vibe.http.server : httpMethodString; string ret; @@ -937,7 +949,10 @@ private string generateRestInterfaceMethods(I)() else url_prefix = q{urlEncode(toRestString(serializeToJson(id)))~"/"}; } - else static if (!ParamNames[i].startsWith("_")) { + else static if ( + !ParamNames[i].startsWith("_") && + !IsAttributedParameter!(overload, ParamNames[i]) + ) { // underscore parameters are sourced from the HTTPServerRequest.params map or from url itself param_handling_str ~= format( q{ From 63b7f0d1366830b0fcde87534f0bd7368fb886cf Mon Sep 17 00:00:00 2001 From: Dicebot Date: Fri, 18 Oct 2013 17:35:16 +0200 Subject: [PATCH 8/9] Add `funcattr` test case to vibe.http.rest suite Also makes test suite exit upon both success and failure --- examples/rest/source/app.d | 75 +++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/examples/rest/source/app.d b/examples/rest/source/app.d index c0860aadcf..3e6b4fab6b 100644 --- a/examples/rest/source/app.d +++ b/examples/rest/source/app.d @@ -254,6 +254,69 @@ unittest assert (routes[HTTPMethod.GET][0].pattern == "/example4_api/:param/:another_param/data"); } +/* It is possible to attach function hooks to methods via User-Define Attributes. + * + * Such hook must be a free function that + * 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. + * + * There is also another attribute function type that can be called + * to post-process method return value. + * + * Refer to `vibe.utils.meta.funcattr` for more details. + */ +@rootPathFromName +interface Example5API +{ + import vibe.utils.meta.funcattr; + + @before!authenticate("user") @after!addBrackets() + string getSecret(int num, User user); +} + +User authenticate(HTTPServerRequest req, HTTPServerResponse res) +{ + return User("admin", true); +} + +struct User +{ + string name; + bool authorized; +} + +string addBrackets(string result, HTTPServerRequest, HTTPServerResponse) +{ + return "{" ~ result ~ "}"; +} + +class Example5 : Example5API +{ + string getSecret(int num, User user) + { + import std.conv : to; + import std.string : format; + + if (!user.authorized) + return ""; + + return format("secret #%s for %s", num, user.name); + } +} + +unittest +{ + auto router = new URLRouter; + registerRestInterface(router, new Example5()); + auto routes = router.getAllRoutes(); + + assert (routes[HTTPMethod.GET][0].pattern == "/example5_api/secret"); +} + shared static this() { // Registering our REST services in router @@ -264,6 +327,7 @@ shared static this() // naming style is default again, those can be router path specific. registerRestInterface(routes, new Example3()); registerRestInterface(routes, new Example4()); + registerRestInterface(routes, new Example5()); auto settings = new HTTPServerSettings(); settings.port = 8080; @@ -279,6 +343,9 @@ shared static this() * will always stay in sync. Care about method style naming convention mismatch though. */ setTimer(dur!"seconds"(1), { + scope(exit) + exitEventLoop(true); + logInfo("Starting communication with REST interface. Use capture tool (i.e. wireshark) to check how it looks on HTTP protocol level"); // Example 1 { @@ -312,7 +379,13 @@ shared static this() api.myNameDoesNotMatter(); assert(api.getParametersInURL("20", "30") == 50); } + // Example 5 + { + auto api = new RestInterfaceClient!Example5API("http://127.0.0.1:8080"); + auto secret = api.getSecret(42, User.init); + assert(secret == "{secret #42 for admin}"); + } + logInfo("Success."); - exitEventLoop(true); }); } From 84eabf8622c812e9562e874a9cbfd51d324b5001 Mon Sep 17 00:00:00 2001 From: Dicebot Date: Mon, 21 Oct 2013 17:30:59 +0200 Subject: [PATCH 9/9] Remove #line static assert --- source/vibe/core/concurrency.d | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/vibe/core/concurrency.d b/source/vibe/core/concurrency.d index f45d3ebf61..692d788195 100644 --- a/source/vibe/core/concurrency.d +++ b/source/vibe/core/concurrency.d @@ -336,8 +336,7 @@ private struct IsolatedRef(T) //mixin isolatedAggregateMethods!T; #line 1 "isolatedAggregateMethodsString" mixin(isolatedAggregateMethodsString!T()); - #line 340 "source/vibe/concurrency.d" - static assert(__LINE__ == 336); + #line 340 "source/vibe/core/concurrency.d" @disable this(this);