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