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