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 import std.algorithm : map, sort, filter, any; 220 import std.array : array; 221 import std.typecons : tuple; 222 // since /foo/:bar and /foo/:baz are the same route, we first normalize the patterns (by replacing each param with just ':') 223 // after that we sort and chunkBy/groupBy, in order to group the related route 224 auto sorted = routes[].map!((route){ 225 return tuple(route,route.fullPathParts.map!((part){ 226 return part.isParameter ? ":" : part.text; 227 }).array()); // can probably remove the array here if we rewrite the comparison functions (in sort and in the foreach) to work on ranges 228 }) 229 .array 230 .sort!((a,b) => a[1] < b[1]); 231 232 typeof(sorted)[] groups; 233 if (sorted.length > 0) 234 { 235 // NOTE: we want to support 2.066 but it doesn't have chunkBy, so we do the classic loop thingy 236 size_t start, idx = 1; 237 foreach(route, path; sorted[1..$]) 238 { 239 if (sorted[idx-1][1] != path) 240 { 241 groups ~= sorted[start..idx]; 242 start = idx; 243 } 244 ++idx; 245 } 246 groups ~= sorted[start..$]; 247 } 248 249 return groups.map!(group => group.map!(tuple => tuple[0])); 250 } 251 252 private static StaticRoute[routeCount] computeStaticRoutes() 253 { 254 static import std.traits; 255 import vibe.web.auth : AuthInfo; 256 import std.algorithm.searching : any, count; 257 import std.meta : AliasSeq; 258 import std.traits : isMutable; 259 260 assert(__ctfe); 261 262 StaticRoute[routeCount] ret; 263 264 alias AUTHTP = AuthInfo!TImpl; 265 266 foreach (fi, func; RouteFunctions) { 267 StaticRoute route; 268 route.functionName = __traits(identifier, func); 269 270 static if (!is(TImpl == I)) 271 alias cfunc = derivedMethod!(TImpl, func); 272 else 273 alias cfunc = func; 274 275 alias FuncType = FunctionTypeOf!func; 276 alias ParameterTypes = ParameterTypeTuple!FuncType; 277 alias ReturnType = std.traits.ReturnType!FuncType; 278 enum parameterNames = [ParameterIdentifierTuple!func]; 279 280 enum meta = extractHTTPMethodAndName!(func, false)(); 281 route.method = meta.method; 282 route.rawName = meta.url; 283 route.pathOverride = meta.hadPathUDA; 284 285 alias WPAT = UDATuple!(WebParamAttribute, func); 286 foreach (i, PT; ParameterTypes) { 287 enum pname = parameterNames[i]; 288 enum ParamWPAT = WebParamUDATuple!(func, i); 289 290 // Comparison template for anySatisfy 291 //template Cmp(WebParamAttribute attr) { enum Cmp = (attr.identifier == ParamNames[i]); } 292 alias CompareParamName = GenCmp!("Loop"~func.mangleof, i, parameterNames[i]); 293 mixin(CompareParamName.Decl); 294 295 StaticParameter pi; 296 pi.name = parameterNames[i]; 297 298 // determine in/out storage class 299 enum SC = ParameterStorageClassTuple!func[i]; 300 static if (SC & ParameterStorageClass.out_) { 301 pi.isOut = true; 302 } else static if (SC & ParameterStorageClass.ref_) { 303 pi.isIn = true; 304 pi.isOut = isMutable!PT; 305 } else { 306 pi.isIn = true; 307 } 308 309 // determine parameter source/destination 310 static if (is(PT == AUTHTP)) { 311 pi.kind = ParameterKind.auth; 312 } else static if (IsAttributedParameter!(func, pname)) { 313 pi.kind = ParameterKind.attributed; 314 } else static if (AliasSeq!(cfunc).length > 0 && IsAttributedParameter!(cfunc, pname)) { 315 pi.kind = ParameterKind.attributed; 316 } else static if (ParamWPAT.length) { 317 static assert(ParamWPAT.length == 1, 318 "Cannot have more than one kind of web parameter attribute " ~ 319 "(`headerParam`, `bodyParam, etc..) on parameter `" ~ 320 parameterNames[i] ~ "` to function `" ~ 321 fullyQualifiedName!RouteFunction ~ "`"); 322 pi.kind = ParamWPAT[0].origin; 323 pi.fieldName = ParamWPAT[0].field; 324 if (pi.kind == ParameterKind.body_ && pi.fieldName == "") 325 pi.kind = ParameterKind.wholeBody; 326 } else static if (anySatisfy!(mixin(CompareParamName.Name), WPAT)) { 327 // TODO: This was useful before DMD v2.082.0, 328 // as UDAs on parameters were not supported. 329 // It should be deprecated once at least one Vibe.d release 330 // containing support for WebParamAttributes on parameters 331 // has been put out. 332 alias PWPAT = Filter!(mixin(CompareParamName.Name), WPAT); 333 pi.kind = PWPAT[0].origin; 334 pi.fieldName = PWPAT[0].field; 335 if (pi.kind == ParameterKind.body_ && pi.fieldName == "") 336 pi.kind = ParameterKind.wholeBody; 337 } else static if (pname.startsWith("_")) { 338 pi.kind = ParameterKind.internal; 339 pi.fieldName = parameterNames[i][1 .. $]; 340 } else static if (i == 0 && pname == "id") { 341 pi.kind = ParameterKind.internal; 342 pi.fieldName = "id"; 343 } else { 344 pi.kind = route.method == HTTPMethod.GET ? ParameterKind.query : ParameterKind.body_; 345 } 346 347 route.parameters ~= pi; 348 } 349 350 auto nhb = route.parameters.count!(p => p.kind == ParameterKind.wholeBody); 351 assert(nhb <= 1, "Multiple whole-body parameters defined for "~route.functionName~"."); 352 assert(nhb == 0 || !route.parameters.any!(p => p.kind == ParameterKind.body_), 353 "Normal body parameters and a whole-body parameter defined at the same time for "~route.functionName~"."); 354 355 ret[fi] = route; 356 } 357 358 return ret; 359 } 360 361 private void computeSubInterfaces() 362 { 363 foreach (i, func; SubInterfaceFunctions) { 364 enum meta = extractHTTPMethodAndName!(func, false)(); 365 366 static if (meta.hadPathUDA) string url = meta.url; 367 else string url = computeDefaultPath!func(meta.url); 368 369 SubInterface si; 370 si.settings = settings.dup; 371 si.settings.baseURL = URL(concatURL(this.baseURL, url, true)); 372 subInterfaces[i] = si; 373 } 374 375 assert(subInterfaces.length == SubInterfaceFunctions.length); 376 } 377 378 private template GetSubInterfaceFunctions() { 379 template Impl(size_t idx) { 380 static if (idx < AllMethods.length) { 381 alias SI = SubInterfaceType!(AllMethods[idx]); 382 static if (!is(SI == void)) { 383 alias Impl = TypeTuple!(AllMethods[idx], Impl!(idx+1)); 384 } else { 385 alias Impl = Impl!(idx+1); 386 } 387 } else alias Impl = TypeTuple!(); 388 } 389 alias GetSubInterfaceFunctions = Impl!0; 390 } 391 392 private template GetSubInterfaceTypes() { 393 template Impl(size_t idx) { 394 static if (idx < AllMethods.length) { 395 alias SI = SubInterfaceType!(AllMethods[idx]); 396 static if (!is(SI == void)) { 397 alias Impl = TypeTuple!(SI, Impl!(idx+1)); 398 } else { 399 alias Impl = Impl!(idx+1); 400 } 401 } else alias Impl = TypeTuple!(); 402 } 403 alias GetSubInterfaceTypes = Impl!0; 404 } 405 406 private template GetRouteFunctions() { 407 template Impl(size_t idx) { 408 static if (idx < AllMethods.length) { 409 alias F = AllMethods[idx]; 410 alias SI = SubInterfaceType!F; 411 static if (is(SI == void)) 412 alias Impl = TypeTuple!(F, Impl!(idx+1)); 413 else alias Impl = Impl!(idx+1); 414 } else alias Impl = TypeTuple!(); 415 } 416 alias GetRouteFunctions = Impl!0; 417 } 418 419 private template GetAllMethods() { 420 template Impl(size_t idx) { 421 static if (idx < memberNames.length) { 422 enum name = memberNames[idx]; 423 // WORKAROUND #1045 / @@BUG14375@@ 424 static if (name.length != 0) 425 alias Impl = TypeTuple!(Filter!(IsRouteMethod, MemberFunctionsTuple!(I, name)), Impl!(idx+1)); 426 else alias Impl = Impl!(idx+1); 427 } else alias Impl = TypeTuple!(); 428 } 429 alias GetAllMethods = Impl!0; 430 } 431 432 private string computeDefaultPath(alias method)(string name) 433 { 434 auto ret = adjustMethodStyle(stripTUnderscore(name, settings), settings.methodStyle); 435 static if (is(I.CollectionIndices)) { 436 alias IdxTypes = typeof(I.CollectionIndices.tupleof); 437 alias PTypes = ParameterTypeTuple!method; 438 enum has_index_param = PTypes.length >= IdxTypes.length && is(PTypes[0 .. IdxTypes.length] == IdxTypes); 439 enum index_name = __traits(identifier, I.CollectionIndices.tupleof[$-1]); 440 441 static if (has_index_param && index_name.startsWith("_")) 442 ret = (":" ~ index_name[1 .. $] ~ "/").concatURL(ret); 443 } 444 return ret; 445 } 446 } 447 448 private enum IsRouteMethod(alias M) = !hasUDA!(M, NoRouteAttribute); 449 450 struct Route { 451 string functionName; // D name of the function 452 HTTPMethod method; 453 string pattern; // relative route path (relative to baseURL) 454 string fullPattern; // absolute version of 'pattern' 455 bool pathHasPlaceholders; // true if path/pattern contains any :placeholers 456 PathPart[] pathParts; // path separated into text and placeholder parts 457 PathPart[] fullPathParts; // full path separated into text and placeholder parts 458 Parameter[] parameters; 459 Parameter wholeBodyParameter; 460 Parameter[] queryParameters; 461 Parameter[] bodyParameters; 462 Parameter[] headerParameters; 463 Parameter[] attributedParameters; 464 Parameter[] internalParameters; 465 Parameter[] authParameters; 466 } 467 468 struct PathPart { 469 /// interpret `text` as a parameter name (including the leading underscore) or as raw text 470 bool isParameter; 471 string text; 472 } 473 474 struct Parameter { 475 ParameterKind kind; 476 string name; 477 string fieldName; 478 bool isIn, isOut; 479 } 480 481 struct StaticRoute { 482 string functionName; // D name of the function 483 string rawName; // raw name as returned 484 bool pathOverride; // @path UDA was used 485 HTTPMethod method; 486 StaticParameter[] parameters; 487 } 488 489 struct StaticParameter { 490 ParameterKind kind; 491 string name; 492 string fieldName; // only set for parameters where the field name can be statically determined - use Parameter.fieldName in usual cases 493 bool isIn, isOut; 494 } 495 496 enum ParameterKind { 497 query, // req.query[] 498 body_, // JSON body (single field) 499 wholeBody, // JSON body 500 header, // req.header[] 501 attributed, // @before 502 internal, // req.params[] 503 auth // @authrorized!T 504 } 505 506 struct SubInterface { 507 RestInterfaceSettings settings; 508 } 509 510 template SubInterfaceType(alias F) { 511 import std.traits : ReturnType, isInstanceOf; 512 alias RT = ReturnType!F; 513 static if (is(RT == interface)) alias SubInterfaceType = RT; 514 else static if (isInstanceOf!(Collection, RT)) alias SubInterfaceType = RT.Interface; 515 else alias SubInterfaceType = void; 516 } 517 518 /** 519 * Get an UDATuple of WebParamAttribute at the index 520 * 521 * This is evil complicated magic. 522 * https://forum.dlang.org/post/qdmpfg$14f5$1@digitalmars.com 523 */ 524 template WebParamUDATuple (alias Func, size_t idx) 525 { 526 import std.meta : AliasSeq; 527 import std.traits : getUDAs; 528 529 static template isWPA (alias Elem) { 530 static if (is(typeof(Elem))) 531 enum isWPA = is(typeof(Elem) == WebParamAttribute); 532 else 533 enum isWPA = false; 534 } 535 536 static if (is(typeof(Func) Params == __parameters)) 537 alias WebParamUDATuple = Filter!(isWPA, __traits(getAttributes, Params[idx .. idx + 1])); 538 else 539 alias WebParamUDATuple = AliasSeq!(); 540 //static assert(0, "Need to pass a function alias to `WebParamUDATuple`"); 541 } 542 543 private bool extractPathParts(ref PathPart[] parts, string pattern) 544 @safe { 545 import std.string : indexOf; 546 547 string p = pattern; 548 549 bool has_placeholders = false; 550 551 void addText(string str) { 552 if (parts.length > 0 && !parts[$-1].isParameter) 553 parts[$-1].text ~= str; 554 else parts ~= PathPart(false, str); 555 } 556 557 while (p.length) { 558 auto cidx = p.indexOf(':'); 559 if (cidx < 0) break; 560 if (cidx > 0) addText(p[0 .. cidx]); 561 p = p[cidx+1 .. $]; 562 563 auto sidx = p.indexOf('/'); 564 if (sidx < 0) sidx = p.length; 565 assert(sidx > 0, "Empty path placeholders are illegal."); 566 parts ~= PathPart(true, "_" ~ p[0 .. sidx]); 567 has_placeholders = true; 568 p = p[sidx .. $]; 569 } 570 571 if (p.length) addText(p); 572 573 return has_placeholders; 574 } 575 576 unittest { 577 interface IDUMMY { void test(int dummy); } 578 class DUMMY : IDUMMY { void test(int) {} } 579 auto test = RestInterface!DUMMY(null, false); 580 } 581 582 unittest { 583 interface IDUMMY {} 584 class DUMMY : IDUMMY {} 585 auto test = RestInterface!DUMMY(null, false); 586 } 587 588 unittest { 589 interface I { 590 void a(); 591 @path("foo") void b(); 592 void c(int id); 593 @path("bar") void d(int id); 594 @path(":baz") void e(int _baz); 595 @path(":foo/:bar/baz") void f(int _foo, int _bar); 596 } 597 598 auto test = RestInterface!I(null, false); 599 600 assert(test.routeCount == 6); 601 assert(test.routes[0].pattern == "a"); 602 assert(test.routes[0].fullPattern == "/a"); 603 assert(test.routes[0].pathParts == [PathPart(false, "a")]); 604 assert(test.routes[0].fullPathParts == [PathPart(false, "/a")]); 605 606 assert(test.routes[1].pattern == "foo"); 607 assert(test.routes[1].fullPattern == "/foo"); 608 assert(test.routes[1].pathParts == [PathPart(false, "foo")]); 609 assert(test.routes[1].fullPathParts == [PathPart(false, "/foo")]); 610 611 assert(test.routes[2].pattern == ":id/c"); 612 assert(test.routes[2].fullPattern == "/:id/c"); 613 assert(test.routes[2].pathParts == [PathPart(true, "id"), PathPart(false, "/c")]); 614 assert(test.routes[2].fullPathParts == [PathPart(false, "/"), PathPart(true, "id"), PathPart(false, "/c")]); 615 616 assert(test.routes[3].pattern == ":id/bar"); 617 assert(test.routes[3].fullPattern == "/:id/bar"); 618 assert(test.routes[3].pathParts == [PathPart(true, "id"), PathPart(false, "/bar")]); 619 assert(test.routes[3].fullPathParts == [PathPart(false, "/"), PathPart(true, "id"), PathPart(false, "/bar")]); 620 621 assert(test.routes[4].pattern == ":baz"); 622 assert(test.routes[4].fullPattern == "/:baz"); 623 assert(test.routes[4].pathParts == [PathPart(true, "_baz")]); 624 assert(test.routes[4].fullPathParts == [PathPart(false, "/"), PathPart(true, "_baz")]); 625 626 assert(test.routes[5].pattern == ":foo/:bar/baz"); 627 assert(test.routes[5].fullPattern == "/:foo/:bar/baz"); 628 assert(test.routes[5].pathParts == [PathPart(true, "_foo"), PathPart(false, "/"), PathPart(true, "_bar"), PathPart(false, "/baz")]); 629 assert(test.routes[5].fullPathParts == [PathPart(false, "/"), PathPart(true, "_foo"), PathPart(false, "/"), PathPart(true, "_bar"), PathPart(false, "/baz")]); 630 } 631 632 unittest { 633 // Note: the RestInterface generates routes in a specific order. 634 // since the assertions below also (indirectly) test ordering, 635 // the assertions might trigger when the ordering of the routes 636 // generated by the RestInterface changes. 637 interface Options { 638 @path("a") void getA(); 639 @path("a") void setA(); 640 @path("bar/:param") void setFoo(int _param); 641 @path("bar/:marap") void addFoo(int _marap); 642 void addFoo(); 643 void getFoo(); 644 } 645 646 auto test = RestInterface!Options(null, false); 647 import std.array : array; 648 import std.algorithm : map; 649 import std.range : dropOne, front; 650 auto options = test.getRoutesGroupedByPattern.array; 651 652 assert(options.length == 3); 653 assert(options[0].front.fullPattern == "/a"); 654 assert(options[0].dropOne.front.fullPattern == "/a"); 655 assert(options[0].map!(route=>route.method).array == [HTTPMethod.GET,HTTPMethod.PUT]); 656 657 assert(options[1].front.fullPattern == "/bar/:param"); 658 assert(options[1].dropOne.front.fullPattern == "/bar/:marap"); 659 assert(options[1].map!(route=>route.method).array == [HTTPMethod.PUT,HTTPMethod.POST]); 660 661 assert(options[2].front.fullPattern == "/foo"); 662 assert(options[2].dropOne.front.fullPattern == "/foo"); 663 assert(options[2].map!(route=>route.method).array == [HTTPMethod.POST,HTTPMethod.GET]); 664 } 665 666 unittest { 667 @rootPathFromName 668 interface Foo 669 { 670 string bar(); 671 } 672 673 auto test = RestInterface!Foo(null, false); 674 675 assert(test.routeCount == 1); 676 assert(test.routes[0].pattern == "bar"); 677 assert(test.routes[0].fullPattern == "/foo/bar"); 678 assert(test.routes[0].pathParts == [PathPart(false, "bar")]); 679 assert(test.routes[0].fullPathParts == [PathPart(false, "/foo/bar")]); 680 } 681 682 unittest { 683 @path("/foo/") 684 interface Foo 685 { 686 @path("/bar/") 687 string bar(); 688 } 689 690 auto test = RestInterface!Foo(null, false); 691 692 assert(test.routeCount == 1); 693 assert(test.routes[0].pattern == "/bar/"); 694 assert(test.routes[0].fullPattern == "/foo/bar/"); 695 assert(test.routes[0].pathParts == [PathPart(false, "/bar/")]); 696 assert(test.routes[0].fullPathParts == [PathPart(false, "/foo/bar/")]); 697 } 698 699 unittest { // #1285 700 interface I { 701 @headerParam("b", "foo") @headerParam("c", "bar") 702 void a(int a, out int b, ref int c); 703 void b(int a, @viaHeader("foo") out int b, @viaHeader("bar") ref int c); 704 } 705 alias RI = RestInterface!I; 706 static foreach (idx; 0 .. 2) 707 { 708 static assert(RI.staticRoutes[idx].parameters[0].name == "a"); 709 static assert(RI.staticRoutes[idx].parameters[0].isIn && !RI.staticRoutes[0].parameters[0].isOut); 710 static assert(RI.staticRoutes[idx].parameters[1].name == "b"); 711 static assert(!RI.staticRoutes[idx].parameters[1].isIn && RI.staticRoutes[0].parameters[1].isOut); 712 static assert(RI.staticRoutes[idx].parameters[2].name == "c"); 713 static assert(RI.staticRoutes[idx].parameters[2].isIn && RI.staticRoutes[0].parameters[2].isOut); 714 } 715 } 716 717 unittest { 718 interface Baz { 719 struct CollectionIndices { 720 string _barid; 721 int _bazid; 722 } 723 724 void test(string _barid, int _bazid); 725 void test2(string _barid); 726 } 727 728 interface Bar { 729 struct CollectionIndices { 730 string _barid; 731 } 732 733 Collection!Baz baz(string _barid); 734 735 void test(string _barid); 736 void test2(); 737 } 738 739 interface Foo { 740 Collection!Bar bar(); 741 } 742 743 auto foo = RestInterface!Foo(null, false); 744 assert(foo.subInterfaceCount == 1); 745 746 auto bar = RestInterface!Bar(foo.subInterfaces[0].settings, false); 747 assert(bar.routeCount == 2); 748 assert(bar.routes[0].fullPattern == "/bar/:barid/test"); 749 assert(bar.routes[0].pathHasPlaceholders); 750 assert(bar.routes[1].fullPattern == "/bar/test2", bar.routes[1].fullPattern); 751 assert(!bar.routes[1].pathHasPlaceholders); 752 assert(bar.subInterfaceCount == 1); 753 754 auto baz = RestInterface!Baz(bar.subInterfaces[0].settings, false); 755 assert(baz.routeCount == 2); 756 assert(baz.routes[0].fullPattern == "/bar/:barid/baz/:bazid/test"); 757 assert(baz.routes[0].pathHasPlaceholders); 758 assert(baz.routes[1].fullPattern == "/bar/:barid/baz/test2"); 759 assert(baz.routes[1].pathHasPlaceholders); 760 } 761 762 unittest { // #1648 763 import vibe.web.auth; 764 765 struct AI {} 766 767 @requiresAuth!AI 768 interface I { 769 void a(); 770 } 771 alias RI = RestInterface!I; 772 } 773 774 unittest { 775 interface I1 { @bodyParam("foo") void a(int foo); } 776 alias RI = RestInterface!I1; 777 interface I2 { @bodyParam("foo") void a(int foo, int bar); } 778 interface I3 { @bodyParam("foo") @bodyParam("bar") void a(int foo, int bar); } 779 static assert(__traits(compiles, RestInterface!I1.init)); 780 static assert(!__traits(compiles, RestInterface!I2.init)); 781 static assert(!__traits(compiles, RestInterface!I3.init)); 782 } 783 784 unittest { 785 import vibe.http.server : HTTPServerResponse, HTTPServerRequest; 786 int foocomp(HTTPServerRequest, HTTPServerResponse) { return 42; } 787 interface I { void test(int foo); } 788 class C : I { @before!foocomp("foo") void test(int foo) { assert(foo == 42); }} 789 alias RI = RestInterface!C; 790 static assert(RI.staticRoutes[0].parameters[0].kind == ParameterKind.attributed); 791 }