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 		import std.algorithm : map, sort, filter, any;
220 		import std.array : array;
221 		import std.typecons : tuple;
222 		// since /foo/:bar and /foo/:baz are the same route, we first normalize the patterns (by replacing each param with just ':')
223 		// after that we sort and chunkBy/groupBy, in order to group the related route
224 		auto sorted = routes[].map!((route){
225 				return tuple(route,route.fullPathParts.map!((part){
226 					return part.isParameter ? ":" : part.text;
227 				}).array()); // can probably remove the array here if we rewrite the comparison functions (in sort and in the foreach) to work on ranges
228 			})
229 			.array
230 			.sort!((a,b) => a[1] < b[1]);
231 
232 		typeof(sorted)[] groups;
233 		if (sorted.length > 0)
234 		{
235 			// NOTE: we want to support 2.066 but it doesn't have chunkBy, so we do the classic loop thingy
236 			size_t start, idx = 1;
237 			foreach(route, path; sorted[1..$])
238 			{
239 				if (sorted[idx-1][1] != path)
240 				{
241 					groups ~= sorted[start..idx];
242 					start = idx;
243 				}
244 				++idx;
245 			}
246 			groups ~= sorted[start..$];
247 		}
248 
249 		return groups.map!(group => group.map!(tuple => tuple[0]));
250 	}
251 
252 	private static StaticRoute[routeCount] computeStaticRoutes()
253 	{
254 		static import std.traits;
255 		import vibe.web.auth : AuthInfo;
256 		import std.algorithm.searching : any, count;
257 		import std.meta : AliasSeq;
258 		import std.traits : isMutable;
259 
260 		assert(__ctfe);
261 
262 		StaticRoute[routeCount] ret;
263 
264 		alias AUTHTP = AuthInfo!TImpl;
265 
266 		foreach (fi, func; RouteFunctions) {
267 			StaticRoute route;
268 			route.functionName = __traits(identifier, func);
269 
270 			static if (!is(TImpl == I))
271 				alias cfunc = derivedMethod!(TImpl, func);
272 			else
273 				alias cfunc = func;
274 
275 			alias FuncType = FunctionTypeOf!func;
276 			alias ParameterTypes = ParameterTypeTuple!FuncType;
277 			alias ReturnType = std.traits.ReturnType!FuncType;
278 			enum parameterNames = [ParameterIdentifierTuple!func];
279 
280 			enum meta = extractHTTPMethodAndName!(func, false)();
281 			route.method = meta.method;
282 			route.rawName = meta.url;
283 			route.pathOverride = meta.hadPathUDA;
284 
285 			alias WPAT = UDATuple!(WebParamAttribute, func);
286 			foreach (i, PT; ParameterTypes) {
287 				enum pname = parameterNames[i];
288 				enum ParamWPAT = WebParamUDATuple!(func, i);
289 
290 				// Comparison template for anySatisfy
291 				//template Cmp(WebParamAttribute attr) { enum Cmp = (attr.identifier == ParamNames[i]); }
292 				alias CompareParamName = GenCmp!("Loop"~func.mangleof, i, parameterNames[i]);
293 				mixin(CompareParamName.Decl);
294 
295 				StaticParameter pi;
296 				pi.name = parameterNames[i];
297 
298 				// determine in/out storage class
299 				enum SC = ParameterStorageClassTuple!func[i];
300 				static if (SC & ParameterStorageClass.out_) {
301 					pi.isOut = true;
302 				} else static if (SC & ParameterStorageClass.ref_) {
303 					pi.isIn = true;
304 					pi.isOut = isMutable!PT;
305 				} else {
306 					pi.isIn = true;
307 				}
308 
309 				// determine parameter source/destination
310 				static if (is(PT == AUTHTP)) {
311 					pi.kind = ParameterKind.auth;
312 				} else static if (IsAttributedParameter!(func, pname)) {
313 					pi.kind = ParameterKind.attributed;
314 				} else static if (AliasSeq!(cfunc).length > 0 && IsAttributedParameter!(cfunc, pname)) {
315 					pi.kind = ParameterKind.attributed;
316 				} else static if (ParamWPAT.length) {
317 					static assert(ParamWPAT.length == 1,
318 						"Cannot have more than one kind of web parameter attribute " ~
319 						"(`headerParam`, `bodyParam, etc..) on parameter `" ~
320 						parameterNames[i] ~ "` to function `" ~
321 						fullyQualifiedName!RouteFunction ~ "`");
322 					pi.kind = ParamWPAT[0].origin;
323 					pi.fieldName = ParamWPAT[0].field;
324 					if (pi.kind == ParameterKind.body_ && pi.fieldName == "")
325 						pi.kind = ParameterKind.wholeBody;
326 				} else static if (anySatisfy!(mixin(CompareParamName.Name), WPAT)) {
327 					// TODO: This was useful before DMD v2.082.0,
328 					// as UDAs on parameters were not supported.
329 					// It should be deprecated once at least one Vibe.d release
330 					// containing support for WebParamAttributes on parameters
331 					// has been put out.
332 					alias PWPAT = Filter!(mixin(CompareParamName.Name), WPAT);
333 					pi.kind = PWPAT[0].origin;
334 					pi.fieldName = PWPAT[0].field;
335 					if (pi.kind == ParameterKind.body_ && pi.fieldName == "")
336 						pi.kind = ParameterKind.wholeBody;
337 				} else static if (pname.startsWith("_")) {
338 					pi.kind = ParameterKind.internal;
339 					pi.fieldName = parameterNames[i][1 .. $];
340 				} else static if (i == 0 && pname == "id") {
341 					pi.kind = ParameterKind.internal;
342 					pi.fieldName = "id";
343 				} else {
344 					pi.kind = route.method == HTTPMethod.GET ? ParameterKind.query : ParameterKind.body_;
345 				}
346 
347 				route.parameters ~= pi;
348 			}
349 
350 			auto nhb = route.parameters.count!(p => p.kind == ParameterKind.wholeBody);
351 			assert(nhb <= 1, "Multiple whole-body parameters defined for "~route.functionName~".");
352 			assert(nhb == 0 || !route.parameters.any!(p => p.kind == ParameterKind.body_),
353 				"Normal body parameters and a whole-body parameter defined at the same time for "~route.functionName~".");
354 
355 			ret[fi] = route;
356 		}
357 
358 		return ret;
359 	}
360 
361 	private void computeSubInterfaces()
362 	{
363 		foreach (i, func; SubInterfaceFunctions) {
364 			enum meta = extractHTTPMethodAndName!(func, false)();
365 
366 			static if (meta.hadPathUDA) string url = meta.url;
367 			else string url = computeDefaultPath!func(meta.url);
368 
369 			SubInterface si;
370 			si.settings = settings.dup;
371 			si.settings.baseURL = URL(concatURL(this.baseURL, url, true));
372 			subInterfaces[i] = si;
373 		}
374 
375 		assert(subInterfaces.length == SubInterfaceFunctions.length);
376 	}
377 
378 	private template GetSubInterfaceFunctions() {
379 		template Impl(size_t idx) {
380 			static if (idx < AllMethods.length) {
381 				alias SI = SubInterfaceType!(AllMethods[idx]);
382 				static if (!is(SI == void)) {
383 					alias Impl = TypeTuple!(AllMethods[idx], Impl!(idx+1));
384 				} else {
385 					alias Impl = Impl!(idx+1);
386 				}
387 			} else alias Impl = TypeTuple!();
388 		}
389 		alias GetSubInterfaceFunctions = Impl!0;
390 	}
391 
392 	private template GetSubInterfaceTypes() {
393 		template Impl(size_t idx) {
394 			static if (idx < AllMethods.length) {
395 				alias SI = SubInterfaceType!(AllMethods[idx]);
396 				static if (!is(SI == void)) {
397 					alias Impl = TypeTuple!(SI, Impl!(idx+1));
398 				} else {
399 					alias Impl = Impl!(idx+1);
400 				}
401 			} else alias Impl = TypeTuple!();
402 		}
403 		alias GetSubInterfaceTypes = Impl!0;
404 	}
405 
406 	private template GetRouteFunctions() {
407 		template Impl(size_t idx) {
408 			static if (idx < AllMethods.length) {
409 				alias F = AllMethods[idx];
410 				alias SI = SubInterfaceType!F;
411 				static if (is(SI == void))
412 					alias Impl = TypeTuple!(F, Impl!(idx+1));
413 				else alias Impl = Impl!(idx+1);
414 			} else alias Impl = TypeTuple!();
415 		}
416 		alias GetRouteFunctions = Impl!0;
417 	}
418 
419 	private template GetAllMethods() {
420 		template Impl(size_t idx) {
421 			static if (idx < memberNames.length) {
422 				enum name = memberNames[idx];
423 				// WORKAROUND #1045 / @@BUG14375@@
424 				static if (name.length != 0)
425 					alias Impl = TypeTuple!(Filter!(IsRouteMethod, MemberFunctionsTuple!(I, name)), Impl!(idx+1));
426 				else alias Impl = Impl!(idx+1);
427 			} else alias Impl = TypeTuple!();
428 		}
429 		alias GetAllMethods = Impl!0;
430 	}
431 
432 	private string computeDefaultPath(alias method)(string name)
433 	{
434 		auto ret = adjustMethodStyle(stripTUnderscore(name, settings), settings.methodStyle);
435 		static if (is(I.CollectionIndices)) {
436 			alias IdxTypes = typeof(I.CollectionIndices.tupleof);
437 			alias PTypes = ParameterTypeTuple!method;
438 			enum has_index_param = PTypes.length >= IdxTypes.length && is(PTypes[0 .. IdxTypes.length] == IdxTypes);
439 			enum index_name = __traits(identifier, I.CollectionIndices.tupleof[$-1]);
440 
441 			static if (has_index_param && index_name.startsWith("_"))
442 				ret = (":" ~ index_name[1 .. $] ~ "/").concatURL(ret);
443 		}
444 		return ret;
445 	}
446 }
447 
448 private enum IsRouteMethod(alias M) = !hasUDA!(M, NoRouteAttribute);
449 
450 struct Route {
451 	string functionName; // D name of the function
452 	HTTPMethod method;
453 	string pattern; // relative route path (relative to baseURL)
454 	string fullPattern; // absolute version of 'pattern'
455 	bool pathHasPlaceholders; // true if path/pattern contains any :placeholers
456 	PathPart[] pathParts; // path separated into text and placeholder parts
457 	PathPart[] fullPathParts; // full path separated into text and placeholder parts
458 	Parameter[] parameters;
459 	Parameter wholeBodyParameter;
460 	Parameter[] queryParameters;
461 	Parameter[] bodyParameters;
462 	Parameter[] headerParameters;
463 	Parameter[] attributedParameters;
464 	Parameter[] internalParameters;
465 	Parameter[] authParameters;
466 }
467 
468 struct PathPart {
469 	/// interpret `text` as a parameter name (including the leading underscore) or as raw text
470 	bool isParameter;
471 	string text;
472 }
473 
474 struct Parameter {
475 	ParameterKind kind;
476 	string name;
477 	string fieldName;
478 	bool isIn, isOut;
479 }
480 
481 struct StaticRoute {
482 	string functionName; // D name of the function
483 	string rawName; // raw name as returned
484 	bool pathOverride; // @path UDA was used
485 	HTTPMethod method;
486 	StaticParameter[] parameters;
487 }
488 
489 struct StaticParameter {
490 	ParameterKind kind;
491 	string name;
492 	string fieldName; // only set for parameters where the field name can be statically determined - use Parameter.fieldName in usual cases
493 	bool isIn, isOut;
494 }
495 
496 enum ParameterKind {
497 	query,       // req.query[]
498 	body_,       // JSON body (single field)
499 	wholeBody,   // JSON body
500 	header,      // req.header[]
501 	attributed,  // @before
502 	internal,    // req.params[]
503 	auth         // @authrorized!T
504 }
505 
506 struct SubInterface {
507 	RestInterfaceSettings settings;
508 }
509 
510 template SubInterfaceType(alias F) {
511 	import std.traits : ReturnType, isInstanceOf;
512 	alias RT = ReturnType!F;
513 	static if (is(RT == interface)) alias SubInterfaceType = RT;
514 	else static if (isInstanceOf!(Collection, RT)) alias SubInterfaceType = RT.Interface;
515 	else alias SubInterfaceType = void;
516 }
517 
518 /**
519  * Get an UDATuple of WebParamAttribute at the index
520  *
521  * This is evil complicated magic.
522  * https://forum.dlang.org/post/qdmpfg$14f5$1@digitalmars.com
523  */
524 template WebParamUDATuple (alias Func, size_t idx)
525 {
526     import std.meta : AliasSeq;
527     import std.traits : getUDAs;
528 
529     static template isWPA (alias Elem) {
530         static if (is(typeof(Elem)))
531             enum isWPA = is(typeof(Elem) == WebParamAttribute);
532         else
533             enum isWPA = false;
534     }
535 
536     static if (is(typeof(Func) Params == __parameters))
537         alias WebParamUDATuple = Filter!(isWPA, __traits(getAttributes, Params[idx .. idx + 1]));
538     else
539         alias WebParamUDATuple = AliasSeq!();
540         //static assert(0, "Need to pass a function alias to `WebParamUDATuple`");
541 }
542 
543 private bool extractPathParts(ref PathPart[] parts, string pattern)
544 @safe {
545 	import std.string : indexOf;
546 
547 	string p = pattern;
548 
549 	bool has_placeholders = false;
550 
551 	void addText(string str) {
552 		if (parts.length > 0 && !parts[$-1].isParameter)
553 			parts[$-1].text ~= str;
554 		else parts ~= PathPart(false, str);
555 	}
556 
557 	while (p.length) {
558 		auto cidx = p.indexOf(':');
559 		if (cidx < 0) break;
560 		if (cidx > 0) addText(p[0 .. cidx]);
561 		p = p[cidx+1 .. $];
562 
563 		auto sidx = p.indexOf('/');
564 		if (sidx < 0) sidx = p.length;
565 		assert(sidx > 0, "Empty path placeholders are illegal.");
566 		parts ~= PathPart(true, "_" ~ p[0 .. sidx]);
567 		has_placeholders = true;
568 		p = p[sidx .. $];
569 	}
570 
571 	if (p.length) addText(p);
572 
573 	return has_placeholders;
574 }
575 
576 unittest {
577 	interface IDUMMY { void test(int dummy); }
578 	class DUMMY : IDUMMY { void test(int) {} }
579 	auto test = RestInterface!DUMMY(null, false);
580 }
581 
582 unittest {
583 	interface IDUMMY {}
584 	class DUMMY : IDUMMY {}
585 	auto test = RestInterface!DUMMY(null, false);
586 }
587 
588 unittest {
589 	interface I {
590 		void a();
591 		@path("foo") void b();
592 		void c(int id);
593 		@path("bar") void d(int id);
594 		@path(":baz") void e(int _baz);
595 		@path(":foo/:bar/baz") void f(int _foo, int _bar);
596 	}
597 
598 	auto test = RestInterface!I(null, false);
599 
600 	assert(test.routeCount == 6);
601 	assert(test.routes[0].pattern == "a");
602 	assert(test.routes[0].fullPattern == "/a");
603 	assert(test.routes[0].pathParts == [PathPart(false, "a")]);
604 	assert(test.routes[0].fullPathParts == [PathPart(false, "/a")]);
605 
606 	assert(test.routes[1].pattern == "foo");
607 	assert(test.routes[1].fullPattern == "/foo");
608 	assert(test.routes[1].pathParts == [PathPart(false, "foo")]);
609 	assert(test.routes[1].fullPathParts == [PathPart(false, "/foo")]);
610 
611 	assert(test.routes[2].pattern == ":id/c");
612 	assert(test.routes[2].fullPattern == "/:id/c");
613 	assert(test.routes[2].pathParts == [PathPart(true, "id"), PathPart(false, "/c")]);
614 	assert(test.routes[2].fullPathParts == [PathPart(false, "/"), PathPart(true, "id"), PathPart(false, "/c")]);
615 
616 	assert(test.routes[3].pattern == ":id/bar");
617 	assert(test.routes[3].fullPattern == "/:id/bar");
618 	assert(test.routes[3].pathParts == [PathPart(true, "id"), PathPart(false, "/bar")]);
619 	assert(test.routes[3].fullPathParts == [PathPart(false, "/"), PathPart(true, "id"), PathPart(false, "/bar")]);
620 
621 	assert(test.routes[4].pattern == ":baz");
622 	assert(test.routes[4].fullPattern == "/:baz");
623 	assert(test.routes[4].pathParts == [PathPart(true, "_baz")]);
624 	assert(test.routes[4].fullPathParts == [PathPart(false, "/"), PathPart(true, "_baz")]);
625 
626 	assert(test.routes[5].pattern == ":foo/:bar/baz");
627 	assert(test.routes[5].fullPattern == "/:foo/:bar/baz");
628 	assert(test.routes[5].pathParts == [PathPart(true, "_foo"), PathPart(false, "/"), PathPart(true, "_bar"), PathPart(false, "/baz")]);
629 	assert(test.routes[5].fullPathParts == [PathPart(false, "/"), PathPart(true, "_foo"), PathPart(false, "/"), PathPart(true, "_bar"), PathPart(false, "/baz")]);
630 }
631 
632 unittest {
633 	// Note: the RestInterface generates routes in a specific order.
634 	// since the assertions below also (indirectly) test ordering,
635 	// the assertions might trigger when the ordering of the routes
636 	// generated by the RestInterface changes.
637 	interface Options {
638 		@path("a") void getA();
639 		@path("a") void setA();
640 		@path("bar/:param") void setFoo(int _param);
641 		@path("bar/:marap") void addFoo(int _marap);
642 		void addFoo();
643 		void getFoo();
644 	}
645 
646 	auto test = RestInterface!Options(null, false);
647 	import std.array : array;
648 	import std.algorithm : map;
649 	import std.range : dropOne, front;
650 	auto options = test.getRoutesGroupedByPattern.array;
651 
652 	assert(options.length == 3);
653 	assert(options[0].front.fullPattern == "/a");
654 	assert(options[0].dropOne.front.fullPattern == "/a");
655 	assert(options[0].map!(route=>route.method).array == [HTTPMethod.GET,HTTPMethod.PUT]);
656 
657 	assert(options[1].front.fullPattern == "/bar/:param");
658 	assert(options[1].dropOne.front.fullPattern == "/bar/:marap");
659 	assert(options[1].map!(route=>route.method).array == [HTTPMethod.PUT,HTTPMethod.POST]);
660 
661 	assert(options[2].front.fullPattern == "/foo");
662 	assert(options[2].dropOne.front.fullPattern == "/foo");
663 	assert(options[2].map!(route=>route.method).array == [HTTPMethod.POST,HTTPMethod.GET]);
664 }
665 
666 unittest {
667 	@rootPathFromName
668 	interface Foo
669 	{
670 		string bar();
671 	}
672 
673 	auto test = RestInterface!Foo(null, false);
674 
675 	assert(test.routeCount == 1);
676 	assert(test.routes[0].pattern == "bar");
677 	assert(test.routes[0].fullPattern == "/foo/bar");
678 	assert(test.routes[0].pathParts == [PathPart(false, "bar")]);
679 	assert(test.routes[0].fullPathParts == [PathPart(false, "/foo/bar")]);
680 }
681 
682 unittest {
683 	@path("/foo/")
684 	interface Foo
685 	{
686 		@path("/bar/")
687 		string bar();
688 	}
689 
690 	auto test = RestInterface!Foo(null, false);
691 
692 	assert(test.routeCount == 1);
693 	assert(test.routes[0].pattern == "/bar/");
694 	assert(test.routes[0].fullPattern == "/foo/bar/");
695 	assert(test.routes[0].pathParts == [PathPart(false, "/bar/")]);
696 	assert(test.routes[0].fullPathParts == [PathPart(false, "/foo/bar/")]);
697 }
698 
699 unittest { // #1285
700 	interface I {
701 		@headerParam("b", "foo") @headerParam("c", "bar")
702 		void a(int a, out int b, ref int c);
703 		void b(int a, @viaHeader("foo") out int b, @viaHeader("bar") ref int c);
704 	}
705 	alias RI = RestInterface!I;
706 	static foreach (idx; 0 .. 2)
707 	{
708 		static assert(RI.staticRoutes[idx].parameters[0].name == "a");
709 		static assert(RI.staticRoutes[idx].parameters[0].isIn && !RI.staticRoutes[0].parameters[0].isOut);
710 		static assert(RI.staticRoutes[idx].parameters[1].name == "b");
711 		static assert(!RI.staticRoutes[idx].parameters[1].isIn && RI.staticRoutes[0].parameters[1].isOut);
712 		static assert(RI.staticRoutes[idx].parameters[2].name == "c");
713 		static assert(RI.staticRoutes[idx].parameters[2].isIn && RI.staticRoutes[0].parameters[2].isOut);
714 	}
715 }
716 
717 unittest {
718 	interface Baz {
719 		struct CollectionIndices {
720 			string _barid;
721 			int _bazid;
722 		}
723 
724 		void test(string _barid, int _bazid);
725 		void test2(string _barid);
726 	}
727 
728 	interface Bar {
729 		struct CollectionIndices {
730 			string _barid;
731 		}
732 
733 		Collection!Baz baz(string _barid);
734 
735 		void test(string _barid);
736 		void test2();
737 	}
738 
739 	interface Foo {
740 		Collection!Bar bar();
741 	}
742 
743 	auto foo = RestInterface!Foo(null, false);
744 	assert(foo.subInterfaceCount == 1);
745 
746 	auto bar = RestInterface!Bar(foo.subInterfaces[0].settings, false);
747 	assert(bar.routeCount == 2);
748 	assert(bar.routes[0].fullPattern == "/bar/:barid/test");
749 	assert(bar.routes[0].pathHasPlaceholders);
750 	assert(bar.routes[1].fullPattern == "/bar/test2", bar.routes[1].fullPattern);
751 	assert(!bar.routes[1].pathHasPlaceholders);
752 	assert(bar.subInterfaceCount == 1);
753 
754 	auto baz = RestInterface!Baz(bar.subInterfaces[0].settings, false);
755 	assert(baz.routeCount == 2);
756 	assert(baz.routes[0].fullPattern == "/bar/:barid/baz/:bazid/test");
757 	assert(baz.routes[0].pathHasPlaceholders);
758 	assert(baz.routes[1].fullPattern == "/bar/:barid/baz/test2");
759 	assert(baz.routes[1].pathHasPlaceholders);
760 }
761 
762 unittest { // #1648
763 	import vibe.web.auth;
764 
765 	struct AI {}
766 
767 	@requiresAuth!AI
768 	interface I {
769 		void a();
770 	}
771 	alias RI = RestInterface!I;
772 }
773 
774 unittest {
775 	interface I1 { @bodyParam("foo") void a(int foo); }
776 	alias RI = RestInterface!I1;
777 	interface I2 { @bodyParam("foo") void a(int foo, int bar); }
778 	interface I3 { @bodyParam("foo") @bodyParam("bar") void a(int foo, int bar); }
779 	static assert(__traits(compiles, RestInterface!I1.init));
780 	static assert(!__traits(compiles, RestInterface!I2.init));
781 	static assert(!__traits(compiles, RestInterface!I3.init));
782 }
783 
784 unittest {
785 	import vibe.http.server : HTTPServerResponse, HTTPServerRequest;
786 	int foocomp(HTTPServerRequest, HTTPServerResponse) { return 42; }
787 	interface I { void test(int foo); }
788 	class C : I { @before!foocomp("foo") void test(int foo) { assert(foo == 42); }}
789 	alias RI = RestInterface!C;
790 	static assert(RI.staticRoutes[0].parameters[0].kind == ParameterKind.attributed);
791 }