From eab240db81218b97ff3a276f9aa97b887d037bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Thu, 3 Oct 2024 09:52:26 +0200 Subject: [PATCH] Add viaStatus REST interface attribute. Allows customizing the HTTP response status in a cleaner way than (ab)using an `after` annotation. --- tests/rest/source/app.d | 15 +++++++++++++ web/vibe/web/common.d | 20 +++++++++-------- web/vibe/web/internal/rest/common.d | 5 ++++- web/vibe/web/rest.d | 35 +++++++++++++++++++++-------- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/tests/rest/source/app.d b/tests/rest/source/app.d index 7905f4921..9b6383a45 100644 --- a/tests/rest/source/app.d +++ b/tests/rest/source/app.d @@ -395,6 +395,8 @@ interface Example6API // Without a parameter, it will represent the entire body string postConcatBody(@viaBody() FooType obj); + int testStatus(@viaStatus out int status, @viaStatus out string status_phrase); + struct FooType { int a; string s; @@ -440,6 +442,13 @@ override: { return postConcat(obj); } + + int testStatus(out int status, out string status_phrase) + { + status = HTTPStatus.accepted; + status_phrase = "Request accepted!"; + return 42; + } } unittest @@ -644,6 +653,12 @@ void runTests(string url) assert(tester == "The cake is a lie", tester); assert(www == `Basic realm="Aperture"`.nullable, www.to!string); } + + int status; + string status_phrase; + assert(api.testStatus(status, status_phrase) == 42); + assert(status == HTTPStatus.accepted); + assert(status_phrase == "Request accepted!"); } // Example 6 -- Query diff --git a/web/vibe/web/common.d b/web/vibe/web/common.d index ced186adf..9ead47cf5 100644 --- a/web/vibe/web/common.d +++ b/web/vibe/web/common.d @@ -11,6 +11,7 @@ import vibe.http.common; import vibe.http.server : HTTPServerRequest; import vibe.data.json; import vibe.internal.meta.uda : onlyAsUda, UDATuple; +import vibe.web.internal.rest.common : ParameterKind; import std.meta : AliasSeq; static import std.utf; @@ -686,8 +687,6 @@ package struct NoRouteAttribute {} * and the parameter (identifier) name of the function. */ public struct WebParamAttribute { - import vibe.web.internal.rest.common : ParameterKind; - /// The type of the WebParamAttribute ParameterKind origin; /// Parameter name (function parameter name). @@ -722,7 +721,6 @@ public struct WebParamAttribute { */ WebParamAttribute viaBody(string field = null) @safe { - import vibe.web.internal.rest.common : ParameterKind; if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__); return WebParamAttribute(ParameterKind.body_, null, field); @@ -735,7 +733,6 @@ in { } do { - import vibe.web.internal.rest.common : ParameterKind; if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__); return WebParamAttribute(ParameterKind.body_, identifier, field); @@ -744,7 +741,6 @@ do /// ditto WebParamAttribute bodyParam(string identifier) @safe { - import vibe.web.internal.rest.common : ParameterKind; if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__); return WebParamAttribute(ParameterKind.body_, identifier, ""); @@ -774,7 +770,6 @@ WebParamAttribute bodyParam(string identifier) */ WebParamAttribute viaHeader(string field) @safe { - import vibe.web.internal.rest.common : ParameterKind; if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__); return WebParamAttribute(ParameterKind.header, null, field); @@ -783,7 +778,6 @@ WebParamAttribute viaHeader(string field) /// Ditto WebParamAttribute headerParam(string identifier, string field) @safe { - import vibe.web.internal.rest.common : ParameterKind; if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__); return WebParamAttribute(ParameterKind.header, identifier, field); @@ -813,7 +807,6 @@ WebParamAttribute headerParam(string identifier, string field) */ WebParamAttribute viaQuery(string field) @safe { - import vibe.web.internal.rest.common : ParameterKind; if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__); return WebParamAttribute(ParameterKind.query, null, field); @@ -822,12 +815,21 @@ WebParamAttribute viaQuery(string field) /// Ditto WebParamAttribute queryParam(string identifier, string field) @safe { - import vibe.web.internal.rest.common : ParameterKind; if (!__ctfe) assert(false, onlyAsUda!__FUNCTION__); return WebParamAttribute(ParameterKind.query, identifier, field); } + +/** Declares a parameter to be transmitted via the HTTP status code or phrase. + + This attribute can be applied to one or two `out` parameters of type + `HTTPStatus`/`int` or `string`. The values of those parameters correspond + to the HTTP status code or phrase, depending on the type. +*/ +enum viaStatus = WebParamAttribute(ParameterKind.status); + + /** Determines the naming convention of an identifier. */ diff --git a/web/vibe/web/internal/rest/common.d b/web/vibe/web/internal/rest/common.d index 9351aa0cf..1566baecf 100644 --- a/web/vibe/web/internal/rest/common.d +++ b/web/vibe/web/internal/rest/common.d @@ -199,6 +199,7 @@ import std.traits : hasUDA; case ParameterKind.internal: route.internalParameters ~= pi; break; case ParameterKind.attributed: route.attributedParameters ~= pi; break; case ParameterKind.auth: route.authParameters ~= pi; break; + case ParameterKind.status: route.statusParameters ~= pi; break; } } @@ -437,6 +438,7 @@ struct Route { Parameter[] attributedParameters; Parameter[] internalParameters; Parameter[] authParameters; + Parameter[] statusParameters; } struct PathPart { @@ -474,7 +476,8 @@ enum ParameterKind { header, // req.header[] attributed, // @before internal, // req.params[] - auth // @authrorized!T + auth, // @authrorized!T + status // res.statusCode/res.statusPhrase } struct SubInterface { diff --git a/web/vibe/web/rest.d b/web/vibe/web/rest.d index e07e8b5f3..73665ff59 100644 --- a/web/vibe/web/rest.d +++ b/web/vibe/web/rest.d @@ -1623,13 +1623,20 @@ private HTTPServerRequestDelegate jsonMethodHandler(alias Func, size_t ridx, T)( handleCors(); foreach (i, P; PTypes) { static if (sroute.parameters[i].isOut) { - static assert (sroute.parameters[i].kind == ParameterKind.header); - static if (isInstanceOf!(Nullable, typeof(params[i]))) { - if (!params[i].isNull) + static if (sroute.parameters[i].kind == ParameterKind.header) { + static if (isInstanceOf!(Nullable, typeof(params[i]))) { + if (!params[i].isNull) + res.headers[route.parameters[i].fieldName] = to!string(params[i]); + } else { res.headers[route.parameters[i].fieldName] = to!string(params[i]); - } else { - res.headers[route.parameters[i].fieldName] = to!string(params[i]); - } + } + } else static if (sroute.parameters[i].kind == ParameterKind.status) { + static if (is(typeof(params[i]) == HTTPStatus) || is(typeof(params[i]) == int)) + res.statusCode = params[i]; + else static if (is(typeof(params[i]) == string)) + res.statusPhrase = params[i]; + else static assert(false, "`@viaStatus` parameters must be of type `HTTPStatus`/`int` or `string`"); + } else static assert (false, "`out` parameters must be annotated with either `@viaHeader` or `@viaStatus`"); } } } @@ -1855,6 +1862,8 @@ private auto executeClientMethod(I, size_t ridx, ARGS...) InetHeaderMap headers; InetHeaderMap reqhdrs; InetHeaderMap opthdrs; + HTTPStatus status_code; + string status_phrase; string url_prefix; @@ -1930,8 +1939,8 @@ private auto executeClientMethod(I, size_t ridx, ARGS...) foreach (i, PT; PTT) { enum sparam = sroute.parameters[i]; auto fieldname = route.parameters[i].fieldName; - static if (sparam.kind == ParameterKind.header) { - static if (sparam.isOut) { + static if (sparam.isOut) { + static if (sparam.kind == ParameterKind.header) { static if (isInstanceOf!(Nullable, PT)) { ARGS[i] = to!(TemplateArgsOf!PT)( opthdrs.get(fieldname, null)); @@ -1939,6 +1948,12 @@ private auto executeClientMethod(I, size_t ridx, ARGS...) if (auto ptr = fieldname in reqhdrs) ARGS[i] = to!PT(*ptr); } + } else static if (sparam.kind == ParameterKind.status) { + static if (is(PT == HTTPStatus) || is(PT == int)) { + ARGS[i] = status_code; + } else static if (is(PT == string)) { + ARGS[i] = status_phrase; + } } } } @@ -1964,6 +1979,8 @@ private auto executeClientMethod(I, size_t ridx, ARGS...) auto ret = request(URL(intf.baseURL), request_filter, request_body_filter, sroute.method, url, headers, query.data, body_, reqhdrs, opthdrs, intf.settings.httpClientSettings); + status_code = cast(HTTPStatus)ret.statusCode; + status_phrase = ret.statusPhrase; static if (is(RT == InputStream)) { return ret.bodyReader.asInterface!InputStream; @@ -2438,7 +2455,7 @@ do { static if (Attr.length != 1) { if (hack) return "%s: Parameter '%s' cannot be %s" .format(FuncId, PN[i], SC & PSC.out_ ? "out" : "ref"); - } else static if (Attr[0].origin != ParameterKind.header) { + } else static if (Attr[0].origin != ParameterKind.header && Attr[0].origin != ParameterKind.status) { if (hack) return "%s: %s parameter '%s' cannot be %s" .format(FuncId, Attr[0].origin, PN[i], SC & PSC.out_ ? "out" : "ref");