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) {