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