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 }