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 }