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