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 }