1 /**
2 	Internal module with common functionality for REST interface generators.
3 
4 	Copyright: © 2015-2016 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibe.web.internal.rest.common;
9 
10 import vibe.http.common : HTTPMethod;
11 import vibe.web.rest;
12 
13 import std.algorithm : endsWith, startsWith;
14 import std.meta : anySatisfy, Filter;
15 import std.traits : hasUDA;
16 
17 
18 /**
19 	Provides all necessary tools to implement an automated REST interface.
20 
21 	The given `TImpl` must be an `interface` or a `class` deriving from one.
22 */
23 /*package(vibe.web.web)*/ struct RestInterface(TImpl)
24 	if (is(TImpl == class) || is(TImpl == interface))
25 {
26 @safe:
27 
28 	import std.traits : FunctionTypeOf, InterfacesTuple, MemberFunctionsTuple,
29 		ParameterIdentifierTuple, ParameterStorageClass,
30 		ParameterStorageClassTuple, ParameterTypeTuple, ReturnType;
31 	import std.typetuple : TypeTuple;
32 	import vibe.inet.url : URL;
33 	import vibe.internal.meta.funcattr : IsAttributedParameter;
34 	import vibe.internal.meta.traits : derivedMethod;
35 	import vibe.internal.meta.uda;
36 
37 	/// The settings used to generate the interface
38 	RestInterfaceSettings settings;
39 
40 	/// Full base path of the interface, including an eventual `@path` annotation.
41 	string basePath;
42 
43 	/// Full base URL of the interface, including an eventual `@path` annotation.
44 	string baseURL;
45 
46 	// determine the implementation interface I and check for validation errors
47 	private alias BaseInterfaces = InterfacesTuple!TImpl;
48 	static assert (BaseInterfaces.length > 0 || is (TImpl == interface),
49 		       "Cannot registerRestInterface type '" ~ TImpl.stringof
50 		       ~ "' because it doesn't implement an interface");
51 	static if (BaseInterfaces.length > 1)
52 		pragma(msg, "Type '" ~ TImpl.stringof ~ "' implements more than one interface: make sure the one describing the REST server is the first one");
53 
54 
55 	static if (is(TImpl == interface))
56 		alias I = TImpl;
57 	else
58 		alias I = BaseInterfaces[0];
59 	static assert(getInterfaceValidationError!I is null, getInterfaceValidationError!(I));
60 
61 	/// The name of each interface member
62 	enum memberNames = [__traits(allMembers, I)];
63 
64 	/// Aliases to all interface methods
65 	alias AllMethods = GetAllMethods!();
66 
67 	/** Aliases for each route method
68 
69 		This tuple has the same number of entries as `routes`.
70 	*/
71 	alias RouteFunctions = GetRouteFunctions!();
72 
73 	enum routeCount = RouteFunctions.length;
74 
75 	/** Information about each route
76 
77 		This array has the same number of fields as `RouteFunctions`
78 	*/
79 	Route[routeCount] routes;
80 
81 	/// Static (compile-time) information about each route
82 	static if (routeCount) static const StaticRoute[routeCount] staticRoutes = computeStaticRoutes();
83 	else static const StaticRoute[0] staticRoutes;
84 
85 	/** Aliases for each sub interface method
86 
87 		This array has the same number of entries as `subInterfaces` and
88 		`SubInterfaceTypes`.
89 	*/
90 	alias SubInterfaceFunctions = GetSubInterfaceFunctions!();
91 
92 	/** The type of each sub interface
93 
94 		This array has the same number of entries as `subInterfaces` and
95 		`SubInterfaceFunctions`.
96 	*/
97 	alias SubInterfaceTypes = GetSubInterfaceTypes!();
98 
99 	enum subInterfaceCount = SubInterfaceFunctions.length;
100 
101 	/** Information about sub interfaces
102 
103 		This array has the same number of entries as `SubInterfaceFunctions` and
104 		`SubInterfaceTypes`.
105 	*/
106 	SubInterface[subInterfaceCount] subInterfaces;
107 
108 
109 	/** Fills the struct with information.
110 
111 		Params:
112 			settings = Optional settings object.
113 			is_client = Whether this struct is used by a client.
114 	*/
115 	this(RestInterfaceSettings settings, bool is_client)
116 	{
117 		import vibe.internal.meta.uda : findFirstUDA;
118 
119 		this.settings = settings ? settings.dup : new RestInterfaceSettings;
120 		if (this.settings.baseURL == URL.init && !is_client) {
121 			// use a valid dummy base URL to be able to construct sub-URLs
122 			// for nested interfaces
123 			this.settings.baseURL = URL("http://localhost/");
124 		}
125 		this.basePath = this.settings.baseURL.path.toString();
126 
127 		enum uda = findFirstUDA!(PathAttribute, I);
128 		static if (uda.found) {
129 			static if (uda.value.data == "") {
130 				auto path = "/" ~ adjustMethodStyle(I.stringof, this.settings.methodStyle);
131 				this.basePath = concatURL(this.basePath, path);
132 			} else {
133 				this.basePath = concatURL(this.basePath, uda.value.data);
134 			}
135 		}
136 
137 		if (this.settings.baseURL != URL.init)
138 		{
139 			URL bu = this.settings.baseURL;
140 			bu.pathString = this.basePath;
141 			this.baseURL = bu.toString();
142 		}
143 		else this.baseURL = this.basePath;
144 
145 		computeRoutes();
146 		computeSubInterfaces();
147 	}
148 
149 	// copying this struct is costly, so we forbid it
150 	@disable this(this);
151 
152 	private void computeRoutes()
153 	{
154 		import std.algorithm.searching : any;
155 
156 		foreach (si, RF; RouteFunctions) {
157 			enum sroute = staticRoutes[si];
158 			Route route;
159 			route.functionName = sroute.functionName;
160 			route.method = sroute.method;
161 
162 			static if (sroute.pathOverride) route.pattern = sroute.rawName;
163 			else route.pattern = computeDefaultPath!RF(sroute.rawName);
164 			route.method = sroute.method;
165 			extractPathParts(route.fullPathParts, this.basePath.endsWith("/") ? this.basePath : this.basePath ~ "/");
166 
167 			route.parameters.length = sroute.parameters.length;
168 
169 			bool prefix_id = false;
170 
171 			alias PT = ParameterTypeTuple!RF;
172 			foreach (i, _; PT) {
173 				enum sparam = sroute.parameters[i];
174 				Parameter pi;
175 				pi.name = sparam.name;
176 				pi.kind = sparam.kind;
177 				pi.isIn = sparam.isIn;
178 				pi.isOut = sparam.isOut;
179 
180 				static if (sparam.kind != ParameterKind.attributed && sparam.fieldName.length == 0) {
181 					pi.fieldName = stripTUnderscore(pi.name, settings);
182 				} else pi.fieldName = sparam.fieldName;
183 
184 				static if (i == 0 && sparam.name == "id") {
185 					prefix_id = true;
186 					if (route.pattern.length && route.pattern[0] != '/')
187 						route.pattern = '/' ~ route.pattern;
188 					route.pathParts ~= PathPart(true, "id");
189 					route.fullPathParts ~= PathPart(true, "id");
190 				}
191 
192 				route.parameters[i] = pi;
193 
194 				final switch (pi.kind) {
195 					case ParameterKind.query: route.queryParameters ~= pi; break;
196 					case ParameterKind.body_: route.bodyParameters ~= pi; break;
197 					case ParameterKind.wholeBody: route.wholeBodyParameter = pi; break;
198 					case ParameterKind.header: route.headerParameters ~= pi; break;
199 					case ParameterKind.internal: route.internalParameters ~= pi; break;
200 					case ParameterKind.attributed: route.attributedParameters ~= pi; break;
201 					case ParameterKind.auth: route.authParameters ~= pi; break;
202 				}
203 			}
204 
205 			extractPathParts(route.pathParts, route.pattern);
206 			extractPathParts(route.fullPathParts, !prefix_id && route.pattern.startsWith("/") ? route.pattern[1 .. $] : route.pattern);
207 			if (prefix_id) route.pattern = ":id" ~ route.pattern;
208 			route.fullPattern = concatURL(this.basePath, route.pattern);
209 			route.pathHasPlaceholders = route.fullPathParts.any!(p => p.isParameter);
210 
211 			routes[si] = route;
212 		}
213 	}
214 
215 	/** Returns an array with routes grouped by path pattern
216 	*/
217 	auto getRoutesGroupedByPattern()
218 	{
219 		static if (__VERSION__ < 2099) {
220 			return () @trusted {
221 				return getRoutesGroupedByPatternImpl(routes);
222 			} ();
223 		} else return getRoutesGroupedByPatternImpl(routes);
224 	}
225 
226 	private static StaticRoute[routeCount] computeStaticRoutes()
227 	{
228 		static import std.traits;
229 		import vibe.web.auth : AuthInfo;
230 		import std.algorithm.searching : any, count;
231 		import std.meta : AliasSeq;
232 		import std.traits : isMutable;
233 
234 		assert(__ctfe);
235 
236 		StaticRoute[routeCount] ret;
237 
238 		alias AUTHTP = AuthInfo!TImpl;
239 
240 		foreach (fi, func; RouteFunctions) {
241 			StaticRoute route;
242 			route.functionName = __traits(identifier, func);
243 
244 			static if (!is(TImpl == I))
245 				alias cfunc = derivedMethod!(TImpl, func);
246 			else
247 				alias cfunc = func;
248 
249 			alias FuncType = FunctionTypeOf!func;
250 			alias ParameterTypes = ParameterTypeTuple!FuncType;
251 			alias ReturnType = std.traits.ReturnType!FuncType;
252 			enum parameterNames = [ParameterIdentifierTuple!func];
253 
254 			enum meta = extractHTTPMethodAndName!(func, false)();
255 			route.method = meta.method;
256 			route.rawName = meta.url;
257 			route.pathOverride = meta.hadPathUDA;
258 
259 			alias WPAT = UDATuple!(WebParamAttribute, func);
260 			foreach (i, PT; ParameterTypes) {
261 				enum pname = parameterNames[i];
262 				enum ParamWPAT = WebParamUDATuple!(func, i);
263 
264 				// Comparison template for anySatisfy
265 				//template Cmp(WebParamAttribute attr) { enum Cmp = (attr.identifier == ParamNames[i]); }
266 				alias CompareParamName = GenCmp!("Loop"~func.mangleof, i, parameterNames[i]);
267 				mixin(CompareParamName.Decl);
268 
269 				StaticParameter pi;
270 				pi.name = parameterNames[i];
271 
272 				// determine in/out storage class
273 				enum SC = ParameterStorageClassTuple!func[i];
274 				static if (SC & ParameterStorageClass.out_) {
275 					pi.isOut = true;
276 				} else static if (SC & ParameterStorageClass.ref_) {
277 					pi.isIn = true;
278 					pi.isOut = isMutable!PT;
279 				} else {
280 					pi.isIn = true;
281 				}
282 
283 				// determine parameter source/destination
284 				static if (is(PT == AUTHTP)) {
285 					pi.kind = ParameterKind.auth;
286 				} else static if (IsAttributedParameter!(func, pname)) {
287 					pi.kind = ParameterKind.attributed;
288 				} else static if (AliasSeq!(cfunc).length > 0 && IsAttributedParameter!(cfunc, pname)) {
289 					pi.kind = ParameterKind.attributed;
290 				} else static if (ParamWPAT.length) {
291 					static assert(ParamWPAT.length == 1,
292 						"Cannot have more than one kind of web parameter attribute " ~
293 						"(`headerParam`, `bodyParam, etc..) on parameter `" ~
294 						parameterNames[i] ~ "` to function `" ~
295 						fullyQualifiedName!RouteFunction ~ "`");
296 					pi.kind = ParamWPAT[0].origin;
297 					pi.fieldName = ParamWPAT[0].field;
298 					if (pi.kind == ParameterKind.body_ && pi.fieldName == "")
299 						pi.kind = ParameterKind.wholeBody;
300 				} else static if (anySatisfy!(mixin(CompareParamName.Name), WPAT)) {
301 					// TODO: This was useful before DMD v2.082.0,
302 					// as UDAs on parameters were not supported.
303 					// It should be deprecated once at least one Vibe.d release
304 					// containing support for WebParamAttributes on parameters
305 					// has been put out.
306 					alias PWPAT = Filter!(mixin(CompareParamName.Name), WPAT);
307 					pi.kind = PWPAT[0].origin;
308 					pi.fieldName = PWPAT[0].field;
309 					if (pi.kind == ParameterKind.body_ && pi.fieldName == "")
310 						pi.kind = ParameterKind.wholeBody;
311 				} else static if (pname.startsWith("_")) {
312 					pi.kind = ParameterKind.internal;
313 					pi.fieldName = parameterNames[i][1 .. $];
314 				} else static if (i == 0 && pname == "id") {
315 					pi.kind = ParameterKind.internal;
316 					pi.fieldName = "id";
317 				} else {
318 					pi.kind = route.method == HTTPMethod.GET ? ParameterKind.query : ParameterKind.body_;
319 				}
320 
321 				route.parameters ~= pi;
322 			}
323 
324 			auto nhb = route.parameters.count!(p => p.kind == ParameterKind.wholeBody);
325 			assert(nhb <= 1, "Multiple whole-body parameters defined for "~route.functionName~".");
326 			assert(nhb == 0 || !route.parameters.any!(p => p.kind == ParameterKind.body_),
327 				"Normal body parameters and a whole-body parameter defined at the same time for "~route.functionName~".");
328 
329 			ret[fi] = route;
330 		}
331 
332 		return ret;
333 	}
334 
335 	private void computeSubInterfaces()
336 	{
337 		foreach (i, func; SubInterfaceFunctions) {
338 			enum meta = extractHTTPMethodAndName!(func, false)();
339 
340 			static if (meta.hadPathUDA) string url = meta.url;
341 			else string url = computeDefaultPath!func(meta.url);
342 
343 			SubInterface si;
344 			si.settings = settings.dup;
345 			si.settings.baseURL = URL(concatURL(this.baseURL, url, true));
346 			subInterfaces[i] = si;
347 		}
348 
349 		assert(subInterfaces.length == SubInterfaceFunctions.length);
350 	}
351 
352 	private template GetSubInterfaceFunctions() {
353 		template Impl(size_t idx) {
354 			static if (idx < AllMethods.length) {
355 				alias SI = SubInterfaceType!(AllMethods[idx]);
356 				static if (!is(SI == void)) {
357 					alias Impl = TypeTuple!(AllMethods[idx], Impl!(idx+1));
358 				} else {
359 					alias Impl = Impl!(idx+1);
360 				}
361 			} else alias Impl = TypeTuple!();
362 		}
363 		alias GetSubInterfaceFunctions = Impl!0;
364 	}
365 
366 	private template GetSubInterfaceTypes() {
367 		template Impl(size_t idx) {
368 			static if (idx < AllMethods.length) {
369 				alias SI = SubInterfaceType!(AllMethods[idx]);
370 				static if (!is(SI == void)) {
371 					alias Impl = TypeTuple!(SI, Impl!(idx+1));
372 				} else {
373 					alias Impl = Impl!(idx+1);
374 				}
375 			} else alias Impl = TypeTuple!();
376 		}
377 		alias GetSubInterfaceTypes = Impl!0;
378 	}
379 
380 	private template GetRouteFunctions() {
381 		template Impl(size_t idx) {
382 			static if (idx < AllMethods.length) {
383 				alias F = AllMethods[idx];
384 				alias SI = SubInterfaceType!F;
385 				static if (is(SI == void))
386 					alias Impl = TypeTuple!(F, Impl!(idx+1));
387 				else alias Impl = Impl!(idx+1);
388 			} else alias Impl = TypeTuple!();
389 		}
390 		alias GetRouteFunctions = Impl!0;
391 	}
392 
393 	private template GetAllMethods() {
394 		template Impl(size_t idx) {
395 			static if (idx < memberNames.length) {
396 				enum name = memberNames[idx];
397 				// WORKAROUND #1045 / @@BUG14375@@
398 				static if (name.length != 0)
399 					alias Impl = TypeTuple!(Filter!(IsRouteMethod, MemberFunctionsTuple!(I, name)), Impl!(idx+1));
400 				else alias Impl = Impl!(idx+1);
401 			} else alias Impl = TypeTuple!();
402 		}
403 		alias GetAllMethods = Impl!0;
404 	}
405 
406 	private string computeDefaultPath(alias method)(string name)
407 	{
408 		auto ret = adjustMethodStyle(stripTUnderscore(name, settings), settings.methodStyle);
409 		static if (is(I.CollectionIndices)) {
410 			alias IdxTypes = typeof(I.CollectionIndices.tupleof);
411 			alias PTypes = ParameterTypeTuple!method;
412 			enum has_index_param = PTypes.length >= IdxTypes.length && is(PTypes[0 .. IdxTypes.length] == IdxTypes);
413 			enum index_name = __traits(identifier, I.CollectionIndices.tupleof[$-1]);
414 
415 			static if (has_index_param && index_name.startsWith("_"))
416 				ret = (":" ~ index_name[1 .. $] ~ "/").concatURL(ret);
417 		}
418 		return ret;
419 	}
420 }
421 
422 private enum IsRouteMethod(alias M) = !hasUDA!(M, NoRouteAttribute);
423 
424 struct Route {
425 	string functionName; // D name of the function
426 	HTTPMethod method;
427 	string pattern; // relative route path (relative to baseURL)
428 	string fullPattern; // absolute version of 'pattern'
429 	bool pathHasPlaceholders; // true if path/pattern contains any :placeholers
430 	PathPart[] pathParts; // path separated into text and placeholder parts
431 	PathPart[] fullPathParts; // full path separated into text and placeholder parts
432 	Parameter[] parameters;
433 	Parameter wholeBodyParameter;
434 	Parameter[] queryParameters;
435 	Parameter[] bodyParameters;
436 	Parameter[] headerParameters;
437 	Parameter[] attributedParameters;
438 	Parameter[] internalParameters;
439 	Parameter[] authParameters;
440 }
441 
442 struct PathPart {
443 	/// interpret `text` as a parameter name (including the leading underscore) or as raw text
444 	bool isParameter;
445 	string text;
446 }
447 
448 struct Parameter {
449 	ParameterKind kind;
450 	string name;
451 	string fieldName;
452 	bool isIn, isOut;
453 }
454 
455 struct StaticRoute {
456 	string functionName; // D name of the function
457 	string rawName; // raw name as returned
458 	bool pathOverride; // @path UDA was used
459 	HTTPMethod method;
460 	StaticParameter[] parameters;
461 }
462 
463 struct StaticParameter {
464 	ParameterKind kind;
465 	string name;
466 	string fieldName; // only set for parameters where the field name can be statically determined - use Parameter.fieldName in usual cases
467 	bool isIn, isOut;
468 }
469 
470 enum ParameterKind {
471 	query,       // req.query[]
472 	body_,       // JSON body (single field)
473 	wholeBody,   // JSON body
474 	header,      // req.header[]
475 	attributed,  // @before
476 	internal,    // req.params[]
477 	auth         // @authrorized!T
478 }
479 
480 struct SubInterface {
481 	RestInterfaceSettings settings;
482 }
483 
484 template SubInterfaceType(alias F) {
485 	import std.traits : ReturnType, isInstanceOf;
486 	import vibe.core.stream : isInputStream;
487 	alias RT = ReturnType!F;
488 	static if (isInputStream!RT) alias SubInterfaceType = void;
489 	else static if (is(RT == interface)) alias SubInterfaceType = RT;
490 	else static if (isInstanceOf!(Collection, RT)) alias SubInterfaceType = RT.Interface;
491 	else alias SubInterfaceType = void;
492 }
493 
494 /**
495  * Get an UDATuple of WebParamAttribute at the index
496  *
497  * This is evil complicated magic.
498  * https://forum.dlang.org/post/qdmpfg$14f5$1@digitalmars.com
499  */
500 template WebParamUDATuple (alias Func, size_t idx)
501 {
502     import std.meta : AliasSeq;
503     import std.traits : getUDAs;
504 
505     static template isWPA (alias Elem) {
506         static if (is(typeof(Elem)))
507             enum isWPA = is(typeof(Elem) == WebParamAttribute);
508         else
509             enum isWPA = false;
510     }
511 
512     static if (is(typeof(Func) Params == __parameters))
513         alias WebParamUDATuple = Filter!(isWPA, __traits(getAttributes, Params[idx .. idx + 1]));
514     else
515         alias WebParamUDATuple = AliasSeq!();
516         //static assert(0, "Need to pass a function alias to `WebParamUDATuple`");
517 }
518 
519 private bool extractPathParts(ref PathPart[] parts, string pattern)
520 @safe {
521 	import std.string : indexOf;
522 
523 	string p = pattern;
524 
525 	bool has_placeholders = false;
526 
527 	void addText(string str) {
528 		if (parts.length > 0 && !parts[$-1].isParameter)
529 			parts[$-1].text ~= str;
530 		else parts ~= PathPart(false, str);
531 	}
532 
533 	while (p.length) {
534 		auto cidx = p.indexOf(':');
535 		if (cidx < 0) break;
536 		if (cidx > 0) addText(p[0 .. cidx]);
537 		p = p[cidx+1 .. $];
538 
539 		auto sidx = p.indexOf('/');
540 		if (sidx < 0) sidx = p.length;
541 		assert(sidx > 0, "Empty path placeholders are illegal.");
542 		parts ~= PathPart(true, "_" ~ p[0 .. sidx]);
543 		has_placeholders = true;
544 		p = p[sidx .. $];
545 	}
546 
547 	if (p.length) addText(p);
548 
549 	return has_placeholders;
550 }
551 
552 private static auto getRoutesGroupedByPatternImpl(scope Route[] routes)
553 {
554 	import std.algorithm : map, sort, filter, any;
555 	import std.array : array;
556 	import std.typecons : tuple;
557 	// since /foo/:bar and /foo/:baz are the same route, we first normalize
558 	// the patterns (by replacing each param with just ':'). after that, we
559 	// sort and groupBy, in order to group related routes
560 	return routes[]
561 		.dup // just to silence scope warnings later in the chain
562 		.map!(route => tuple(route,
563 				route.fullPathParts
564 					.map!(part => part.isParameter ? ":" : part.text)
565 					.array) // can probably remove the array here if we rewrite the comparison functions (in sort and in the foreach) to work on ranges
566 			)
567 		.array
568 		.sort!((a,b) => a[1] < b[1])
569 		.groupBy
570 		.map!(group => group.map!(tuple => tuple[0]).array)
571 		.array;
572 }
573 
574 
575 unittest {
576 	interface IDUMMY { void test(int dummy); }
577 	class DUMMY : IDUMMY { void test(int) {} }
578 	auto test = RestInterface!DUMMY(null, false);
579 }
580 
581 unittest {
582 	interface IDUMMY {}
583 	class DUMMY : IDUMMY {}
584 	auto test = RestInterface!DUMMY(null, false);
585 }
586 
587 unittest {
588 	interface I {
589 		void a();
590 		@path("foo") void b();
591 		void c(int id);
592 		@path("bar") void d(int id);
593 		@path(":baz") void e(int _baz);
594 		@path(":foo/:bar/baz") void f(int _foo, int _bar);
595 	}
596 
597 	auto test = RestInterface!I(null, false);
598 
599 	assert(test.routeCount == 6);
600 	assert(test.routes[0].pattern == "a");
601 	assert(test.routes[0].fullPattern == "/a");
602 	assert(test.routes[0].pathParts == [PathPart(false, "a")]);
603 	assert(test.routes[0].fullPathParts == [PathPart(false, "/a")]);
604 
605 	assert(test.routes[1].pattern == "foo");
606 	assert(test.routes[1].fullPattern == "/foo");
607 	assert(test.routes[1].pathParts == [PathPart(false, "foo")]);
608 	assert(test.routes[1].fullPathParts == [PathPart(false, "/foo")]);
609 
610 	assert(test.routes[2].pattern == ":id/c");
611 	assert(test.routes[2].fullPattern == "/:id/c");
612 	assert(test.routes[2].pathParts == [PathPart(true, "id"), PathPart(false, "/c")]);
613 	assert(test.routes[2].fullPathParts == [PathPart(false, "/"), PathPart(true, "id"), PathPart(false, "/c")]);
614 
615 	assert(test.routes[3].pattern == ":id/bar");
616 	assert(test.routes[3].fullPattern == "/:id/bar");
617 	assert(test.routes[3].pathParts == [PathPart(true, "id"), PathPart(false, "/bar")]);
618 	assert(test.routes[3].fullPathParts == [PathPart(false, "/"), PathPart(true, "id"), PathPart(false, "/bar")]);
619 
620 	assert(test.routes[4].pattern == ":baz");
621 	assert(test.routes[4].fullPattern == "/:baz");
622 	assert(test.routes[4].pathParts == [PathPart(true, "_baz")]);
623 	assert(test.routes[4].fullPathParts == [PathPart(false, "/"), PathPart(true, "_baz")]);
624 
625 	assert(test.routes[5].pattern == ":foo/:bar/baz");
626 	assert(test.routes[5].fullPattern == "/:foo/:bar/baz");
627 	assert(test.routes[5].pathParts == [PathPart(true, "_foo"), PathPart(false, "/"), PathPart(true, "_bar"), PathPart(false, "/baz")]);
628 	assert(test.routes[5].fullPathParts == [PathPart(false, "/"), PathPart(true, "_foo"), PathPart(false, "/"), PathPart(true, "_bar"), PathPart(false, "/baz")]);
629 }
630 
631 unittest {
632 	// Note: the RestInterface generates routes in a specific order.
633 	// since the assertions below also (indirectly) test ordering,
634 	// the assertions might trigger when the ordering of the routes
635 	// generated by the RestInterface changes.
636 	interface Options {
637 		@path("a") void getA();
638 		@path("a") void setA();
639 		@path("bar/:param") void setFoo(int _param);
640 		@path("bar/:marap") void addFoo(int _marap);
641 		void addFoo();
642 		void getFoo();
643 	}
644 
645 	auto test = RestInterface!Options(null, false);
646 	import std.array : array;
647 	import std.algorithm : map;
648 	import std.range : dropOne, front;
649 	auto options = test.getRoutesGroupedByPattern.array;
650 
651 	assert(options.length == 3);
652 	assert(options[0].front.fullPattern == "/a");
653 	assert(options[0].dropOne.front.fullPattern == "/a");
654 	assert(options[0].map!(route=>route.method).array == [HTTPMethod.GET,HTTPMethod.PUT]);
655 
656 	assert(options[1].front.fullPattern == "/bar/:param");
657 	assert(options[1].dropOne.front.fullPattern == "/bar/:marap");
658 	assert(options[1].map!(route=>route.method).array == [HTTPMethod.PUT,HTTPMethod.POST]);
659 
660 	assert(options[2].front.fullPattern == "/foo");
661 	assert(options[2].dropOne.front.fullPattern == "/foo");
662 	assert(options[2].map!(route=>route.method).array == [HTTPMethod.POST,HTTPMethod.GET]);
663 }
664 
665 unittest {
666 	@rootPathFromName
667 	interface Foo
668 	{
669 		string bar();
670 	}
671 
672 	auto test = RestInterface!Foo(null, false);
673 
674 	assert(test.routeCount == 1);
675 	assert(test.routes[0].pattern == "bar");
676 	assert(test.routes[0].fullPattern == "/foo/bar");
677 	assert(test.routes[0].pathParts == [PathPart(false, "bar")]);
678 	assert(test.routes[0].fullPathParts == [PathPart(false, "/foo/bar")]);
679 }
680 
681 unittest {
682 	@path("/foo/")
683 	interface Foo
684 	{
685 		@path("/bar/")
686 		string bar();
687 	}
688 
689 	auto test = RestInterface!Foo(null, false);
690 
691 	assert(test.routeCount == 1);
692 	assert(test.routes[0].pattern == "/bar/");
693 	assert(test.routes[0].fullPattern == "/foo/bar/");
694 	assert(test.routes[0].pathParts == [PathPart(false, "/bar/")]);
695 	assert(test.routes[0].fullPathParts == [PathPart(false, "/foo/bar/")]);
696 }
697 
698 unittest { // #1285
699 	interface I {
700 		@headerParam("b", "foo") @headerParam("c", "bar")
701 		void a(int a, out int b, ref int c);
702 		void b(int a, @viaHeader("foo") out int b, @viaHeader("bar") ref int c);
703 	}
704 	alias RI = RestInterface!I;
705 	static foreach (idx; 0 .. 2)
706 	{
707 		static assert(RI.staticRoutes[idx].parameters[0].name == "a");
708 		static assert(RI.staticRoutes[idx].parameters[0].isIn && !RI.staticRoutes[0].parameters[0].isOut);
709 		static assert(RI.staticRoutes[idx].parameters[1].name == "b");
710 		static assert(!RI.staticRoutes[idx].parameters[1].isIn && RI.staticRoutes[0].parameters[1].isOut);
711 		static assert(RI.staticRoutes[idx].parameters[2].name == "c");
712 		static assert(RI.staticRoutes[idx].parameters[2].isIn && RI.staticRoutes[0].parameters[2].isOut);
713 	}
714 }
715 
716 unittest {
717 	interface Baz {
718 		struct CollectionIndices {
719 			string _barid;
720 			int _bazid;
721 		}
722 
723 		void test(string _barid, int _bazid);
724 		void test2(string _barid);
725 	}
726 
727 	interface Bar {
728 		struct CollectionIndices {
729 			string _barid;
730 		}
731 
732 		Collection!Baz baz(string _barid);
733 
734 		void test(string _barid);
735 		void test2();
736 	}
737 
738 	interface Foo {
739 		Collection!Bar bar();
740 	}
741 
742 	auto foo = RestInterface!Foo(null, false);
743 	assert(foo.subInterfaceCount == 1);
744 
745 	auto bar = RestInterface!Bar(foo.subInterfaces[0].settings, false);
746 	assert(bar.routeCount == 2);
747 	assert(bar.routes[0].fullPattern == "/bar/:barid/test");
748 	assert(bar.routes[0].pathHasPlaceholders);
749 	assert(bar.routes[1].fullPattern == "/bar/test2", bar.routes[1].fullPattern);
750 	assert(!bar.routes[1].pathHasPlaceholders);
751 	assert(bar.subInterfaceCount == 1);
752 
753 	auto baz = RestInterface!Baz(bar.subInterfaces[0].settings, false);
754 	assert(baz.routeCount == 2);
755 	assert(baz.routes[0].fullPattern == "/bar/:barid/baz/:bazid/test");
756 	assert(baz.routes[0].pathHasPlaceholders);
757 	assert(baz.routes[1].fullPattern == "/bar/:barid/baz/test2");
758 	assert(baz.routes[1].pathHasPlaceholders);
759 }
760 
761 unittest { // #1648
762 	import vibe.web.auth;
763 
764 	struct AI {}
765 
766 	@requiresAuth!AI
767 	interface I {
768 		void a();
769 	}
770 	alias RI = RestInterface!I;
771 }
772 
773 unittest {
774 	interface I1 { @bodyParam("foo") void a(int foo); }
775 	alias RI = RestInterface!I1;
776 	interface I2 { @bodyParam("foo") void a(int foo, int bar); }
777 	interface I3 { @bodyParam("foo") @bodyParam("bar") void a(int foo, int bar); }
778 	static assert(__traits(compiles, RestInterface!I1.init));
779 	static assert(!__traits(compiles, RestInterface!I2.init));
780 	static assert(!__traits(compiles, RestInterface!I3.init));
781 }
782 
783 unittest {
784 	import vibe.http.server : HTTPServerResponse, HTTPServerRequest;
785 	int foocomp(HTTPServerRequest, HTTPServerResponse) { return 42; }
786 	interface I { void test(int foo); }
787 	class C : I { @before!foocomp("foo") void test(int foo) { assert(foo == 42); }}
788 	alias RI = RestInterface!C;
789 	static assert(RI.staticRoutes[0].parameters[0].kind == ParameterKind.attributed);
790 }