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