1 /** 2 Contains common functionality for the REST and WEB interface generators. 3 4 Copyright: © 2012-2017 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.common; 9 10 import vibe.http.common; 11 import vibe.http.server : HTTPServerRequest; 12 import vibe.data.json; 13 import vibe.internal.meta.uda : onlyAsUda, UDATuple; 14 15 import std.meta : AliasSeq; 16 static import std.utf; 17 static import std.string; 18 import std.traits : getUDAs, ReturnType; 19 import std.typecons : Nullable; 20 21 22 /** 23 Adjusts the naming convention for a given function name to the specified style. 24 25 The input name is assumed to be in lowerCamelCase (D-style) or PascalCase. Acronyms 26 (e.g. "HTML") should be written all caps 27 */ 28 string adjustMethodStyle(string name, MethodStyle style) 29 @safe { 30 if (!name.length) { 31 return ""; 32 } 33 34 import std.uni; 35 36 string separate(char separator, bool upper_case) 37 { 38 string ret; 39 size_t start = 0, i = 0; 40 while (i < name.length) { 41 // skip acronyms 42 while (i < name.length && (i+1 >= name.length || (name[i+1] >= 'A' && name[i+1] <= 'Z'))) { 43 std.utf.decode(name, i); 44 } 45 46 // skip the main (lowercase) part of a word 47 while (i < name.length && !(name[i] >= 'A' && name[i] <= 'Z')) { 48 std.utf.decode(name, i); 49 } 50 51 // add a single word 52 if( ret.length > 0 ) { 53 ret ~= separator; 54 } 55 ret ~= name[start .. i]; 56 57 // quick skip the capital and remember the start of the next word 58 start = i; 59 if (i < name.length) { 60 std.utf.decode(name, i); 61 } 62 } 63 if (start < name.length) { 64 ret ~= separator ~ name[start .. $]; 65 } 66 return upper_case ? std..string.toUpper(ret) : std..string.toLower(ret); 67 } 68 69 final switch(style) { 70 case MethodStyle.unaltered: 71 return name; 72 case MethodStyle.camelCase: 73 size_t i = 0; 74 foreach (idx, dchar ch; name) { 75 if (isUpper(ch)) { 76 i = idx; 77 } 78 else break; 79 } 80 if (i == 0) { 81 std.utf.decode(name, i); 82 return std..string.toLower(name[0 .. i]) ~ name[i .. $]; 83 } else { 84 std.utf.decode(name, i); 85 if (i < name.length) { 86 return std..string.toLower(name[0 .. i-1]) ~ name[i-1 .. $]; 87 } 88 else { 89 return std..string.toLower(name); 90 } 91 } 92 case MethodStyle.pascalCase: 93 size_t idx = 0; 94 std.utf.decode(name, idx); 95 return std..string.toUpper(name[0 .. idx]) ~ name[idx .. $]; 96 case MethodStyle.lowerCase: 97 return std..string.toLower(name); 98 case MethodStyle.upperCase: 99 return std..string.toUpper(name); 100 case MethodStyle.lowerUnderscored: return separate('_', false); 101 case MethodStyle.upperUnderscored: return separate('_', true); 102 case MethodStyle.lowerDashed: return separate('-', false); 103 case MethodStyle.upperDashed: return separate('-', true); 104 } 105 } 106 107 unittest 108 { 109 assert(adjustMethodStyle("methodNameTest", MethodStyle.unaltered) == "methodNameTest"); 110 assert(adjustMethodStyle("methodNameTest", MethodStyle.camelCase) == "methodNameTest"); 111 assert(adjustMethodStyle("methodNameTest", MethodStyle.pascalCase) == "MethodNameTest"); 112 assert(adjustMethodStyle("methodNameTest", MethodStyle.lowerCase) == "methodnametest"); 113 assert(adjustMethodStyle("methodNameTest", MethodStyle.upperCase) == "METHODNAMETEST"); 114 assert(adjustMethodStyle("methodNameTest", MethodStyle.lowerUnderscored) == "method_name_test"); 115 assert(adjustMethodStyle("methodNameTest", MethodStyle.upperUnderscored) == "METHOD_NAME_TEST"); 116 assert(adjustMethodStyle("MethodNameTest", MethodStyle.unaltered) == "MethodNameTest"); 117 assert(adjustMethodStyle("MethodNameTest", MethodStyle.camelCase) == "methodNameTest"); 118 assert(adjustMethodStyle("MethodNameTest", MethodStyle.pascalCase) == "MethodNameTest"); 119 assert(adjustMethodStyle("MethodNameTest", MethodStyle.lowerCase) == "methodnametest"); 120 assert(adjustMethodStyle("MethodNameTest", MethodStyle.upperCase) == "METHODNAMETEST"); 121 assert(adjustMethodStyle("MethodNameTest", MethodStyle.lowerUnderscored) == "method_name_test"); 122 assert(adjustMethodStyle("MethodNameTest", MethodStyle.upperUnderscored) == "METHOD_NAME_TEST"); 123 assert(adjustMethodStyle("MethodNameTest", MethodStyle.lowerDashed) == "method-name-test"); 124 assert(adjustMethodStyle("MethodNameTest", MethodStyle.upperDashed) == "METHOD-NAME-TEST"); 125 assert(adjustMethodStyle("Q", MethodStyle.lowerUnderscored) == "q"); 126 assert(adjustMethodStyle("getHTML", MethodStyle.lowerUnderscored) == "get_html"); 127 assert(adjustMethodStyle("getHTMLEntity", MethodStyle.lowerUnderscored) == "get_html_entity"); 128 assert(adjustMethodStyle("ID", MethodStyle.lowerUnderscored) == "id"); 129 assert(adjustMethodStyle("ID", MethodStyle.pascalCase) == "ID"); 130 assert(adjustMethodStyle("ID", MethodStyle.camelCase) == "id"); 131 assert(adjustMethodStyle("IDTest", MethodStyle.lowerUnderscored) == "id_test"); 132 assert(adjustMethodStyle("IDTest", MethodStyle.pascalCase) == "IDTest"); 133 assert(adjustMethodStyle("IDTest", MethodStyle.camelCase) == "idTest"); 134 assert(adjustMethodStyle("anyA", MethodStyle.lowerUnderscored) == "any_a", adjustMethodStyle("anyA", MethodStyle.lowerUnderscored)); 135 } 136 137 138 /** 139 Determines the HTTP method and path for a given function symbol. 140 141 The final method and path are determined from the function name, as well as 142 any $(D @method) and $(D @path) attributes that may be applied to it. 143 144 This function is designed for CTFE usage and will assert at run time. 145 146 Returns: 147 A tuple of three elements is returned: 148 $(UL 149 $(LI flag "was UDA used to override path") 150 $(LI $(D HTTPMethod) extracted) 151 $(LI URL path extracted) 152 ) 153 */ 154 auto extractHTTPMethodAndName(alias Func, bool indexSpecialCase)() 155 { 156 if (!__ctfe) 157 assert(false); 158 159 struct HandlerMeta 160 { 161 bool hadPathUDA; 162 HTTPMethod method; 163 string url; 164 } 165 166 import vibe.internal.meta.uda : findFirstUDA; 167 import vibe.internal.meta.traits : isPropertySetter, 168 isPropertyGetter; 169 import std.algorithm : startsWith; 170 import std.typecons : Nullable; 171 172 immutable httpMethodPrefixes = [ 173 HTTPMethod.GET : [ "get", "query" ], 174 HTTPMethod.PUT : [ "put", "set" ], 175 HTTPMethod.PATCH : [ "update", "patch" ], 176 HTTPMethod.POST : [ "add", "create", "post" ], 177 HTTPMethod.DELETE : [ "remove", "erase", "delete" ], 178 ]; 179 180 enum name = __traits(identifier, Func); 181 alias T = typeof(&Func); 182 183 Nullable!HTTPMethod udmethod; 184 Nullable!string udurl; 185 186 // Cases may conflict and are listed in order of priority 187 188 // Workaround for Nullable incompetence 189 enum uda1 = findFirstUDA!(MethodAttribute, Func); 190 enum uda2 = findFirstUDA!(PathAttribute, Func); 191 192 static if (uda1.found) { 193 udmethod = uda1.value; 194 } 195 static if (uda2.found) { 196 udurl = uda2.value; 197 } 198 199 // Everything is overriden, no further analysis needed 200 if (!udmethod.isNull() && !udurl.isNull()) { 201 return HandlerMeta(true, udmethod.get(), udurl.get()); 202 } 203 204 // Anti-copy-paste delegate 205 typeof(return) udaOverride( HTTPMethod method, string url ){ 206 return HandlerMeta( 207 !udurl.isNull(), 208 udmethod.isNull() ? method : udmethod.get(), 209 udurl.isNull() ? url : udurl.get() 210 ); 211 } 212 213 if (isPropertyGetter!T) { 214 return udaOverride(HTTPMethod.GET, name); 215 } 216 else if(isPropertySetter!T) { 217 return udaOverride(HTTPMethod.PUT, name); 218 } 219 else { 220 foreach (method, prefixes; httpMethodPrefixes) { 221 foreach (prefix; prefixes) { 222 import std.uni : isLower; 223 if (name.startsWith(prefix) && (name.length == prefix.length || !name[prefix.length].isLower)) { 224 string tmp = name[prefix.length..$]; 225 return udaOverride(method, tmp.length ? tmp : "/"); 226 } 227 } 228 } 229 230 static if (indexSpecialCase && name == "index") { 231 return udaOverride(HTTPMethod.GET, "/"); 232 } else 233 return udaOverride(HTTPMethod.POST, name); 234 } 235 } 236 237 unittest 238 { 239 interface Sample 240 { 241 string getInfo(); 242 string updateDescription(); 243 244 @method(HTTPMethod.DELETE) 245 string putInfo(); 246 247 @path("matters") 248 string getMattersnot(); 249 250 @path("compound/path") @method(HTTPMethod.POST) 251 string mattersnot(); 252 253 string get(); 254 255 string posts(); 256 257 string patches(); 258 } 259 260 enum ret1 = extractHTTPMethodAndName!(Sample.getInfo, false,); 261 static assert (ret1.hadPathUDA == false); 262 static assert (ret1.method == HTTPMethod.GET); 263 static assert (ret1.url == "Info"); 264 enum ret2 = extractHTTPMethodAndName!(Sample.updateDescription, false); 265 static assert (ret2.hadPathUDA == false); 266 static assert (ret2.method == HTTPMethod.PATCH); 267 static assert (ret2.url == "Description"); 268 enum ret3 = extractHTTPMethodAndName!(Sample.putInfo, false); 269 static assert (ret3.hadPathUDA == false); 270 static assert (ret3.method == HTTPMethod.DELETE); 271 static assert (ret3.url == "Info"); 272 enum ret4 = extractHTTPMethodAndName!(Sample.getMattersnot, false); 273 static assert (ret4.hadPathUDA == true); 274 static assert (ret4.method == HTTPMethod.GET); 275 static assert (ret4.url == "matters"); 276 enum ret5 = extractHTTPMethodAndName!(Sample.mattersnot, false); 277 static assert (ret5.hadPathUDA == true); 278 static assert (ret5.method == HTTPMethod.POST); 279 static assert (ret5.url == "compound/path"); 280 enum ret6 = extractHTTPMethodAndName!(Sample.get, false); 281 static assert (ret6.hadPathUDA == false); 282 static assert (ret6.method == HTTPMethod.GET); 283 static assert (ret6.url == "/"); 284 enum ret7 = extractHTTPMethodAndName!(Sample.posts, false); 285 static assert(ret7.hadPathUDA == false); 286 static assert(ret7.method == HTTPMethod.POST); 287 static assert(ret7.url == "posts"); 288 enum ret8 = extractHTTPMethodAndName!(Sample.patches, false); 289 static assert(ret8.hadPathUDA == false); 290 static assert(ret8.method == HTTPMethod.POST); 291 static assert(ret8.url == "patches"); 292 } 293 294 295 /** 296 Attribute to define the content type for methods. 297 298 This currently applies only to methods returning an $(D InputStream) or 299 $(D ubyte[]). 300 */ 301 ContentTypeAttribute contentType(string data) 302 @safe { 303 if (!__ctfe) 304 assert(false, onlyAsUda!__FUNCTION__); 305 return ContentTypeAttribute(data); 306 } 307 308 309 /** 310 Attribute to force a specific HTTP method for an interface method. 311 312 The usual URL generation rules are still applied, so if there 313 are any "get", "query" or similar prefixes, they are filtered out. 314 */ 315 MethodAttribute method(HTTPMethod data) 316 @safe { 317 if (!__ctfe) 318 assert(false, onlyAsUda!__FUNCTION__); 319 return MethodAttribute(data); 320 } 321 322 /// 323 unittest { 324 interface IAPI 325 { 326 // Will be "POST /info" instead of default "GET /info" 327 @method(HTTPMethod.POST) string getInfo(); 328 } 329 } 330 331 332 /** 333 Attibute to force a specific URL path. 334 335 This attribute can be applied either to an interface itself, in which 336 case it defines the root path for all methods within it, 337 or on any function, in which case it defines the relative path 338 of this method. 339 Path are always relative, even path on interfaces, as you can 340 see in the example below. 341 342 See_Also: $(D rootPathFromName) for automatic name generation. 343 */ 344 PathAttribute path(string data) 345 @safe { 346 if (!__ctfe) 347 assert(false, onlyAsUda!__FUNCTION__); 348 return PathAttribute(data); 349 } 350 351 /// 352 @safe unittest { 353 @path("/foo") 354 interface IAPI 355 { 356 @path("info2") string getInfo() @safe; 357 } 358 359 class API : IAPI { 360 string getInfo() @safe { return "Hello, World!"; } 361 } 362 363 void test() 364 @safe { 365 import vibe.http.router; 366 import vibe.web.rest; 367 368 auto router = new URLRouter; 369 370 // Tie IAPI.getInfo to "GET /root/foo/info2" 371 router.registerRestInterface!IAPI(new API(), "/root/"); 372 373 // Or just to "GET /foo/info2" 374 router.registerRestInterface!IAPI(new API()); 375 376 // ... 377 } 378 } 379 380 381 /// Convenience alias to generate a name from the interface's name. 382 @property PathAttribute rootPathFromName() 383 @safe { 384 if (!__ctfe) 385 assert(false, onlyAsUda!__FUNCTION__); 386 return PathAttribute(""); 387 } 388 /// 389 @safe unittest 390 { 391 import vibe.http.router; 392 import vibe.web.rest; 393 394 @rootPathFromName 395 interface IAPI 396 { 397 int getFoo() @safe; 398 } 399 400 class API : IAPI 401 { 402 int getFoo() 403 { 404 return 42; 405 } 406 } 407 408 auto router = new URLRouter(); 409 registerRestInterface(router, new API()); 410 auto routes= router.getAllRoutes(); 411 412 assert(routes[0].pattern == "/iapi/foo" && routes[0].method == HTTPMethod.GET); 413 } 414 415 416 /** 417 Methods marked with this attribute will not be treated as web endpoints. 418 419 This attribute enables the definition of public methods that do not take 420 part in the interface genration process. 421 */ 422 @property NoRouteAttribute noRoute() 423 { 424 import vibe.web.common : onlyAsUda; 425 if (!__ctfe) 426 assert(false, onlyAsUda!__FUNCTION__); 427 return NoRouteAttribute.init; 428 } 429 430 /// 431 unittest { 432 interface IAPI { 433 // Accessible as "GET /info" 434 string getInfo(); 435 436 // Not accessible over HTTP 437 @noRoute 438 int getFoo(); 439 } 440 } 441 442 443 /** 444 Respresents a Rest error response 445 */ 446 class RestException : HTTPStatusException { 447 private { 448 Json m_jsonResult; 449 } 450 451 /// 452 this (int status, string result, string file = __FILE__, int line = __LINE__, 453 Throwable next = null) @safe 454 { 455 Json jsonResult = Json.emptyObject; 456 jsonResult["statusMessage"] = result; 457 this(status, jsonResult, file, line); 458 } 459 460 /// 461 this (int status, Json jsonResult, string file = __FILE__, int line = __LINE__, 462 Throwable next = null) @safe 463 { 464 if (jsonResult.type == Json.Type.object && jsonResult["statusMessage"].type == Json.Type..string) { 465 super(status, jsonResult["statusMessage"].get!string, file, line, next); 466 } 467 else { 468 super(status, httpStatusText(status) ~ " (" ~ jsonResult.toString() ~ ")", file, line, next); 469 } 470 471 m_jsonResult = jsonResult; 472 } 473 474 /// The result text reported to the client 475 @property inout(Json) jsonResult () inout nothrow pure @safe @nogc 476 { 477 return m_jsonResult; 478 } 479 } 480 481 /// private 482 package struct ContentTypeAttribute 483 { 484 string data; 485 alias data this; 486 } 487 488 /// private 489 package struct MethodAttribute 490 { 491 HTTPMethod data; 492 alias data this; 493 } 494 495 496 /** UDA for using a custom serializer for the method return value. 497 498 Instead of using the default serializer (JSON), this allows to define 499 custom serializers. Multiple serializers can be specified and will be 500 matched against the `Accept` header of the HTTP request. 501 502 Params: 503 serialize = An alias to a generic function taking an output range as 504 its first argument and the value to be serialized as its second 505 argument. The result of the serialization is written byte-wise into 506 the output range. 507 deserialize = An alias to a generic function taking a forward range 508 as its first argument and a reference to the value that is to be 509 deserialized. 510 content_type = The MIME type of the serialized representation. 511 */ 512 alias resultSerializer(alias serialize, alias deserialize, string content_type) 513 = ResultSerializer!(serialize, deserialize, content_type); 514 515 /// 516 unittest { 517 import std.bitmanip : bigEndianToNative, nativeToBigEndian; 518 519 interface MyRestInterface { 520 static struct Point { 521 int x, y; 522 } 523 524 static void serialize(R, T)(ref R output_range, const ref T value) 525 { 526 static assert(is(T == Point)); // only Point supported in this example 527 output_range.put(nativeToBigEndian(value.x)); 528 output_range.put(nativeToBigEndian(value.y)); 529 } 530 531 static T deserialize(T, R)(R input_range) 532 { 533 static assert(is(T == Point)); // only Point supported in this example 534 T ret; 535 ubyte[4] xbuf, ybuf; 536 input_range.takeExactly(4).copy(xbuf[]); 537 input_range.takeExactly(4).copy(ybuf[]); 538 ret.x = bigEndianToNative!int(xbuf); 539 ret.y = bigEndianToNative!int(ybuf); 540 return ret; 541 } 542 543 // serialize as binary data in network byte order 544 @resultSerializer!(serialize, deserialize, "application/binary") 545 Point getPoint(); 546 } 547 } 548 549 /// private 550 struct ResultSerializer(alias ST, alias DT, string ContentType) { 551 enum contentType = ContentType; 552 alias serialize = ST; 553 alias deserialize = DT; 554 } 555 556 557 package void defaultSerialize (alias P, T, RT) (ref RT output_range, const scope ref T value) 558 { 559 static struct R { 560 typeof(output_range) underlying; 561 void put(char ch) { underlying.put(ch); } 562 void put(scope const(char)[] ch) { underlying.put(cast(const(ubyte)[])ch); } 563 } 564 auto dst = R(output_range); 565 // NOTE: serializeWithPolicy does not take value as scope due to issues 566 // deeply buried in the standard library 567 () @trusted { return value; } ().serializeWithPolicy!(JsonStringSerializer!R, P) (dst); 568 } 569 570 package T defaultDeserialize (alias P, T, R) (R input_range) 571 { 572 return deserializeWithPolicy!(JsonStringSerializer!(typeof(std..string.assumeUTF(input_range))), P, T) 573 (std..string.assumeUTF(input_range)); 574 } 575 576 package alias DefaultSerializerT = ResultSerializer!( 577 defaultSerialize, defaultDeserialize, "application/json; charset=UTF-8"); 578 579 580 /// Convenience template to get all the ResultSerializers for a function 581 package template ResultSerializersT(alias func) { 582 alias DefinedSerializers = getUDAs!(func, ResultSerializer); 583 static if (DefinedSerializers.length) 584 alias ResultSerializersT = DefinedSerializers; 585 else 586 alias ResultSerializersT = AliasSeq!(DefaultSerializerT); 587 } 588 589 /// 590 package template SerPolicyT (Iface) 591 { 592 static if (getUDAs!(Iface, SerPolicy).length) 593 { 594 alias SerPolicyT = getUDAs!(Iface, SerPolicy)[0]; 595 } 596 else 597 { 598 alias SerPolicyT = SerPolicy!DefaultPolicy; 599 } 600 } 601 602 /// 603 package struct SerPolicy (alias PolicyTemplatePar) 604 { 605 alias PolicyTemplate = PolicyTemplatePar; 606 } 607 608 /// 609 public alias serializationPolicy (Args...) = SerPolicy!(Args); 610 611 unittest 612 { 613 import vibe.data.serialization : Base64ArrayPolicy; 614 import std.array : appender; 615 import std.conv : to; 616 617 struct X 618 { 619 string name = "test"; 620 ubyte[] arr = [138, 245, 231, 234, 142, 132, 142]; 621 } 622 X x; 623 624 // Interface using Base64 array serialization 625 @serializationPolicy!(Base64ArrayPolicy) 626 interface ITestBase64 627 { 628 @safe X getTest(); 629 } 630 631 alias serPolicyFound = SerPolicyT!ITestBase64; 632 alias resultSerializerFound = ResultSerializersT!(ITestBase64.getTest)[0]; 633 634 // serialization test with base64 encoding 635 auto output = appender!string(); 636 637 resultSerializerFound.serialize!(serPolicyFound.PolicyTemplate)(output, x); 638 auto serialized = output.data; 639 assert(serialized == `{"name":"test","arr":"ivXn6o6Ejg=="}`, 640 "serialization is not correct, produced: " ~ serialized); 641 642 // deserialization test with base64 encoding 643 auto deserialized = serialized.deserializeWithPolicy!(JsonStringSerializer!string, serPolicyFound.PolicyTemplate, X)(); 644 assert(deserialized.name == "test", "deserialization of `name` is not correct, produced: " ~ deserialized.name); 645 assert(deserialized.arr == [138, 245, 231, 234, 142, 132, 142], 646 "deserialization of `arr` is not correct, produced: " ~ to!string(deserialized.arr)); 647 648 // Interface NOT using Base64 array serialization 649 interface ITestPlain 650 { 651 @safe X getTest(); 652 } 653 654 alias plainSerPolicyFound = SerPolicyT!ITestPlain; 655 alias plainResultSerializerFound = ResultSerializersT!(ITestPlain.getTest)[0]; 656 657 // serialization test without base64 encoding 658 output = appender!string(); 659 plainResultSerializerFound.serialize!(plainSerPolicyFound.PolicyTemplate)(output, x); 660 serialized = output.data; 661 assert(serialized == `{"name":"test","arr":[138,245,231,234,142,132,142]}`, 662 "serialization is not correct, produced: " ~ serialized); 663 664 // deserialization test without base64 encoding 665 deserialized = serialized.deserializeWithPolicy!(JsonStringSerializer!string, plainSerPolicyFound.PolicyTemplate, X)(); 666 assert(deserialized.name == "test", "deserialization of `name` is not correct, produced: " ~ deserialized.name); 667 assert(deserialized.arr == [138, 245, 231, 234, 142, 132, 142], 668 "deserialization of `arr` is not correct, produced: " ~ to!string(deserialized.arr)); 669 } 670 671 /** 672 * This struct contains the name of a route specified by the `path` function. 673 */ 674 struct PathAttribute 675 { 676 /// The specified path 677 string data; 678 alias data this; 679 } 680 681 /// private 682 package struct NoRouteAttribute {} 683 684 /** 685 * This struct contains a mapping between the name used by HTTP (field) 686 * and the parameter (identifier) name of the function. 687 */ 688 public struct WebParamAttribute { 689 import vibe.web.internal.rest.common : ParameterKind; 690 691 /// The type of the WebParamAttribute 692 ParameterKind origin; 693 /// Parameter name (function parameter name). 694 string identifier; 695 /// The meaning of this field depends on the origin. (HTTP request name) 696 string field; 697 } 698 699 700 /** 701 * Declare that a parameter will be transmitted to the API through the body. 702 * 703 * It will be serialized as part of a JSON object. 704 * The serialization format is currently not customizable. 705 * If no fieldname is given, the entire body is serialized into the object. 706 * 707 * There are currently two kinds of symbol to do this: `viaBody` and `bodyParam`. 708 * `viaBody` should be applied to the parameter itself, while `bodyParam` 709 * is applied to the function. 710 * `bodyParam` was introduced long before the D language for UDAs on parameters 711 * (introduced in DMD v2.082.0), and will be deprecated in a future release. 712 * 713 * Params: 714 * identifier = The name of the parameter to customize. A compiler error will be issued on mismatch. 715 * field = The name of the field in the JSON object. 716 * 717 * ---- 718 * void ship(@viaBody("package") int pack); 719 * // The server will receive the following body for a call to ship(42): 720 * // { "package": 42 } 721 * ---- 722 */ 723 WebParamAttribute viaBody(string field = null) 724 @safe { 725 import vibe.web.internal.rest.common : ParameterKind; 726 if (!__ctfe) 727 assert(false, onlyAsUda!__FUNCTION__); 728 return WebParamAttribute(ParameterKind.body_, null, field); 729 } 730 731 /// Ditto 732 WebParamAttribute bodyParam(string identifier, string field) @safe 733 in { 734 assert(field.length > 0, "fieldname can't be empty."); 735 } 736 do 737 { 738 import vibe.web.internal.rest.common : ParameterKind; 739 if (!__ctfe) 740 assert(false, onlyAsUda!__FUNCTION__); 741 return WebParamAttribute(ParameterKind.body_, identifier, field); 742 } 743 744 /// ditto 745 WebParamAttribute bodyParam(string identifier) 746 @safe { 747 import vibe.web.internal.rest.common : ParameterKind; 748 if (!__ctfe) 749 assert(false, onlyAsUda!__FUNCTION__); 750 return WebParamAttribute(ParameterKind.body_, identifier, ""); 751 } 752 753 /** 754 * Declare that a parameter will be transmitted to the API through the headers. 755 * 756 * If the parameter is a string, or any scalar type (float, int, char[], ...), it will be send as a string. 757 * If it's an aggregate, it will be serialized as JSON. 758 * However, passing aggregate via header isn't a good practice and should be avoided for new production code. 759 * 760 * There are currently two kinds of symbol to do this: `viaHeader` and `headerParam`. 761 * `viaHeader` should be applied to the parameter itself, while `headerParam` 762 * is applied to the function. 763 * `headerParam` was introduced long before the D language for UDAs on parameters 764 * (introduced in DMD v2.082.0), and will be deprecated in a future release. 765 * 766 * Params: 767 * identifier = The name of the parameter to customize. A compiler error will be issued on mismatch. 768 * field = The name of the header field to use (e.g: 'Accept', 'Content-Type'...). 769 * 770 * ---- 771 * // The server will receive the content of the "Authorization" header. 772 * void login(@viaHeader("Authorization") string auth); 773 * ---- 774 */ 775 WebParamAttribute viaHeader(string field) 776 @safe { 777 import vibe.web.internal.rest.common : ParameterKind; 778 if (!__ctfe) 779 assert(false, onlyAsUda!__FUNCTION__); 780 return WebParamAttribute(ParameterKind.header, null, field); 781 } 782 783 /// Ditto 784 WebParamAttribute headerParam(string identifier, string field) 785 @safe { 786 import vibe.web.internal.rest.common : ParameterKind; 787 if (!__ctfe) 788 assert(false, onlyAsUda!__FUNCTION__); 789 return WebParamAttribute(ParameterKind.header, identifier, field); 790 } 791 792 /** 793 * Declare that a parameter will be transmitted to the API through the query string. 794 * 795 * It will be serialized as part of a JSON object, and will go through URL serialization. 796 * The serialization format is not customizable. 797 * 798 * There are currently two kinds of symbol to do this: `viaQuery` and `queryParam`. 799 * `viaQuery` should be applied to the parameter itself, while `queryParam` 800 * is applied to the function. 801 * `queryParam` was introduced long before the D language for UDAs on parameters 802 * (introduced in DMD v2.082.0), and will be deprecated in a future release. 803 * 804 * Params: 805 * identifier = The name of the parameter to customize. A compiler error will be issued on mismatch. 806 * field = The field name to use. 807 * 808 * ---- 809 * // For a call to postData("D is awesome"), the server will receive the query: 810 * // POST /data?test=%22D is awesome%22 811 * void postData(@viaQuery("test") string data); 812 * ---- 813 */ 814 WebParamAttribute viaQuery(string field) 815 @safe { 816 import vibe.web.internal.rest.common : ParameterKind; 817 if (!__ctfe) 818 assert(false, onlyAsUda!__FUNCTION__); 819 return WebParamAttribute(ParameterKind.query, null, field); 820 } 821 822 /// Ditto 823 WebParamAttribute queryParam(string identifier, string field) 824 @safe { 825 import vibe.web.internal.rest.common : ParameterKind; 826 if (!__ctfe) 827 assert(false, onlyAsUda!__FUNCTION__); 828 return WebParamAttribute(ParameterKind.query, identifier, field); 829 } 830 831 /** 832 Determines the naming convention of an identifier. 833 */ 834 enum MethodStyle 835 { 836 /// Special value for free-style conventions 837 unaltered, 838 /// camelCaseNaming 839 camelCase, 840 /// PascalCaseNaming 841 pascalCase, 842 /// lowercasenaming 843 lowerCase, 844 /// UPPERCASENAMING 845 upperCase, 846 /// lower_case_naming 847 lowerUnderscored, 848 /// UPPER_CASE_NAMING 849 upperUnderscored, 850 /// lower-case-naming 851 lowerDashed, 852 /// UPPER-CASE-NAMING 853 upperDashed, 854 } 855 856 857 /// Speficies how D fields are mapped to form field names 858 enum NestedNameStyle { 859 underscore, /// Use underscores to separate fields and array indices 860 d /// Use native D style and separate fields by dots and put array indices into brackets 861 } 862 863 864 // concatenates two URL parts avoiding any duplicate slashes 865 // in resulting URL. `trailing` defines of result URL must 866 // end with slash 867 package string concatURL(string prefix, string url, bool trailing = false) 868 @safe { 869 import std.algorithm : startsWith, endsWith; 870 871 auto pre = prefix.endsWith("/"); 872 auto post = url.startsWith("/"); 873 874 if (!url.length) return trailing && !pre ? prefix ~ "/" : prefix; 875 876 auto suffix = trailing && !url.endsWith("/") ? "/" : null; 877 878 if (pre) { 879 // "/" is ASCII, so can just slice 880 if (post) return prefix ~ url[1 .. $] ~ suffix; 881 else return prefix ~ url ~ suffix; 882 } else { 883 if (post) return prefix ~ url ~ suffix; 884 else return prefix ~ "/" ~ url ~ suffix; 885 } 886 } 887 888 @safe unittest { 889 assert(concatURL("/test/", "/it/", false) == "/test/it/"); 890 assert(concatURL("/test", "it/", false) == "/test/it/"); 891 assert(concatURL("/test", "it", false) == "/test/it"); 892 assert(concatURL("/test", "", false) == "/test"); 893 assert(concatURL("/test/", "", false) == "/test/"); 894 assert(concatURL("/test/", "/it/", true) == "/test/it/"); 895 assert(concatURL("/test", "it/", true) == "/test/it/"); 896 assert(concatURL("/test", "it", true) == "/test/it/"); 897 assert(concatURL("/test", "", true) == "/test/"); 898 assert(concatURL("/test/", "", true) == "/test/"); 899 } 900 901 902 /// private 903 template isNullable(T) { 904 import std.traits; 905 enum isNullable = isInstanceOf!(Nullable, T); 906 } 907 908 static assert(isNullable!(Nullable!int)); 909 910 package struct ParamError { 911 string field; 912 string text; 913 } 914 915 package enum ParamResult { 916 ok, 917 skipped, 918 error 919 } 920 921 // maximum array index in the parameter fields. 922 private enum MAX_ARR_INDEX = 0xffff; 923 924 // handle the actual data inside the parameter 925 private ParamResult processFormParam(T)(scope string data, string fieldname, ref T dst, ref ParamError err) 926 { 927 static if (is(T == bool)) 928 { 929 // getting here means the item is present, set to true. 930 dst = true; 931 return ParamResult.ok; 932 } 933 else 934 { 935 if (!data.webConvTo(dst, err)) { 936 err.field = fieldname; 937 return ParamResult.error; 938 } 939 return ParamResult.ok; 940 } 941 } 942 943 // NOTE: dst is assumed to be uninitialized 944 package ParamResult readFormParamRec(T)(scope HTTPServerRequest req, ref T dst, string fieldname, bool required, NestedNameStyle style, ref ParamError err) 945 { 946 import std.traits; 947 import std.typecons; 948 import vibe.data.serialization; 949 import std.algorithm : startsWith; 950 951 static if (isStaticArray!T || (isDynamicArray!T && !isSomeString!(OriginalType!T))) { 952 alias EL = typeof(T.init[0]); 953 enum isSimpleElement = !(isDynamicArray!EL && !isSomeString!(OriginalType!EL)) && 954 !isStaticArray!EL && 955 !(is(EL == struct) && 956 !is(typeof(EL.fromString(string.init))) && 957 !is(typeof(EL.fromStringValidate(string.init, null))) && 958 !is(typeof(EL.fromISOExtString(string.init)))); 959 960 static if (isStaticArray!T) 961 { 962 bool[T.length] seen; 963 } 964 else 965 { 966 static assert(!is(EL == bool), 967 "Boolean arrays are not allowed, because their length cannot " ~ 968 "be uniquely determined. Use a static array instead."); 969 // array to check for duplicates 970 dst = T.init; 971 bool[] seen; 972 } 973 // process the items in the order they appear. 974 char indexSep = style == NestedNameStyle.d ? '[' : '_'; 975 const minLength = fieldname.length + (style == NestedNameStyle.d ? 2 : 1); 976 const indexTrailer = style == NestedNameStyle.d ? "]" : ""; 977 978 ParamResult processItems(DL)(DL dlist) 979 { 980 foreach (k, v; dlist.byKeyValue) 981 { 982 if (k.length < minLength) 983 // sanity check to prevent out of bounds 984 continue; 985 if (k.startsWith(fieldname) && k[fieldname.length] == indexSep) 986 { 987 // found a potential match 988 string key = k[fieldname.length + 1 .. $]; 989 size_t idx; 990 if (key == indexTrailer) 991 { 992 static if (isSimpleElement) 993 { 994 // this is a non-indexed array item. Find an empty slot, or expand the array 995 import std.algorithm : countUntil; 996 idx = seen[].countUntil(false); 997 static if (isStaticArray!T) 998 { 999 if (idx == size_t.max) 1000 { 1001 // ignore extras, and we know there are no more matches to come. 1002 break; 1003 } 1004 } 1005 else if (idx == size_t.max) 1006 { 1007 // append to the end. 1008 idx = dst.length; 1009 } 1010 } 1011 else 1012 { 1013 // not valid for non-simple elements. 1014 continue; 1015 } 1016 } 1017 else 1018 { 1019 import std.conv; 1020 idx = key.parse!size_t; 1021 static if (isStaticArray!T) 1022 { 1023 if (idx >= T.length) 1024 // keep existing behavior, ignore extras 1025 continue; 1026 } 1027 else if (idx > MAX_ARR_INDEX) 1028 { 1029 // Getting a bit large, we don't want to allow DOS attacks. 1030 err.field = k; 1031 err.text = "Maximum index exceeded"; 1032 return ParamResult.error; 1033 } 1034 static if (isSimpleElement) 1035 { 1036 if (key != indexTrailer) 1037 // this can't be a match, simple elements are parsed from 1038 // the string, there should be no more to the key. 1039 continue; 1040 } 1041 else 1042 { 1043 // ensure there's more than just the index trailer 1044 if (key.length == indexTrailer.length || !key.startsWith(indexTrailer)) 1045 // not a valid entry. ignore this entry to preserve existing behavior. 1046 continue; 1047 } 1048 } 1049 1050 static if (!isStaticArray!T) 1051 { 1052 // check to see if we need to expand the array 1053 if (dst.length <= idx) 1054 { 1055 dst.length = idx + 1; 1056 seen.length = idx + 1; 1057 } 1058 } 1059 1060 if (seen[idx]) 1061 { 1062 // don't store it twice 1063 continue; 1064 } 1065 seen[idx] = true; 1066 1067 static if (isSimpleElement) 1068 { 1069 auto result = processFormParam(v, k, dst[idx], err); 1070 } 1071 else 1072 { 1073 auto subFieldname = k[0 .. $ - key.length + indexTrailer.length]; 1074 auto result = readFormParamRec(req, dst[idx], subFieldname, true, style, err); 1075 } 1076 if (result != ParamResult.ok) 1077 return result; 1078 } 1079 } 1080 1081 return ParamResult.ok; 1082 } 1083 1084 if (processItems(req.form) == ParamResult.error) 1085 return ParamResult.error; 1086 if (processItems(req.query) == ParamResult.error) 1087 return ParamResult.error; 1088 1089 // make sure all static array items have been seen 1090 static if (isStaticArray!T) 1091 { 1092 import std.algorithm : countUntil; 1093 auto notSeen = seen[].countUntil(false); 1094 if (notSeen != -1) 1095 { 1096 err.field = style.getArrayFieldName(fieldname, notSeen); 1097 err.text = "Missing array form field entry."; 1098 return ParamResult.error; 1099 } 1100 } 1101 } else static if (isNullable!T) { 1102 typeof(dst.get()) el = void; 1103 auto r = readFormParamRec(req, el, fieldname, false, style, err); 1104 final switch (r) { 1105 case ParamResult.ok: dst.setVoid(el); break; 1106 case ParamResult.skipped: dst.setVoid(T.init); break; 1107 case ParamResult.error: return ParamResult.error; 1108 } 1109 } else static if (is(T == struct) && 1110 !is(typeof(T.fromString(string.init))) && 1111 !is(typeof(T.fromStringValidate(string.init, null))) && 1112 !is(typeof(T.fromISOExtString(string.init)))) 1113 { 1114 foreach (m; __traits(allMembers, T)) { 1115 auto r = readFormParamRec(req, __traits(getMember, dst, m), style.getMemberFieldName(fieldname, m), required, style, err); 1116 if (r != ParamResult.ok) 1117 return r; // FIXME: in case of errors the struct will be only partially initialized! All previous fields should be deinitialized first. 1118 } 1119 } else static if (is(T == bool)) { 1120 dst = (fieldname in req.form) !is null || (fieldname in req.query) !is null; 1121 } else if (auto pv = fieldname in req.form) { 1122 if (!(*pv).webConvTo(dst, err)) { 1123 err.field = fieldname; 1124 return ParamResult.error; 1125 } 1126 } else if (auto pv = fieldname in req.query) { 1127 if (!(*pv).webConvTo(dst, err)) { 1128 err.field = fieldname; 1129 return ParamResult.error; 1130 } 1131 } else if (required) { 1132 err.field = fieldname; 1133 err.text = "Missing form field."; 1134 return ParamResult.error; 1135 } 1136 else return ParamResult.skipped; 1137 1138 return ParamResult.ok; 1139 } 1140 1141 // test new array mechanisms 1142 unittest { 1143 import vibe.http.server; 1144 import vibe.inet.url; 1145 1146 auto req = createTestHTTPServerRequest(URL("http://localhost/route?arr_0=1&arr_2=2&arr_=3")); 1147 int[] arr; 1148 ParamError err; 1149 auto result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.underscore, err); 1150 assert(result == ParamResult.ok); 1151 assert(arr == [1,3,2]); 1152 1153 // try with static array 1154 int[3] staticarr; 1155 result = req.readFormParamRec(staticarr, "arr", false, NestedNameStyle.underscore, err); 1156 assert(result == ParamResult.ok); 1157 assert(staticarr == [1,3,2]); 1158 1159 // d style 1160 req = createTestHTTPServerRequest(URL("http://localhost/route?arr[2]=1&arr[0]=2&arr[]=3")); 1161 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1162 assert(result == ParamResult.ok); 1163 assert(arr == [2,3,1]); 1164 1165 result = req.readFormParamRec(staticarr, "arr", false, NestedNameStyle.d, err); 1166 assert(result == ParamResult.ok); 1167 assert(staticarr == [2,3,1]); 1168 1169 // try nested arrays 1170 req = createTestHTTPServerRequest(URL("http://localhost/route?arr[2][]=1&arr[0][]=2&arr[1][]=3&arr[0][]=4")); 1171 int[][] arr2; 1172 result = req.readFormParamRec(arr2, "arr", false, NestedNameStyle.d, err); 1173 assert(result == ParamResult.ok); 1174 assert(arr2 == [[2,4],[3],[1]]); 1175 1176 int[][2] staticarr2; 1177 result = req.readFormParamRec(staticarr2, "arr", false, NestedNameStyle.d, err); 1178 assert(result == ParamResult.ok); 1179 assert(staticarr2 == [[2,4],[3]]); 1180 1181 // bug with key length 1182 req = createTestHTTPServerRequest(URL("http://localhost/route?arr=1")); 1183 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1184 assert(result == ParamResult.ok); 1185 assert(arr.length == 0); 1186 } 1187 1188 unittest { // complex array parameters 1189 import vibe.http.server; 1190 import vibe.inet.url; 1191 1192 static struct S { 1193 int a, b; 1194 } 1195 1196 S[] arr; 1197 ParamError err; 1198 1199 // d style 1200 auto req = createTestHTTPServerRequest(URL("http://localhost/route?arr[0].a=1&arr[0].b=2")); 1201 auto result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1202 assert(result == ParamResult.ok); 1203 assert(arr == [S(1, 2)]); 1204 1205 // underscore style 1206 req = createTestHTTPServerRequest(URL("http://localhost/route?arr_0_a=1&arr_0_b=2")); 1207 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.underscore, err); 1208 assert(result == ParamResult.ok); 1209 assert(arr == [S(1, 2)]); 1210 } 1211 1212 package bool webConvTo(T)(string str, ref T dst, ref ParamError err) 1213 nothrow { 1214 import std.conv; 1215 import std.exception; 1216 try { 1217 static if (is(typeof(T.fromStringValidate(str, &err.text)))) { 1218 static assert(is(typeof(T.fromStringValidate(str, &err.text)) == Nullable!T)); 1219 auto res = T.fromStringValidate(str, &err.text); 1220 if (res.isNull()) return false; 1221 dst.setVoid(res.get); 1222 } else static if (is(typeof(T.fromString(str)))) { 1223 static assert(is(typeof(T.fromString(str)) == T)); 1224 dst.setVoid(T.fromString(str)); 1225 } else static if (is(typeof(T.fromISOExtString(str)))) { 1226 static assert(is(typeof(T.fromISOExtString(str)) == T)); 1227 dst.setVoid(T.fromISOExtString(str)); 1228 } else { 1229 dst.setVoid(str.to!T()); 1230 } 1231 } catch (Exception e) { 1232 import vibe.core.log : logDebug; 1233 import std.encoding : sanitize; 1234 err.text = e.msg; 1235 debug try logDebug("Error converting web field: %s", e.toString().sanitize); 1236 catch (Exception) {} 1237 return false; 1238 } 1239 return true; 1240 } 1241 1242 // properly sets an uninitialized variable 1243 package void setVoid(T, U)(ref T dst, U value) 1244 { 1245 import std.traits; 1246 static if (hasElaborateAssign!T) { 1247 static if (is(T == U)) { 1248 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&value)[0 .. T.sizeof]; 1249 typeid(T).postblit(&dst); 1250 } else { 1251 static T init = T.init; 1252 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&init)[0 .. T.sizeof]; 1253 dst = value; 1254 } 1255 } else dst = value; 1256 } 1257 1258 unittest { 1259 static assert(!__traits(compiles, { bool[] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1260 static assert(__traits(compiles, { bool[2] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1261 1262 enum Test: string { a = "AAA", b="BBB" } 1263 static assert(__traits(compiles, { Test barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1264 } 1265 1266 private string getArrayFieldName(T)(NestedNameStyle style, string prefix, T index) 1267 { 1268 import std.format : format; 1269 final switch (style) { 1270 case NestedNameStyle.underscore: return format("%s_%s", prefix, index); 1271 case NestedNameStyle.d: return format("%s[%s]", prefix, index); 1272 } 1273 } 1274 1275 private string getMemberFieldName(NestedNameStyle style, string prefix, string member) 1276 @safe { 1277 import std.format : format; 1278 final switch (style) { 1279 case NestedNameStyle.underscore: return format("%s_%s", prefix, member); 1280 case NestedNameStyle.d: return format("%s.%s", prefix, member); 1281 } 1282 }