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 hb.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", "index" ], 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 hb.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 hb.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 hb.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 * Declare that a parameter will be transmitted to the API through the body. 468 * 469 * It will be serialized as part of a JSON object. 470 * The serialization format is currently not customizable. 471 * 472 * Params: 473 * - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch. 474 * - field: The name of the field in the JSON object. 475 * 476 * ---- 477 * @bodyParam("pack", "package") 478 * void ship(int pack); 479 * // The server will receive the following body for a call to ship(42): 480 * // { "package": 42 } 481 * ---- 482 */ 483 WebParamAttribute bodyParam(string identifier, string field) 484 @safe { 485 import hb.web.internal.rest.common : ParameterKind; 486 if (!__ctfe) 487 assert(false, onlyAsUda!__FUNCTION__); 488 return WebParamAttribute(ParameterKind.body_, identifier, field); 489 } 490 491 /** 492 * Declare that a parameter will be transmitted to the API through the headers. 493 * 494 * If the parameter is a string, or any scalar type (float, int, char[], ...), it will be send as a string. 495 * If it's an aggregate, it will be serialized as JSON. 496 * However, passing aggregate via header isn't a good practice and should be avoided for new production code. 497 * 498 * Params: 499 * - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch. 500 * - field: The name of the header field to use (e.g: 'Accept', 'Content-Type'...). 501 * 502 * ---- 503 * // The server will receive the content of the "Authorization" header. 504 * @headerParam("auth", "Authorization") 505 * void login(string auth); 506 * ---- 507 */ 508 WebParamAttribute headerParam(string identifier, string field) 509 @safe { 510 import hb.web.internal.rest.common : ParameterKind; 511 if (!__ctfe) 512 assert(false, onlyAsUda!__FUNCTION__); 513 return WebParamAttribute(ParameterKind.header, identifier, field); 514 } 515 516 /** 517 * Declare that a parameter will be transmitted to the API through the query string. 518 * 519 * It will be serialized as part of a JSON object, and will go through URL serialization. 520 * The serialization format is not customizable. 521 * 522 * Params: 523 * - identifier: The name of the parameter to customize. A compiler error will be issued on mismatch. 524 * - field: The field name to use. 525 * 526 * ---- 527 * // For a call to postData("D is awesome"), the server will receive the query: 528 * // POST /data?test=%22D is awesome%22 529 * @queryParam("data", "test") 530 * void postData(string data); 531 * ---- 532 */ 533 WebParamAttribute queryParam(string identifier, string field) 534 @safe { 535 import hb.web.internal.rest.common : ParameterKind; 536 if (!__ctfe) 537 assert(false, onlyAsUda!__FUNCTION__); 538 return WebParamAttribute(ParameterKind.query, identifier, field); 539 } 540 541 /** 542 Determines the naming convention of an identifier. 543 */ 544 enum MethodStyle 545 { 546 /// Special value for free-style conventions 547 unaltered, 548 /// camelCaseNaming 549 camelCase, 550 /// PascalCaseNaming 551 pascalCase, 552 /// lowercasenaming 553 lowerCase, 554 /// UPPERCASENAMING 555 upperCase, 556 /// lower_case_naming 557 lowerUnderscored, 558 /// UPPER_CASE_NAMING 559 upperUnderscored, 560 561 /// deprecated 562 Unaltered = unaltered, 563 /// deprecated 564 CamelCase = camelCase, 565 /// deprecated 566 PascalCase = pascalCase, 567 /// deprecated 568 LowerCase = lowerCase, 569 /// deprecated 570 UpperCase = upperCase, 571 /// deprecated 572 LowerUnderscored = lowerUnderscored, 573 /// deprecated 574 UpperUnderscored = upperUnderscored, 575 } 576 577 578 /// Speficies how D fields are mapped to form field names 579 enum NestedNameStyle { 580 underscore, /// Use underscores to separate fields and array indices 581 d /// Use native D style and separate fields by dots and put array indices into brackets 582 } 583 584 585 // concatenates two URL parts avoiding any duplicate slashes 586 // in resulting URL. `trailing` defines of result URL must 587 // end with slash 588 package string concatURL(string prefix, string url, bool trailing = false) 589 @safe { 590 import std.algorithm : startsWith, endsWith; 591 592 auto pre = prefix.endsWith("/"); 593 auto post = url.startsWith("/"); 594 595 if (!url.length) return trailing && !pre ? prefix ~ "/" : prefix; 596 597 auto suffix = trailing && !url.endsWith("/") ? "/" : null; 598 599 if (pre) { 600 // "/" is ASCII, so can just slice 601 if (post) return prefix ~ url[1 .. $] ~ suffix; 602 else return prefix ~ url ~ suffix; 603 } else { 604 if (post) return prefix ~ url ~ suffix; 605 else return prefix ~ "/" ~ url ~ suffix; 606 } 607 } 608 609 @safe unittest { 610 assert(concatURL("/test/", "/it/", false) == "/test/it/"); 611 assert(concatURL("/test", "it/", false) == "/test/it/"); 612 assert(concatURL("/test", "it", false) == "/test/it"); 613 assert(concatURL("/test", "", false) == "/test"); 614 assert(concatURL("/test/", "", false) == "/test/"); 615 assert(concatURL("/test/", "/it/", true) == "/test/it/"); 616 assert(concatURL("/test", "it/", true) == "/test/it/"); 617 assert(concatURL("/test", "it", true) == "/test/it/"); 618 assert(concatURL("/test", "", true) == "/test/"); 619 assert(concatURL("/test/", "", true) == "/test/"); 620 } 621 622 623 /// private 624 template isNullable(T) { 625 import std.traits; 626 enum isNullable = isInstanceOf!(Nullable, T); 627 } 628 629 static assert(isNullable!(Nullable!int)); 630 631 package struct ParamError { 632 string field; 633 string text; 634 string debugText; 635 } 636 637 package enum ParamResult { 638 ok, 639 skipped, 640 error 641 } 642 643 // NOTE: dst is assumed to be uninitialized 644 package ParamResult readFormParamRec(T)(scope HTTPServerRequest req, ref T dst, string fieldname, bool required, NestedNameStyle style, ref ParamError err) 645 { 646 import std.traits; 647 import std.typecons; 648 import vibe.data.serialization; 649 650 static if (isDynamicArray!T && !isSomeString!T) { 651 alias EL = typeof(T.init[0]); 652 static assert(!is(EL == bool), 653 "Boolean arrays are not allowed, because their length cannot " ~ 654 "be uniquely determined. Use a static array instead."); 655 size_t idx = 0; 656 dst = T.init; 657 while (true) { 658 EL el = void; 659 auto r = readFormParamRec(req, el, style.getArrayFieldName(fieldname, idx), false, style, err); 660 if (r == ParamResult.error) return r; 661 if (r == ParamResult.skipped) break; 662 dst ~= el; 663 idx++; 664 } 665 } else static if (isStaticArray!T) { 666 foreach (i; 0 .. T.length) { 667 auto r = readFormParamRec(req, dst[i], style.getArrayFieldName(fieldname, i), true, style, err); 668 if (r == ParamResult.error) return r; 669 assert(r != ParamResult.skipped); break; 670 } 671 } else static if (isNullable!T) { 672 typeof(dst.get()) el = void; 673 auto r = readFormParamRec(req, el, fieldname, false, style, err); 674 if (r == ParamResult.ok) 675 dst.setVoid(el); 676 else dst.setVoid(T.init); 677 } else static if (is(T == struct) && 678 !is(typeof(T.fromString(string.init))) && 679 !is(typeof(T.fromStringValidate(string.init, null))) && 680 !is(typeof(T.fromISOExtString(string.init)))) 681 { 682 foreach (m; __traits(allMembers, T)) { 683 // TODO: why is this access issue not appearing in vibe.d? 684 static if (is(typeof( 685 __traits(getMember, Foo.init, mem) 686 ))) 687 { 688 auto r = readFormParamRec(req, __traits(getMember, dst, m), style.getMemberFieldName(fieldname, m), required, style, err); 689 if (r != ParamResult.ok) 690 return r; // FIXME: in case of errors the struct will be only partially initialized! All previous fields should be deinitialized first. 691 } 692 } 693 } else static if (is(T == bool)) { 694 dst = (fieldname in req.form) !is null || (fieldname in req.query) !is null; 695 } else if (auto pv = fieldname in req.form) { 696 if (!(*pv).webConvTo(dst, err)) { 697 err.field = fieldname; 698 return ParamResult.error; 699 } 700 } else if (auto pv = fieldname in req.query) { 701 if (!(*pv).webConvTo(dst, err)) { 702 err.field = fieldname; 703 return ParamResult.error; 704 } 705 } else if (required) { 706 err.field = fieldname; 707 err.text = "Missing form field."; 708 return ParamResult.error; 709 } 710 else return ParamResult.skipped; 711 712 return ParamResult.ok; 713 } 714 715 package bool webConvTo(T)(string str, ref T dst, ref ParamError err) 716 nothrow { 717 import std.conv; 718 import std.exception; 719 try { 720 static if (is(typeof(T.fromStringValidate(str, &err.text)))) { 721 static assert(is(typeof(T.fromStringValidate(str, &err.text)) == Nullable!T)); 722 auto res = T.fromStringValidate(str, &err.text); 723 if (res.isNull()) return false; 724 dst.setVoid(res); 725 } else static if (is(typeof(T.fromString(str)))) { 726 static assert(is(typeof(T.fromString(str)) == T)); 727 dst.setVoid(T.fromString(str)); 728 } else static if (is(typeof(T.fromISOExtString(str)))) { 729 static assert(is(typeof(T.fromISOExtString(str)) == T)); 730 dst.setVoid(T.fromISOExtString(str)); 731 } else { 732 dst.setVoid(str.to!T()); 733 } 734 } catch (Exception e) { 735 import std.encoding : sanitize; 736 err.text = e.msg; 737 try err.debugText = e.toString().sanitize; 738 catch (Exception) {} 739 return false; 740 } 741 return true; 742 } 743 744 // properly sets an uninitialized variable 745 package void setVoid(T, U)(ref T dst, U value) 746 { 747 import std.traits; 748 static if (hasElaborateAssign!T) { 749 static if (is(T == U)) { 750 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&value)[0 .. T.sizeof]; 751 typeid(T).postblit(&dst); 752 } else { 753 static T init = T.init; 754 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&init)[0 .. T.sizeof]; 755 dst = value; 756 } 757 } else dst = value; 758 } 759 760 unittest { 761 static assert(!__traits(compiles, { bool[] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 762 static assert(__traits(compiles, { bool[2] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 763 } 764 765 private string getArrayFieldName(T)(NestedNameStyle style, string prefix, T index) 766 { 767 import std.format : format; 768 final switch (style) { 769 case NestedNameStyle.underscore: return format("%s_%s", prefix, index); 770 case NestedNameStyle.d: return format("%s[%s]", prefix, index); 771 } 772 } 773 774 private string getMemberFieldName(NestedNameStyle style, string prefix, string member) 775 @safe { 776 import std.format : format; 777 final switch (style) { 778 case NestedNameStyle.underscore: return format("%s_%s", prefix, member); 779 case NestedNameStyle.d: return format("%s.%s", prefix, member); 780 } 781 }