1 /** 2 Contains common functionality for the REST and WEB interface generators. 3 4 Copyright: © 2012-2014 RejectedSoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Михаил Страшун 7 */ 8 module vibe.web.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; 14 15 static import std.utf; 16 static import std.string; 17 import std.typecons : Nullable; 18 19 20 /** 21 Adjusts the naming convention for a given function name to the specified style. 22 23 The input name is assumed to be in lowerCamelCase (D-style) or PascalCase. Acronyms 24 (e.g. "HTML") should be written all caps 25 */ 26 string adjustMethodStyle(string name, MethodStyle style) 27 @safe { 28 if (!name.length) { 29 return ""; 30 } 31 32 import std.uni; 33 34 final switch(style) { 35 case MethodStyle.unaltered: 36 return name; 37 case MethodStyle.camelCase: 38 size_t i = 0; 39 foreach (idx, dchar ch; name) { 40 if (isUpper(ch)) { 41 i = idx; 42 } 43 else break; 44 } 45 if (i == 0) { 46 std.utf.decode(name, i); 47 return std..string.toLower(name[0 .. i]) ~ name[i .. $]; 48 } else { 49 std.utf.decode(name, i); 50 if (i < name.length) { 51 return std..string.toLower(name[0 .. i-1]) ~ name[i-1 .. $]; 52 } 53 else { 54 return std..string.toLower(name); 55 } 56 } 57 case MethodStyle.pascalCase: 58 size_t idx = 0; 59 std.utf.decode(name, idx); 60 return std..string.toUpper(name[0 .. idx]) ~ name[idx .. $]; 61 case MethodStyle.lowerCase: 62 return std..string.toLower(name); 63 case MethodStyle.upperCase: 64 return std..string.toUpper(name); 65 case MethodStyle.lowerUnderscored: 66 case MethodStyle.upperUnderscored: 67 string ret; 68 size_t start = 0, i = 0; 69 while (i < name.length) { 70 // skip acronyms 71 while (i < name.length && (i+1 >= name.length || (name[i+1] >= 'A' && name[i+1] <= 'Z'))) { 72 std.utf.decode(name, i); 73 } 74 75 // skip the main (lowercase) part of a word 76 while (i < name.length && !(name[i] >= 'A' && name[i] <= 'Z')) { 77 std.utf.decode(name, i); 78 } 79 80 // add a single word 81 if( ret.length > 0 ) { 82 ret ~= "_"; 83 } 84 ret ~= name[start .. i]; 85 86 // quick skip the capital and remember the start of the next word 87 start = i; 88 if (i < name.length) { 89 std.utf.decode(name, i); 90 } 91 } 92 if (start < name.length) { 93 ret ~= "_" ~ name[start .. $]; 94 } 95 return style == MethodStyle.lowerUnderscored ? 96 std..string.toLower(ret) : std..string.toUpper(ret); 97 } 98 } 99 100 @safe unittest 101 { 102 assert(adjustMethodStyle("methodNameTest", MethodStyle.unaltered) == "methodNameTest"); 103 assert(adjustMethodStyle("methodNameTest", MethodStyle.camelCase) == "methodNameTest"); 104 assert(adjustMethodStyle("methodNameTest", MethodStyle.pascalCase) == "MethodNameTest"); 105 assert(adjustMethodStyle("methodNameTest", MethodStyle.lowerCase) == "methodnametest"); 106 assert(adjustMethodStyle("methodNameTest", MethodStyle.upperCase) == "METHODNAMETEST"); 107 assert(adjustMethodStyle("methodNameTest", MethodStyle.lowerUnderscored) == "method_name_test"); 108 assert(adjustMethodStyle("methodNameTest", MethodStyle.upperUnderscored) == "METHOD_NAME_TEST"); 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("Q", MethodStyle.lowerUnderscored) == "q"); 117 assert(adjustMethodStyle("getHTML", MethodStyle.lowerUnderscored) == "get_html"); 118 assert(adjustMethodStyle("getHTMLEntity", MethodStyle.lowerUnderscored) == "get_html_entity"); 119 assert(adjustMethodStyle("ID", MethodStyle.lowerUnderscored) == "id"); 120 assert(adjustMethodStyle("ID", MethodStyle.pascalCase) == "ID"); 121 assert(adjustMethodStyle("ID", MethodStyle.camelCase) == "id"); 122 assert(adjustMethodStyle("IDTest", MethodStyle.lowerUnderscored) == "id_test"); 123 assert(adjustMethodStyle("IDTest", MethodStyle.pascalCase) == "IDTest"); 124 assert(adjustMethodStyle("IDTest", MethodStyle.camelCase) == "idTest"); 125 assert(adjustMethodStyle("anyA", MethodStyle.lowerUnderscored) == "any_a", adjustMethodStyle("anyA", MethodStyle.lowerUnderscored)); 126 } 127 128 129 /** 130 Determines the HTTP method and path for a given function symbol. 131 132 The final method and path are determined from the function name, as well as 133 any $(D @method) and $(D @path) attributes that may be applied to it. 134 135 This function is designed for CTFE usage and will assert at run time. 136 137 Returns: 138 A tuple of three elements is returned: 139 $(UL 140 $(LI flag "was UDA used to override path") 141 $(LI $(D HTTPMethod) extracted) 142 $(LI URL path extracted) 143 ) 144 */ 145 auto extractHTTPMethodAndName(alias Func, bool indexSpecialCase)() 146 { 147 if (!__ctfe) 148 assert(false); 149 150 struct HandlerMeta 151 { 152 bool hadPathUDA; 153 HTTPMethod method; 154 string url; 155 } 156 157 import vibe.internal.meta.uda : findFirstUDA; 158 import vibe.internal.meta.traits : isPropertySetter, 159 isPropertyGetter; 160 import std.algorithm : startsWith; 161 import std.typecons : Nullable; 162 163 immutable httpMethodPrefixes = [ 164 HTTPMethod.GET : [ "get", "query" ], 165 HTTPMethod.PUT : [ "put", "set" ], 166 HTTPMethod.PATCH : [ "update", "patch" ], 167 HTTPMethod.POST : [ "add", "create", "post" ], 168 HTTPMethod.DELETE : [ "remove", "erase", "delete" ], 169 ]; 170 171 enum name = __traits(identifier, Func); 172 alias T = typeof(&Func); 173 174 Nullable!HTTPMethod udmethod; 175 Nullable!string udurl; 176 177 // Cases may conflict and are listed in order of priority 178 179 // Workaround for Nullable incompetence 180 enum uda1 = findFirstUDA!(MethodAttribute, Func); 181 enum uda2 = findFirstUDA!(PathAttribute, Func); 182 183 static if (uda1.found) { 184 udmethod = uda1.value; 185 } 186 static if (uda2.found) { 187 udurl = uda2.value; 188 } 189 190 // Everything is overriden, no further analysis needed 191 if (!udmethod.isNull() && !udurl.isNull()) { 192 return HandlerMeta(true, udmethod.get(), udurl.get()); 193 } 194 195 // Anti-copy-paste delegate 196 typeof(return) udaOverride( HTTPMethod method, string url ){ 197 return HandlerMeta( 198 !udurl.isNull(), 199 udmethod.isNull() ? method : udmethod.get(), 200 udurl.isNull() ? url : udurl.get() 201 ); 202 } 203 204 if (isPropertyGetter!T) { 205 return udaOverride(HTTPMethod.GET, name); 206 } 207 else if(isPropertySetter!T) { 208 return udaOverride(HTTPMethod.PUT, name); 209 } 210 else { 211 foreach (method, prefixes; httpMethodPrefixes) { 212 foreach (prefix; prefixes) { 213 import std.uni : isLower; 214 if (name.startsWith(prefix) && (name.length == prefix.length || !name[prefix.length].isLower)) { 215 string tmp = name[prefix.length..$]; 216 return udaOverride(method, tmp.length ? tmp : "/"); 217 } 218 } 219 } 220 221 static if (indexSpecialCase && name == "index") { 222 return udaOverride(HTTPMethod.GET, "/"); 223 } else 224 return udaOverride(HTTPMethod.POST, name); 225 } 226 } 227 228 unittest 229 { 230 interface Sample 231 { 232 string getInfo(); 233 string updateDescription(); 234 235 @method(HTTPMethod.DELETE) 236 string putInfo(); 237 238 @path("matters") 239 string getMattersnot(); 240 241 @path("compound/path") @method(HTTPMethod.POST) 242 string mattersnot(); 243 244 string get(); 245 246 string posts(); 247 248 string patches(); 249 } 250 251 enum ret1 = extractHTTPMethodAndName!(Sample.getInfo, false,); 252 static assert (ret1.hadPathUDA == false); 253 static assert (ret1.method == HTTPMethod.GET); 254 static assert (ret1.url == "Info"); 255 enum ret2 = extractHTTPMethodAndName!(Sample.updateDescription, false); 256 static assert (ret2.hadPathUDA == false); 257 static assert (ret2.method == HTTPMethod.PATCH); 258 static assert (ret2.url == "Description"); 259 enum ret3 = extractHTTPMethodAndName!(Sample.putInfo, false); 260 static assert (ret3.hadPathUDA == false); 261 static assert (ret3.method == HTTPMethod.DELETE); 262 static assert (ret3.url == "Info"); 263 enum ret4 = extractHTTPMethodAndName!(Sample.getMattersnot, false); 264 static assert (ret4.hadPathUDA == true); 265 static assert (ret4.method == HTTPMethod.GET); 266 static assert (ret4.url == "matters"); 267 enum ret5 = extractHTTPMethodAndName!(Sample.mattersnot, false); 268 static assert (ret5.hadPathUDA == true); 269 static assert (ret5.method == HTTPMethod.POST); 270 static assert (ret5.url == "compound/path"); 271 enum ret6 = extractHTTPMethodAndName!(Sample.get, false); 272 static assert (ret6.hadPathUDA == false); 273 static assert (ret6.method == HTTPMethod.GET); 274 static assert (ret6.url == "/"); 275 enum ret7 = extractHTTPMethodAndName!(Sample.posts, false); 276 static assert(ret7.hadPathUDA == false); 277 static assert(ret7.method == HTTPMethod.POST); 278 static assert(ret7.url == "posts"); 279 enum ret8 = extractHTTPMethodAndName!(Sample.patches, false); 280 static assert(ret8.hadPathUDA == false); 281 static assert(ret8.method == HTTPMethod.POST); 282 static assert(ret8.url == "patches"); 283 } 284 285 286 /** 287 Attribute to define the content type for methods. 288 289 This currently applies only to methods returning an $(D InputStream) or 290 $(D ubyte[]). 291 */ 292 ContentTypeAttribute contentType(string data) 293 @safe { 294 if (!__ctfe) 295 assert(false, onlyAsUda!__FUNCTION__); 296 return ContentTypeAttribute(data); 297 } 298 299 300 /** 301 Attribute to force a specific HTTP method for an interface method. 302 303 The usual URL generation rules are still applied, so if there 304 are any "get", "query" or similar prefixes, they are filtered out. 305 */ 306 MethodAttribute method(HTTPMethod data) 307 @safe { 308 if (!__ctfe) 309 assert(false, onlyAsUda!__FUNCTION__); 310 return MethodAttribute(data); 311 } 312 313 /// 314 unittest { 315 interface IAPI 316 { 317 // Will be "POST /info" instead of default "GET /info" 318 @method(HTTPMethod.POST) string getInfo(); 319 } 320 } 321 322 323 /** 324 Attibute to force a specific URL path. 325 326 This attribute can be applied either to an interface itself, in which 327 case it defines the root path for all methods within it, 328 or on any function, in which case it defines the relative path 329 of this method. 330 Path are always relative, even path on interfaces, as you can 331 see in the example below. 332 333 See_Also: $(D rootPathFromName) for automatic name generation. 334 */ 335 PathAttribute path(string data) 336 @safe { 337 if (!__ctfe) 338 assert(false, onlyAsUda!__FUNCTION__); 339 return PathAttribute(data); 340 } 341 342 /// 343 unittest { 344 @path("/foo") 345 interface IAPI 346 { 347 @path("info2") string getInfo(); 348 } 349 350 class API : IAPI { 351 string getInfo() { return "Hello, World!"; } 352 } 353 354 void test() 355 { 356 import vibe.http.router; 357 import vibe.web.rest; 358 359 auto router = new URLRouter; 360 361 // Tie IAPI.getInfo to "GET /root/foo/info2" 362 router.registerRestInterface!IAPI(new API(), "/root/"); 363 364 // Or just to "GET /foo/info2" 365 router.registerRestInterface!IAPI(new API()); 366 367 // ... 368 } 369 } 370 371 372 /// Convenience alias to generate a name from the interface's name. 373 @property PathAttribute rootPathFromName() 374 @safe { 375 if (!__ctfe) 376 assert(false, onlyAsUda!__FUNCTION__); 377 return PathAttribute(""); 378 } 379 /// 380 unittest 381 { 382 import vibe.http.router; 383 import vibe.web.rest; 384 385 @rootPathFromName 386 interface IAPI 387 { 388 int getFoo(); 389 } 390 391 class API : IAPI 392 { 393 int getFoo() 394 { 395 return 42; 396 } 397 } 398 399 auto router = new URLRouter(); 400 registerRestInterface(router, new API()); 401 auto routes= router.getAllRoutes(); 402 403 assert(routes[0].pattern == "/iapi/foo" && routes[0].method == HTTPMethod.GET); 404 } 405 406 407 /** 408 Respresents a Rest error response 409 */ 410 class RestException : HTTPStatusException { 411 private { 412 Json m_jsonResult; 413 } 414 415 @safe: 416 417 /// 418 this(int status, Json jsonResult, string file = __FILE__, int line = __LINE__, Throwable next = null) 419 { 420 if (jsonResult.type == Json.Type.Object && jsonResult["statusMessage"].type == Json.Type.String) { 421 super(status, jsonResult["statusMessage"].get!string, file, line, next); 422 } 423 else { 424 super(status, httpStatusText(status) ~ " (" ~ jsonResult.toString() ~ ")", file, line, next); 425 } 426 427 m_jsonResult = jsonResult; 428 } 429 430 /// The HTTP status code 431 @property const(Json) jsonResult() const { return m_jsonResult; } 432 } 433 434 /// private 435 package struct ContentTypeAttribute 436 { 437 string data; 438 alias data this; 439 } 440 441 /// private 442 package struct MethodAttribute 443 { 444 HTTPMethod data; 445 alias data this; 446 } 447 448 /// private 449 package struct PathAttribute 450 { 451 string data; 452 alias data this; 453 } 454 455 /// Private struct describing the origin of a parameter (Query, Header, Body). 456 package struct WebParamAttribute { 457 import vibe.web.internal.rest.common : ParameterKind; 458 459 ParameterKind origin; 460 /// Parameter name 461 string identifier; 462 /// The meaning of this field depends on the origin. 463 string field; 464 } 465 466 467 /** 468 * Declare that a parameter will be transmitted to the API through the body. 469 * 470 * It will be serialized as part of a JSON object. 471 * The serialization format is currently not customizable. 472 * If no fieldname is given, the entire body is serialized into the object. 473 * 474 * Params: 475 * - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch. 476 * - field: The name of the field in the JSON object. 477 * 478 * ---- 479 * @bodyParam("pack", "package") 480 * void ship(int pack); 481 * // The server will receive the following body for a call to ship(42): 482 * // { "package": 42 } 483 * ---- 484 */ 485 WebParamAttribute bodyParam(string identifier, string field) @safe 486 in { 487 assert(field.length > 0, "fieldname can't be empty."); 488 } 489 body 490 { 491 import vibe.web.internal.rest.common : ParameterKind; 492 if (!__ctfe) 493 assert(false, onlyAsUda!__FUNCTION__); 494 return WebParamAttribute(ParameterKind.body_, identifier, field); 495 } 496 497 /// ditto 498 WebParamAttribute bodyParam(string identifier) 499 @safe { 500 import vibe.web.internal.rest.common : ParameterKind; 501 if (!__ctfe) 502 assert(false, onlyAsUda!__FUNCTION__); 503 return WebParamAttribute(ParameterKind.body_, identifier, ""); 504 } 505 506 /** 507 * Declare that a parameter will be transmitted to the API through the headers. 508 * 509 * If the parameter is a string, or any scalar type (float, int, char[], ...), it will be send as a string. 510 * If it's an aggregate, it will be serialized as JSON. 511 * However, passing aggregate via header isn't a good practice and should be avoided for new production code. 512 * 513 * Params: 514 * - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch. 515 * - field: The name of the header field to use (e.g: 'Accept', 'Content-Type'...). 516 * 517 * ---- 518 * // The server will receive the content of the "Authorization" header. 519 * @headerParam("auth", "Authorization") 520 * void login(string auth); 521 * ---- 522 */ 523 WebParamAttribute headerParam(string identifier, string field) 524 @safe { 525 import vibe.web.internal.rest.common : ParameterKind; 526 if (!__ctfe) 527 assert(false, onlyAsUda!__FUNCTION__); 528 return WebParamAttribute(ParameterKind.header, identifier, field); 529 } 530 531 /** 532 * Declare that a parameter will be transmitted to the API through the query string. 533 * 534 * It will be serialized as part of a JSON object, and will go through URL serialization. 535 * The serialization format is not customizable. 536 * 537 * Params: 538 * - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch. 539 * - field: The field name to use. 540 * 541 * ---- 542 * // For a call to postData("D is awesome"), the server will receive the query: 543 * // POST /data?test=%22D is awesome%22 544 * @queryParam("data", "test") 545 * void postData(string data); 546 * ---- 547 */ 548 WebParamAttribute queryParam(string identifier, string field) 549 @safe { 550 import vibe.web.internal.rest.common : ParameterKind; 551 if (!__ctfe) 552 assert(false, onlyAsUda!__FUNCTION__); 553 return WebParamAttribute(ParameterKind.query, identifier, field); 554 } 555 556 /** 557 Determines the naming convention of an identifier. 558 */ 559 enum MethodStyle 560 { 561 /// Special value for free-style conventions 562 unaltered, 563 /// camelCaseNaming 564 camelCase, 565 /// PascalCaseNaming 566 pascalCase, 567 /// lowercasenaming 568 lowerCase, 569 /// UPPERCASENAMING 570 upperCase, 571 /// lower_case_naming 572 lowerUnderscored, 573 /// UPPER_CASE_NAMING 574 upperUnderscored, 575 576 /// deprecated 577 Unaltered = unaltered, 578 /// deprecated 579 CamelCase = camelCase, 580 /// deprecated 581 PascalCase = pascalCase, 582 /// deprecated 583 LowerCase = lowerCase, 584 /// deprecated 585 UpperCase = upperCase, 586 /// deprecated 587 LowerUnderscored = lowerUnderscored, 588 /// deprecated 589 UpperUnderscored = upperUnderscored, 590 } 591 592 593 /// Speficies how D fields are mapped to form field names 594 enum NestedNameStyle { 595 underscore, /// Use underscores to separate fields and array indices 596 d /// Use native D style and separate fields by dots and put array indices into brackets 597 } 598 599 600 // concatenates two URL parts avoiding any duplicate slashes 601 // in resulting URL. `trailing` defines of result URL must 602 // end with slash 603 package string concatURL(string prefix, string url, bool trailing = false) 604 @safe { 605 import std.algorithm : startsWith, endsWith; 606 607 auto pre = prefix.endsWith("/"); 608 auto post = url.startsWith("/"); 609 610 if (!url.length) return trailing && !pre ? prefix ~ "/" : prefix; 611 612 auto suffix = trailing && !url.endsWith("/") ? "/" : null; 613 614 if (pre) { 615 // "/" is ASCII, so can just slice 616 if (post) return prefix ~ url[1 .. $] ~ suffix; 617 else return prefix ~ url ~ suffix; 618 } else { 619 if (post) return prefix ~ url ~ suffix; 620 else return prefix ~ "/" ~ url ~ suffix; 621 } 622 } 623 624 @safe unittest { 625 assert(concatURL("/test/", "/it/", false) == "/test/it/"); 626 assert(concatURL("/test", "it/", false) == "/test/it/"); 627 assert(concatURL("/test", "it", false) == "/test/it"); 628 assert(concatURL("/test", "", false) == "/test"); 629 assert(concatURL("/test/", "", false) == "/test/"); 630 assert(concatURL("/test/", "/it/", true) == "/test/it/"); 631 assert(concatURL("/test", "it/", true) == "/test/it/"); 632 assert(concatURL("/test", "it", true) == "/test/it/"); 633 assert(concatURL("/test", "", true) == "/test/"); 634 assert(concatURL("/test/", "", true) == "/test/"); 635 } 636 637 638 /// private 639 template isNullable(T) { 640 import std.traits; 641 enum isNullable = isInstanceOf!(Nullable, T); 642 } 643 644 static assert(isNullable!(Nullable!int)); 645 646 package struct ParamError { 647 string field; 648 string text; 649 string debugText; 650 } 651 652 package enum ParamResult { 653 ok, 654 skipped, 655 error 656 } 657 658 // NOTE: dst is assumed to be uninitialized 659 package ParamResult readFormParamRec(T)(scope HTTPServerRequest req, ref T dst, string fieldname, bool required, NestedNameStyle style, ref ParamError err) 660 { 661 import std.traits; 662 import std.typecons; 663 import vibe.data.serialization; 664 665 static if (isDynamicArray!T && !isSomeString!T) { 666 alias EL = typeof(T.init[0]); 667 static assert(!is(EL == bool), 668 "Boolean arrays are not allowed, because their length cannot " ~ 669 "be uniquely determined. Use a static array instead."); 670 size_t idx = 0; 671 dst = T.init; 672 while (true) { 673 EL el = void; 674 auto r = readFormParamRec(req, el, style.getArrayFieldName(fieldname, idx), false, style, err); 675 if (r == ParamResult.error) return r; 676 if (r == ParamResult.skipped) break; 677 dst ~= el; 678 idx++; 679 } 680 } else static if (isStaticArray!T) { 681 foreach (i; 0 .. T.length) { 682 auto r = readFormParamRec(req, dst[i], style.getArrayFieldName(fieldname, i), true, style, err); 683 if (r == ParamResult.error) return r; 684 assert(r != ParamResult.skipped); break; 685 } 686 } else static if (isNullable!T) { 687 typeof(dst.get()) el = void; 688 auto r = readFormParamRec(req, el, fieldname, false, style, err); 689 final switch (r) { 690 case ParamResult.ok: dst.setVoid(el); break; 691 case ParamResult.skipped: dst.setVoid(T.init); break; 692 case ParamResult.error: return ParamResult.error; 693 } 694 } else static if (is(T == struct) && 695 !is(typeof(T.fromString(string.init))) && 696 !is(typeof(T.fromStringValidate(string.init, null))) && 697 !is(typeof(T.fromISOExtString(string.init)))) 698 { 699 foreach (m; __traits(allMembers, T)) { 700 auto r = readFormParamRec(req, __traits(getMember, dst, m), style.getMemberFieldName(fieldname, m), required, style, err); 701 if (r != ParamResult.ok) 702 return r; // FIXME: in case of errors the struct will be only partially initialized! All previous fields should be deinitialized first. 703 } 704 } else static if (is(T == bool)) { 705 dst = (fieldname in req.form) !is null || (fieldname in req.query) !is null; 706 } else if (auto pv = fieldname in req.form) { 707 if (!(*pv).webConvTo(dst, err)) { 708 err.field = fieldname; 709 return ParamResult.error; 710 } 711 } else if (auto pv = fieldname in req.query) { 712 if (!(*pv).webConvTo(dst, err)) { 713 err.field = fieldname; 714 return ParamResult.error; 715 } 716 } else if (required) { 717 err.field = fieldname; 718 err.text = "Missing form field."; 719 return ParamResult.error; 720 } 721 else return ParamResult.skipped; 722 723 return ParamResult.ok; 724 } 725 726 package bool webConvTo(T)(string str, ref T dst, ref ParamError err) 727 nothrow { 728 import std.conv; 729 import std.exception; 730 try { 731 static if (is(typeof(T.fromStringValidate(str, &err.text)))) { 732 static assert(is(typeof(T.fromStringValidate(str, &err.text)) == Nullable!T)); 733 auto res = T.fromStringValidate(str, &err.text); 734 if (res.isNull()) return false; 735 dst.setVoid(res); 736 } else static if (is(typeof(T.fromString(str)))) { 737 static assert(is(typeof(T.fromString(str)) == T)); 738 dst.setVoid(T.fromString(str)); 739 } else static if (is(typeof(T.fromISOExtString(str)))) { 740 static assert(is(typeof(T.fromISOExtString(str)) == T)); 741 dst.setVoid(T.fromISOExtString(str)); 742 } else { 743 dst.setVoid(str.to!T()); 744 } 745 } catch (Exception e) { 746 import vibe.core.log : logDebug; 747 import std.encoding : sanitize; 748 err.text = e.msg; 749 debug try logDebug("Error converting web field: %s", e.toString().sanitize); 750 catch (Exception) {} 751 return false; 752 } 753 return true; 754 } 755 756 // properly sets an uninitialized variable 757 package void setVoid(T, U)(ref T dst, U value) 758 { 759 import std.traits; 760 static if (hasElaborateAssign!T) { 761 static if (is(T == U)) { 762 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&value)[0 .. T.sizeof]; 763 typeid(T).postblit(&dst); 764 } else { 765 static T init = T.init; 766 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&init)[0 .. T.sizeof]; 767 dst = value; 768 } 769 } else dst = value; 770 } 771 772 unittest { 773 static assert(!__traits(compiles, { bool[] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 774 static assert(__traits(compiles, { bool[2] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 775 } 776 777 private string getArrayFieldName(T)(NestedNameStyle style, string prefix, T index) 778 { 779 import std.format : format; 780 final switch (style) { 781 case NestedNameStyle.underscore: return format("%s_%s", prefix, index); 782 case NestedNameStyle.d: return format("%s[%s]", prefix, index); 783 } 784 } 785 786 private string getMemberFieldName(NestedNameStyle style, string prefix, string member) 787 @safe { 788 import std.format : format; 789 final switch (style) { 790 case NestedNameStyle.underscore: return format("%s_%s", prefix, member); 791 case NestedNameStyle.d: return format("%s.%s", prefix, member); 792 } 793 }