diff --git a/examples/rest/source/app.d b/examples/rest/source/app.d index ad654246fe..3e6b4fab6b 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. @@ -136,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 @@ -166,12 +191,23 @@ class Example3 : Example3API class Example3Nested : Example3APINested { override: - int getNumber() + int getNumber(int def_arg) { - return 42; + return def_arg; } } +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 +244,78 @@ 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"); +} + +/* 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() { @@ -219,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; @@ -234,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 { @@ -259,6 +371,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 { @@ -266,6 +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."); }); } 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); diff --git a/source/vibe/http/rest.d b/source/vibe/http/rest.d index 7c0df50507..3e154030e9 100644 --- a/source/vibe/http/rest.d +++ b/source/vibe/http/rest.d @@ -7,26 +7,14 @@ */ 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; /** - 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,324 +39,429 @@ import std.typetuple; Any interface that you return from a getter will be made available with the base url and its name appended. - Examples: - - 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(); - - @property string greeting(); - @property void greeting(string text); - - void addNewUser(string name); - @property string[] users(); - string getName(int id); - - @property IMyItemsApi items(); - } - - class MyItemsApiImpl : IMyItemsApi { - string getText() { return "Hello, World"; } - int getIndex(int id) { return id; } - } - - 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 string greeting() { return m_greeting; } - @property void greeting(string text) { m_greeting = text; } - - 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; } - } - - static this() - { - auto routes = new URLRouter; - - registerRestInterface(routes, new MyApiImpl, "/api/"); - - listenHTTP(new HTTPServerSettings, routes); - } - --- - See_Also: RestInterfaceClient class for a seamless way to acces such a generated API + */ -void registerRestInterface(TImpl)(URLRouter router, TImpl instance, string urlPrefix, +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()); + logDiagnostic( + "REST route: %s %s %s", + httpVerb, + url, + params.filter!(p => !p.startsWith("_") && p != "id")().array() + ); } - alias T = baseInterface!TImpl; + alias I = 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)(), urlPrefix ~ url ~ "/"); + 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 { - auto handler = jsonMethodHandler!(T, method, overload)(instance); - string id_supplement; - size_t skip = 0; + // normal handler + auto handler = jsonMethodHandler!(I, method, overload)(instance); + + string[] params = [ ParameterIdentifierTuple!overload ]; + // 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 (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) { - alias T = baseInterface!TImpl; - enum uda = extractUda!(RootPath, T); + // 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!T(router, instance, "/", style); + registerRestInterface!I(router, instance, "/", style); else { static if (uda.data == "") { - auto path = "/" ~ adjustMethodStyle(T.stringof, style) ~ "/"; - registerRestInterface!T(router, instance, path, style); + auto path = "/" ~ adjustMethodStyle(I.stringof, style); + registerRestInterface!I(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); + registerRestInterface!I( + router, + instance, + concatURL("/", uda.data), + style + ); } } } +/// 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. -/** - Implements the given interface by forwarding all public methods to a REST server. + // all details related to HTTP are inferred from + // interface declaration - The server must talk the same protocol as registerRestInterface() generates. Be sure to set - 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. + interface IMyAPI + { + // GET /api/greeting + @property string greeting(); + + // PUT /api/greeting + @property void greeting(string text); - Examples: + // 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 - An example client that accesses the API defined in the registerRestInterface() example: + class API : IMyAPI + { + private { + string m_greeting; + string[] m_users; + } + + @property string greeting() + { + return m_greeting; + } - --- - import vibe.d; + @property void greeting(string text) + { + m_greeting = text; + } - interface IMyApi { - string getStatus(); + void addNewUser(string name) + { + m_users ~= name; + } - @property string greeting(); - @property void greeting(string text); - - void addNewUser(string name); - @property string[] users(); - string getName(int id); + @property string[] users() + { + return m_users; } - static this() + string getName(int id) { - 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); + return m_users[id]; } - --- + } + + // actual usage, this is usually done in app.d module + // constructor + + void static_this() + { + import vibe.http.server, vibe.http.router; + + 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) +{ + 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 ); + if (trailing) { + return post.endsWith("/") ? (pre ~ post) : (pre ~ post ~ "/"); + } + else { + return post.endsWith("/") ? (pre ~ post[0..$-1]) : (pre ~ post); + } +} + +/** + Implements the given interface by forwarding all public methods to a REST server. + + The server must talk the same protocol as registerRestInterface() generates. Be sure to set + 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. */ class RestInterfaceClient(I) : I { //pragma(msg, "imports for "~I.stringof~":"); //pragma(msg, generateModuleImports!(I)()); +#line 1 "module imports" mixin(generateModuleImports!I()); +#line 244 + + 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 294 } - /** 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 310 } + //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 322 "source/vibe/http/rest.d" + + protected { + import vibe.data.json : Json; + import vibe.textfilter.urlencode; -#line 307 "source/vibe/http/rest.d" -static assert(__LINE__ == 307); - 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); } /** @@ -379,24 +472,34 @@ unittest */ string adjustMethodStyle(string name, MethodStyle style) { - if (name.length == 0) return null; + if (!name.length) { + return ""; + } + 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; @@ -412,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 .. $]; } - if (i < name.length) ret ~= "_" ~ name[start .. $]; - return style == MethodStyle.lowerUnderscored ? std.string.toLower(ret) : std.string.toUpper(ret); + return style == MethodStyle.lowerUnderscored ? + std.string.toLower(ret) : std.string.toUpper(ret); } } @@ -457,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 @@ -496,70 +610,127 @@ 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; + import vibe.utils.meta.funcattr : IsAttributedParameter; + + 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 + ) + ); + + // 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"]); 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) ){ - __traits(getMember, inst, method)(params); + import vibe.utils.meta.funcattr; + + auto handler = createAttributedFunction!Func(req, res); + + static if (is(RT == void)) { + 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) { - 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 + ); } } @@ -571,27 +742,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 ); @@ -608,24 +794,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{ @@ -634,15 +829,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; } @@ -651,22 +847,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"; @@ -682,23 +892,32 @@ 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.utils.meta.funcattr : IsAttributedParameter; + 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 { @@ -709,23 +928,33 @@ 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("_") && + !IsAttributedParameter!(overload, ParamNames[i]) + ) { // 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; @@ -739,45 +968,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__; @@ -796,9 +1033,9 @@ private string generateRestInterfaceMethods(I)() } }, cloneFunction!overload, - url, - paramHandlingStr, - requestStr + meta.url, + param_handling_str, + request_str ); } } @@ -807,49 +1044,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;"); } /** @@ -926,10 +1190,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" ], @@ -951,32 +1228,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); } @@ -1008,25 +1290,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 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 diff --git a/source/vibe/utils/meta/funcattr.d b/source/vibe/utils/meta/funcattr.d index 222d0c5d37..6dab01dc92 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 { @@ -170,6 +226,8 @@ private { int index; // fully qualified return type of attached function string type; + // for non-basic types - module to import + string origin; } /** @@ -187,7 +245,7 @@ private { { import std.typetuple : Filter, staticMap, staticIndexOf; import std.traits : ParameterIdentifierTuple, ReturnType, - fullyQualifiedName; + fullyQualifiedName, moduleName; private alias attributes = Filter!( isInputAttribute, @@ -209,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; @@ -298,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!( @@ -390,11 +459,14 @@ private { */ struct AttributedFunction(alias Function, alias StoredArgTypes) { - import std.traits : isSomeFunction, ReturnType; - 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 (isSomeFunction!(typeof(Function))); + static assert (is(FunctionTypeOf!Function)); /** Stores argument tuple for attached function calls @@ -430,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); } @@ -443,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; @@ -488,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; @@ -526,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 @@ -534,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; @@ -552,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) @@ -594,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); + } } } @@ -629,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