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 }