From cc0d0a45405831f4313e26e30cad4431e3f66144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= <sludwig@rejectedsoftware.com> Date: Tue, 4 Aug 2015 22:26:41 +0200 Subject: [PATCH] Switch REST interface server to use RestInterface!T. --- source/vibe/web/rest.d | 297 ++++++++--------------------------------- 1 file changed, 58 insertions(+), 239 deletions(-) diff --git a/source/vibe/web/rest.d b/source/vibe/web/rest.d index 2ad5c16632..f5eeea62a0 100644 --- a/source/vibe/web/rest.d +++ b/source/vibe/web/rest.d @@ -18,6 +18,7 @@ import vibe.http.status : isSuccessCode; import vibe.internal.meta.uda; import vibe.inet.url; import vibe.inet.message : InetHeaderMap; +import vibe.web.internal.rest.common : RestInterface; import std.algorithm : startsWith, endsWith; import std.typetuple : anySatisfy, Filter; @@ -63,96 +64,26 @@ import std.traits; */ void registerRestInterface(TImpl)(URLRouter router, TImpl instance, RestInterfaceSettings settings = null) { - import std.traits : InterfacesTuple; - import vibe.internal.meta.uda : findFirstUDA; - - alias IT = InterfacesTuple!TImpl; - static assert (IT.length > 0 || is (TImpl == interface), - "Cannot registerRestInterface type '" ~ TImpl.stringof - ~ "' because it doesn't implement an interface"); - static if (IT.length > 1) - pragma(msg, "Type '" ~ TImpl.stringof ~ "' implements more than one interface: make sure the one describing the REST server is the first one"); - static if (is(TImpl == interface)) - alias I = TImpl; - else - alias I = IT[0]; - - static assert(getInterfaceValidationError!(I) is null, getInterfaceValidationError!(I)); - - if (!settings) settings = new RestInterfaceSettings; + import std.algorithm : filter, map; + import std.array : array; + import vibe.web.internal.rest.common : ParameterKind; - string url_prefix = settings.baseURL.path.toString(); + auto intf = RestInterface!TImpl(settings); - enum uda = findFirstUDA!(PathAttribute, I); - static if (uda.found) { - static if (uda.value.data == "") { - auto path = "/" ~ adjustMethodStyle(I.stringof, settings.methodStyle); - url_prefix = concatURL(url_prefix, path); - } else { - url_prefix = concatURL(url_prefix, uda.value.data); - } + foreach (i, T; intf.SubInterfaceTypes) { + enum fname = __traits(identifier, intf.SubInterfaceFunctions[i]); + router.registerRestInterface!T(__traits(getMember, instance, fname)(), intf.subInterfaces[i].settings); } - 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() - ); - } - - foreach (method; __traits(allMembers, I)) { - // WORKAROUND #1045 / @@BUG14375@@ - static if (method.length != 0) - foreach (overload; MemberFunctionsTuple!(I, method)) { + foreach (i, func; intf.RouteFunctions) { + auto route = intf.routes[i]; - enum meta = extractHTTPMethodAndName!(overload, false)(); + // normal handler + auto handler = jsonMethodHandler!(func, i)(instance, intf); - static if (meta.hadPathUDA) { - string url = meta.url; - } - else { - string url = adjustMethodStyle(stripTUnderscore(meta.url, settings), settings.methodStyle); - } - - 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!" - ); - auto subSettings = settings.dup; - subSettings.baseURL = URL(concatURL(url_prefix, url, true)); - registerRestInterface!RT( - router, - __traits(getMember, instance, method)(), - subSettings - ); - } else { - // normal handler - auto handler = jsonMethodHandler!(I, overload)(instance, settings); - - 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", true), - url); - addRoute(meta.method, combined_url, handler, params); - } else { - addRoute(meta.method, concatURL(url_prefix, url), handler, params); - } - } - } + auto diagparams = route.parameters.filter!(p => p.kind != ParameterKind.internal).map!(p => p.fieldName).array; + logDiagnostic("REST route: %s %s %s", route.method, route.fullPattern, diagparams); + router.match(route.method, route.fullPattern, handler); } } @@ -283,7 +214,6 @@ HTTPServerRequestDelegate serveRestJSClient(TImpl)(RestInterfaceSettings setting */ void generateRestJSClient(TImpl, R)(ref R output, RestInterfaceSettings settings = null) { - import vibe.web.internal.rest.common : RestInterface; import vibe.web.internal.rest.jsclient : generateInterface; output.generateInterface!TImpl(null, settings); } @@ -558,7 +488,7 @@ unittest class RestInterfaceSettings { /** The public URL below which the REST interface is registered. */ - URL baseURL; + URL baseURL = URL("http://api.example.com/"); /** Naming convention used for the generated URLs. */ @@ -612,179 +542,68 @@ class RestInterfaceSettings { * Returns: * A delegate suitable to use as an handler for an HTTP request. */ -private HTTPServerRequestDelegate jsonMethodHandler(T, alias Func)(T inst, RestInterfaceSettings settings) +private HTTPServerRequestDelegate jsonMethodHandler(alias Func, size_t ridx, T)(T inst, ref RestInterface!T intf) { import std.string : format; - import std.algorithm : startsWith; - + //import std.traits : ParameterIdentifierTuple; import vibe.http.server : HTTPServerRequest, HTTPServerResponse; import vibe.http.common : HTTPStatusException, HTTPStatus, enforceBadRequest; import vibe.utils.string : sanitizeUTF8; - import vibe.internal.meta.funcattr : IsAttributedParameter; - - alias PT = ParameterTypeTuple!Func; - alias RT = ReturnType!Func; - alias ParamDefaults = ParameterDefaultValueTuple!Func; - alias WPAT = UDATuple!(WebParamAttribute, Func); + import vibe.web.internal.rest.common : ParameterKind; + import vibe.internal.meta.funcattr : IsAttributedParameter, computeAttributedParameterCtx; + import vibe.textfilter.urlencode : urlDecode; enum Method = __traits(identifier, Func); - enum ParamNames = [ ParameterIdentifierTuple!Func ]; - enum FuncId = (fullyQualifiedName!T~ "." ~ Method); + alias PTypes = ParameterTypeTuple!Func; + enum PNames = [ParameterIdentifierTuple!Func]; + alias PDefaults = ParameterDefaultValueTuple!Func; + alias RT = ReturnType!(FunctionTypeOf!Func); + auto route = intf.routes[ridx]; void handler(HTTPServerRequest req, HTTPServerResponse res) { - PT params; - - foreach (i, P; PT) { - // will be re-written by UDA function anyway - static if (!IsAttributedParameter!(Func, ParamNames[i])) { - // Comparison template for anySatisfy - //template Cmp(WebParamAttribute attr) { enum Cmp = (attr.identifier == ParamNames[i]); } - alias CompareParamName = GenCmp!("Loop", i, ParamNames[i]); - mixin(CompareParamName.Decl); - // Find origin of parameter - 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 (anySatisfy!(mixin(CompareParamName.Name), WPAT)) { - // User anotated the origin of this parameter. - alias PWPAT = Filter!(mixin(CompareParamName.Name), WPAT); - // @headerParam. - static if (PWPAT[0].origin == WebParamAttribute.Origin.Header) { - // If it has no default value - static if (is (ParamDefaults[i] == void)) { - auto fld = enforceBadRequest(PWPAT[0].field in req.headers, - format("Expected field '%s' in header", PWPAT[0].field)); - } else { - auto fld = PWPAT[0].field in req.headers; - if (fld is null) { - params[i] = ParamDefaults[i]; - logDebug("No header param %s, using default value", PWPAT[0].identifier); - continue; - } - } - logDebug("Header param: %s <- %s", PWPAT[0].identifier, *fld); - params[i] = fromRestString!P(*fld); - } else static if (PWPAT[0].origin == WebParamAttribute.Origin.Query) { - // Note: Doesn't work if HTTPServerOption.parseQueryString is disabled. - static if (is (ParamDefaults[i] == void)) { - auto fld = enforceBadRequest(PWPAT[0].field in req.query, - format("Expected form field '%s' in query", PWPAT[0].field)); - } else { - auto fld = PWPAT[0].field in req.query; - if (fld is null) { - params[i] = ParamDefaults[i]; - logDebug("No query param %s, using default value", PWPAT[0].identifier); - continue; - } - } - logDebug("Query param: %s <- %s", PWPAT[0].identifier, *fld); - params[i] = fromRestString!P(*fld); - } else static if (PWPAT[0].origin == WebParamAttribute.Origin.Body) { - enforceBadRequest( - req.contentType == "application/json", - "The Content-Type header needs to be set to application/json." - ); - enforceBadRequest( - req.json.type != Json.Type.Undefined, - "The request body does not contain a valid JSON value." - ); - enforceBadRequest( - req.json.type == Json.Type.Object, - "The request body must contain a JSON object with an entry for each parameter." - ); - - auto par = req.json[PWPAT[0].field]; - static if (is(ParamDefaults[i] == void)) { - enforceBadRequest(par.type != Json.Type.Undefined, - format("Missing parameter %s", PWPAT[0].field) - ); - } else { - if (par.type == Json.Type.Undefined) { - logDebug("No body param %s, using default value", PWPAT[0].identifier); - params[i] = ParamDefaults[i]; - continue; - } - } - params[i] = deserializeJson!P(par); - logDebug("Body param: %s <- %s", PWPAT[0].identifier, par); - } else static assert (false, "Internal error: Origin "~to!string(PWPAT[0].origin)~" is not implemented."); - } else static if (ParamNames[i].startsWith("_")) { - import vibe.textfilter.urlencode; - // URL parameter - static if (ParamNames[i] != "_dummy") { - enforceBadRequest( - 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(urlDecode(req.params[ParamNames[i][1 .. $]])); - } - } else { - // normal parameter - alias DefVal = ParamDefaults[i]; - auto pname = stripTUnderscore(ParamNames[i], settings); - - if (req.method == HTTPMethod.GET) { - logDebug("query %s of %s", pname, req.query); - - static if (is (DefVal == void)) { - enforceBadRequest( - pname in req.query, - format("Missing query parameter '%s'", pname) - ); - } else { - if (pname !in req.query) { - params[i] = DefVal; - continue; - } - } + if (route.bodyParameters.length) { + logInfo("BODYPARAMS: %s %s", Method, route.bodyParameters.length); + /*enforceBadRequest(req.contentType == "application/json", + "The Content-Type header needs to be set to application/json.");*/ + enforceBadRequest(req.json.type != Json.Type.undefined, + "The request body does not contain a valid JSON value."); + enforceBadRequest(req.json.type == Json.Type.object, + "The request body must contain a JSON object with an entry for each parameter."); + } - params[i] = fromRestString!P(req.query[pname]); - } else { - logDebug("%s %s", FuncId, pname); - - enforceBadRequest( - req.contentType == "application/json", - "The Content-Type header needs to be set to application/json." - ); - enforceBadRequest( - req.json.type != Json.Type.Undefined, - "The request body does not contain a valid JSON value." - ); - enforceBadRequest( - req.json.type == Json.Type.Object, - "The request body must contain a JSON object with an entry for each parameter." - ); - - static if (is(DefVal == void)) { - auto par = req.json[pname]; - enforceBadRequest(par.type != Json.Type.Undefined, - format("Missing parameter %s", pname) - ); - params[i] = deserializeJson!P(par); - } else { - if (req.json[pname].type == Json.Type.Undefined) { - params[i] = DefVal; - continue; - } - } - } - } + PTypes params; + + foreach (i, PT; PTypes) { + auto pinfo = route.parameters[i]; + enum pname = PNames[i]; + Nullable!PT v; + final switch (pinfo.kind) { // TODO: make this a CT decision + case ParameterKind.query: v = fromRestString!PT(req.query[pinfo.fieldName]); break; + case ParameterKind.body_: v = deserializeJson!PT(req.json[pinfo.fieldName]); break; + case ParameterKind.header: v = fromRestString!PT(req.headers[pinfo.fieldName]); break; + case ParameterKind.attributed: + static if (IsAttributedParameter!(Func, pname)) { // Workaround for non-CT switch + v = computeAttributedParameterCtx!(Func, pname)(inst, req, res); + break; + } else assert(false); + case ParameterKind.internal: v = fromRestString!PT(urlDecode(req.params[pinfo.fieldName])); break; } + + if (v.isNull()) { + static if (!is(PDefaults[i] == void)) params[i] = PDefaults[i]; + else enforceBadRequest(false, "Missing non-optional "~pinfo.kind.to!string~" parameter '"~(pinfo.fieldName.length?pinfo.fieldName:pinfo.name)~"'."); + } else params[i] = v; } try { import vibe.internal.meta.funcattr; - auto handler = createAttributedFunction!Func(req, res); - static if (is(RT == void)) { - handler(&__traits(getMember, inst, Method), params); + __traits(getMember, inst, Method)(params); res.writeJsonBody(Json.emptyObject); } else { - auto ret = handler(&__traits(getMember, inst, Method), params); + auto ret = __traits(getMember, inst, Method)(params); res.writeJsonBody(ret); } } catch (HTTPStatusException e) {