1 module dud.pkgdescription.json;
2 
3 import std.algorithm.iteration : map, each, joiner, splitter;
4 import std.algorithm.mutation : copy;
5 import std.algorithm.searching : canFind, startsWith;
6 import std.array : array, empty, front, popFront, appender;
7 import std.conv : to;
8 import std.exception : enforce;
9 import std.format : format, formattedWrite;
10 import std.json;
11 import std.range : tee;
12 import std.stdio;
13 import std..string : indexOf;
14 import std.typecons : nullable, Nullable, tuple;
15 import std.traits : FieldNameTuple;
16 
17 import dud.pkgdescription.exception;
18 import dud.pkgdescription.outpututils;
19 import dud.pkgdescription.udas;
20 import dud.pkgdescription;
21 import dud.semver.semver : SemVer;
22 import dud.semver.versionrange;
23 
24 @safe pure:
25 
26 //
27 // PackageDescription
28 //
29 
30 PackageDescription jsonToPackageDescription(string js) {
31 	import std.encoding : getBOM, BOM, BOMSeq;
32 	immutable(BOMSeq) bom = () @trusted {
33 		return getBOM(cast(ubyte[])js);
34 	}();
35 	js = js[bom.sequence.length .. $];
36 
37 	JSONValue jv = parseJSON(js);
38 	return jGetPackageDescription(jv);
39 }
40 
41 PackageDescription jsonToPackageDescription(JSONValue jv) {
42 	return jGetPackageDescription(jv);
43 }
44 
45 //
46 // Platform
47 //
48 
49 Platform[] keyToPlatform(string key) {
50 	auto s = key.startsWith("-")
51 		? key[1 .. $].splitter('-')
52 		: key.splitter('-');
53 	enforce!EmptyInput(!s.empty, format("'%s' is an invalid key", key));
54 	s.popFront();
55 	return toPlatform(s);
56 }
57 
58 Platform[] toPlatform(In)(ref In input) {
59 	import std.algorithm.sorting : sort;
60 	import std.algorithm : uniq;
61 
62 	return input
63 		.map!(it => it == "unittest"
64 				? "unittest_"
65 				: it == "assert"
66 					? "assert_"
67 					: it
68 		)
69 		.map!(it => to!Platform(it))
70 		.array
71 		.sort
72 		.uniq
73 		.array;
74 }
75 
76 void platformToS(Out)(auto ref Out o, const(Platform)[] p) {
77 	p
78 		.map!(it => it == Platform.unittest_
79 				? "unittest"
80 				: it == Platform.assert_
81 					? "assert"
82 					: to!string(it)
83 		)
84 		.map!(s => to!string(s))
85 		.joiner("-")
86 		.copy(o);
87 }
88 
89 string platformKeyToS(string key, const(Platform)[] p) {
90 	auto app = appender!string();
91 	formattedWrite(app, "%s", key);
92 	if(!p.empty) {
93 		app.put('-');
94 		platformToS(app, p);
95 	}
96 	return app.data;
97 }
98 
99 Platform[][] jGetPlatforms(ref JSONValue jv) {
100 	typeCheck(jv, [JSONType.array]);
101 
102 	return jv.arrayNoRef()
103 		.tee!(it => typeCheck(it, [JSONType..string]))
104 		.map!(it => it.str())
105 		.map!(it => it.splitter("-").map!(s => to!Platform(s)).array)
106 		.array;
107 }
108 
109 JSONValue platformsToJ(const Platform[][] plts) {
110 	return plts.empty
111 		? JSONValue.init
112 		: JSONValue(
113 				plts
114 					.map!(plt => plt
115 						.map!(p => to!string(p))
116 						.joiner("-")
117 						.array)
118 					.array
119 			);
120 }
121 
122 //
123 // SemVer
124 //
125 
126 JSONValue semVerToJ(const SemVer v) {
127 	return v == SemVer.init
128 		? JSONValue.init
129 		: JSONValue(v.toString());
130 }
131 
132 SemVer jGetSemVer(ref JSONValue jv) {
133 	import dud.semver.parse : parseSemVer;
134 	string s = jGetString(jv);
135 	return parseSemVer(s);
136 }
137 
138 //
139 // string
140 //
141 
142 string jGetString(ref JSONValue jv) {
143 	typeCheck(jv, [JSONType..string]);
144 	return jv.str();
145 }
146 
147 JSONValue stringToJ(const string s) {
148 	return s.empty ? JSONValue.init : JSONValue(s);
149 }
150 
151 //
152 // Strings, StringsPlatform
153 //
154 
155 void jGetStringsPlatform(ref JSONValue jv, string key, ref Strings output) {
156 	typeCheck(jv, [JSONType.array]);
157 
158 	StringsPlatform ret;
159 	ret.strs = jGetStrings(jv);
160 	ret.platforms = keyToPlatform(key);
161 
162 	output.platforms ~= ret;
163 }
164 
165 void stringsPlatformToJ(const Strings s, const string key,
166 		ref JSONValue output)
167 {
168 	typeCheck(output, [JSONType.object, JSONType.null_]);
169 
170 	s.platforms.each!(delegate(const(StringsPlatform) it) pure @safe {
171 		string nKey = platformKeyToS(key, it.platforms);
172 		if(output.type == JSONType.object && nKey in output) {
173 			throw new ConflictingOutput(format(
174 				"'%s' already present in output JSON", nKey));
175 		} else if(output.type == JSONType.object && nKey !in output) {
176 			output[nKey] = JSONValue(it.strs);
177 		} else {
178 			output = JSONValue([nKey : it.strs]);
179 		}
180 	});
181 }
182 
183 //
184 // String, StringPlatform
185 //
186 
187 void jGetStringPlatform(ref JSONValue jv, string key, ref String output) {
188 	typeCheck(jv, [JSONType..string]);
189 
190 	StringPlatform ret;
191 	ret.str = jv.str();
192 	ret.platforms = keyToPlatform(key);
193 
194 	output.platforms ~= ret;
195 }
196 
197 void stringPlatformToJ(const String s, const string key, ref JSONValue output) {
198 	typeCheck(output, [JSONType.object, JSONType.null_]);
199 
200 	s.platforms.each!(it => output[platformKeyToS(key, it.platforms)] =
201 			JSONValue(it.str));
202 }
203 
204 //
205 // strings
206 //
207 
208 string[] jGetStrings(ref JSONValue jv) {
209 	typeCheck(jv, [JSONType.array]);
210 	return jv.arrayNoRef().map!(it => jGetString(it)).array;
211 }
212 
213 JSONValue stringsToJ(const string[] ss) {
214 	return ss.empty
215 		? JSONValue.init
216 		: JSONValue(ss.map!(s => s).array);
217 }
218 
219 //
220 // path
221 //
222 
223 void jGetUnprocessedPath(ref JSONValue jv, string key,
224 		ref UnprocessedPath output)
225 {
226 	typeCheck(jv, [JSONType..string]);
227 
228 	output = UnprocessedPath(jv.str());
229 }
230 
231 void unprocessedPathToJ(const UnprocessedPath s, const string key,
232 		ref JSONValue output)
233 {
234 	typeCheck(output, [JSONType.object, JSONType.null_]);
235 
236 	if(!s.path.empty) {
237 		output[key] = JSONValue(s.path);
238 	}
239 }
240 
241 void jGetPath(ref JSONValue jv, string key, ref Path output) {
242 	typeCheck(jv, [JSONType..string]);
243 
244 	PathPlatform ret;
245 	ret.path = UnprocessedPath(jv.str());
246 	ret.platforms = keyToPlatform(key);
247 	output.platforms ~= ret;
248 }
249 
250 void pathToJ(const Path s, const string key, ref JSONValue output) {
251 	typeCheck(output, [JSONType.object, JSONType.null_]);
252 
253 	s.platforms.each!(it => output[platformKeyToS(key, it.platforms)] =
254 			JSONValue(it.path.path));
255 }
256 
257 //
258 // paths
259 //
260 
261 void jGetPaths(ref JSONValue jv, string key, ref Paths output) {
262 	typeCheck(jv, [JSONType.array]);
263 
264 	PathsPlatform tmp;
265 	tmp.platforms = keyToPlatform(key);
266 	tmp.paths = jv.arrayNoRef()
267 		.map!(j => j.str())
268 		.map!(s => UnprocessedPath(s)).array;
269 
270 	output.platforms ~= tmp;
271 }
272 
273 void pathsToJ(const Paths ss, const string key, ref JSONValue output) {
274 	typeCheck(output, [JSONType.object, JSONType.null_]);
275 
276 	ss.platforms
277 		.each!(pp =>
278 				output[platformKeyToS(key, pp.platforms)]
279 					= JSONValue(pp.paths.map!(p => p.path).array)
280 		);
281 }
282 
283 //
284 // bool
285 //
286 
287 bool jGetBool(ref JSONValue jv) {
288 	typeCheck(jv, [JSONType.true_, JSONType.false_]);
289 	return jv.boolean();
290 }
291 
292 //
293 // Dependency
294 //
295 
296 void jGetDependencies(ref JSONValue jv, string key, ref Dependency[] deps) {
297 	void insert(ref Dependency[string] ret, Dependency nd) pure {
298 		ret[nd.name] = nd;
299 	}
300 
301 	Dependency depFromJSON(T)(ref T it, Platform[] plts) pure {
302 		Dependency t = extractDependency(it.value);
303 		t.platforms = plts;
304 		t.name = it.key;
305 		return t;
306 	}
307 
308 	Dependency extractDependencyStr(ref JSONValue jv) pure {
309 		import dud.semver.versionrange;
310 
311 		typeCheck(jv, [JSONType..string]);
312 
313 		Dependency ret;
314 		ret.version_ = parseVersionRange(jv.str());
315 		return ret;
316 	}
317 
318 	Dependency extractDependencyObj(ref JSONValue jv) pure {
319 		import dud.semver.versionrange;
320 
321 		typeCheck(jv, [JSONType.object]);
322 
323 		Dependency ret;
324 		foreach(keyF, value; jv.objectNoRef()) {
325 			switch(keyF) {
326 				case "version":
327 					ret.version_ = parseVersionRange(jGetString(value));
328 					break;
329 				case "path":
330 					ret.path.path = jGetString(value);
331 					break;
332 				case "optional":
333 					ret.optional = nullable(jGetBool(value));
334 					break;
335 				case "default":
336 					ret.default_ = nullable(jGetBool(value));
337 					break;
338 				default:
339 					throw new Exception(format(
340 							"Key '%s' is not part of a Dependency declaration",
341 							keyF));
342 			}
343 		}
344 
345 		return ret;
346 	}
347 
348 	Dependency extractDependency(ref JSONValue jv) pure {
349 		typeCheck(jv, [JSONType.object, JSONType..string]);
350 		return jv.type == JSONType.object
351 			? extractDependencyObj(jv)
352 			: extractDependencyStr(jv);
353 	}
354 
355 	typeCheck(jv, [JSONType.object]);
356 
357 	const string noPlatform = splitOutKey(key);
358 	Platform[] plts = keyToPlatform(key);
359 	deps ~= jv.objectNoRef()
360 		.byKeyValue()
361 		.map!(it => depFromJSON(it, plts))
362 		.array;
363 }
364 
365 void dependenciesToJ(const Dependency[] deps, string key, ref JSONValue jv) {
366 	JSONValue[string][string] tmp;
367 	deps.each!(dep => tmp[platformKeyToS(key, dep.platforms)][dep.name] =
368 		dependencyToJ(dep));
369 	foreach(keyF, value; tmp) {
370 		jv[keyF] = JSONValue(value);
371 	}
372 }
373 
374 JSONValue dependencyToJ(const Dependency dep) {
375 	import dud.pkgdescription.helper;
376 
377 	bool isShortFrom(const Dependency d) pure {
378 		return !d.version_.isNull()
379 			&& d.path.path.empty
380 			&& d.optional.isNull()
381 			&& d.default_.isNull();
382 	}
383 
384 	JSONValue ret;
385 	if(isShortFrom(dep)) {
386 		return JSONValue(dep.version_.get().toString());
387 	}
388 	static foreach(mem; FieldNameTuple!Dependency) {{
389 		alias MemType = typeof(__traits(getMember, Dependency, mem));
390 
391 		enum Mem = PreprocessKey!(mem);
392 
393 		static if(is(MemType == string)) {{
394 			// no need to handle this, this is stored as a json key
395 		}} else static if(is(MemType == Nullable!VersionRange)) {{
396 			if(!__traits(getMember, dep, mem).isNull()) {
397 				ret[Mem] = __traits(getMember, dep, mem).get().toString();
398 			}
399 		}} else static if(is(MemType == UnprocessedPath)) {{
400 			const UnprocessedPath p = __traits(getMember, dep, mem);
401 			if(!p.path.empty) {
402 				ret[Mem] = p.path;
403 			}
404 		}} else static if(is(MemType == Nullable!bool)) {{
405 			if(!__traits(getMember, dep, mem).isNull()) {
406 				ret[Mem] = __traits(getMember, dep, mem).get();
407 			}
408 		}} else static if(is(MemType == Platform[])) {{
409 			// not handled here
410 		}} else {
411 			static assert(false, "Unhandeld type " ~ MemType.stringof ~
412 				" for mem " ~ Mem);
413 		}
414 	}}
415 	return ret;
416 }
417 
418 //
419 // SubPackage
420 //
421 
422 SubPackage jGetSubpackageStr(ref JSONValue jv) {
423 	SubPackage ret;
424 	PathPlatform pp;
425 	pp.path.path = jGetString(jv);
426 	ret.path.platforms ~= pp;
427 	return ret;
428 }
429 
430 SubPackage jGetSubpackageObj(ref JSONValue jv) {
431 	SubPackage ret;
432 	ret.inlinePkg = jGetPackageDescription(jv);
433 	return ret;
434 }
435 
436 SubPackage jGetSubPackage(ref JSONValue jv) {
437 	typeCheck(jv, [JSONType.object, JSONType..string]);
438 	return jv.type == JSONType.object
439 		? jGetSubpackageObj(jv)
440 		: jGetSubpackageStr(jv);
441 }
442 
443 SubPackage[] jGetSubPackages(ref JSONValue jv) {
444 	typeCheck(jv, [JSONType.array]);
445 	return jv.arrayNoRef().map!(it => jGetSubPackage(it)).array;
446 }
447 
448 JSONValue subPackagesToJ(const SubPackage[] sps) {
449 	if(sps.empty) {
450 		return JSONValue.init;
451 	}
452 
453 	JSONValue[] ret;
454 	foreach(sp; sps) {
455 		if(!sp.inlinePkg.isNull()) {
456 			ret ~= packageDescriptionToJ(sp.inlinePkg.get());
457 		} else {
458 			enforce!EmptyInput(!sp.path.platforms.empty,
459 				"SubPackage entry must be either Package description or path");
460 			ret ~= stringToJ(sp.path.platforms.front.path.path);
461 		}
462 	}
463 	return JSONValue(ret);
464 }
465 
466 //
467 // BuildType
468 //
469 
470 void jGetBuildType(ref JSONValue jv, string key, ref BuildType bt) {
471 	bt.name = key;
472 	bt.pkg = jGetPackageDescription(jv);
473 }
474 
475 void jGetBuildTypes(ref JSONValue jv, string key, ref BuildType[string] bts) {
476 	typeCheck(jv, [JSONType.object]);
477 	foreach(keyF, value; jv.objectNoRef()) {
478 		BuildType tmp;
479 		jGetBuildType(value, keyF, tmp);
480 		bts[keyF] = tmp;
481 	}
482 }
483 
484 void buildTypesToJ(const BuildType[string] bts, const string key,
485 		ref JSONValue ret)
486 {
487 	typeCheck(ret, [JSONType.object, JSONType.null_]);
488 	if(bts.empty) {
489 		return;
490 	}
491 
492 	JSONValue[string] map;
493 	foreach(keyF, value; bts) {
494 		JSONValue tmp = packageDescriptionToJ(value.pkg);
495 		map[keyF] = tmp;
496 	}
497 	ret["buildTypes"] = map;
498 }
499 
500 //
501 // BuildOption
502 //
503 
504 void jGetBuildOptions(ref JSONValue jv, string key, ref BuildOptions bos) {
505 	immutable(Platform[]) iPlts = keyToPlatform(key);
506 
507 	if(iPlts.empty) {
508 		bos.unspecifiedPlatform = jv.arrayNoRef()
509 			.map!(it => it.str())
510 			.map!(s => to!BuildOption(s))
511 			.array;
512 	} else {
513 		bos.platforms[iPlts] = jv.arrayNoRef()
514 			.map!(it => it.str())
515 			.map!(s => to!BuildOption(s))
516 			.array;
517 	}
518 }
519 
520 void buildOptionsToJ(const BuildOptions bos, const string key,
521 		ref JSONValue ret)
522 {
523 	if(!bos.unspecifiedPlatform.empty) {
524 		JSONValue j = JSONValue(
525 			bos.unspecifiedPlatform.map!(bo => to!string(bo)).array);
526 		ret["buildOptions"] = j;
527 	}
528 
529 	foreach(plts, value; bos.platforms) {
530 		ret[platformKeyToS("buildOptions", plts)] =
531 			JSONValue(value.map!(bo => to!string(bo)).array);
532 	}
533 }
534 
535 //
536 // TargetType
537 //
538 
539 TargetType jGetTargetType(ref JSONValue jv) {
540 	import std.conv : to;
541 	string s = jGetString(jv);
542 	return to!TargetType(s);
543 }
544 
545 JSONValue targetTypeToJ(const TargetType t) {
546 	return t == TargetType.autodetect ? JSONValue.init : JSONValue(to!string(t));
547 }
548 
549 //
550 // PackageDescription
551 //
552 
553 PackageDescription[string] jGetPackageDescriptions(JSONValue js) {
554 	typeCheck(js, [JSONType.array]);
555 	PackageDescription[string] ret;
556 	js.arrayNoRef()
557 		.each!((JSONValue it) {
558 			PackageDescription tmp = jGetPackageDescription(it);
559 			ret[tmp.name] = tmp;
560 		});
561 	return ret;
562 }
563 
564 template isPlatfromDependend(T) {
565 	enum isPlatfromDependend =
566 		is(T == String)
567 		|| is(T == Strings)
568 		|| is(T == Dependency[])
569 		|| is(T == Path)
570 		|| is(T == UnprocessedPath)
571 		|| is(T == BuildRequirements)
572 		|| is(T == SubConfigs)
573 		|| is(T == BuildOptions)
574 		|| is(T == BuildType[string])
575 		|| is(T == ToolchainRequirement[Toolchain])
576 		|| is(T == Paths);
577 }
578 
579 PackageDescription jGetPackageDescription(JSONValue js) {
580 	typeCheck(js, [JSONType.object, JSONType.null_]);
581 
582 	PackageDescription ret;
583 	if(js.type == JSONType.null_) {
584 		return ret;
585 	}
586 
587 	foreach(string key, ref JSONValue value; js.objectNoRef()) {
588 		const string noPlatform = splitOutKey(key);
589 		sw: switch(noPlatform) {
590 			static foreach(mem; FieldNameTuple!PackageDescription) {{
591 				enum Mem = JSONName!mem;
592 				alias get = JSONGet!mem;
593 				alias MemType = typeof(__traits(getMember, ret, mem));
594 				case Mem:
595 					static if(isPlatfromDependend!MemType) {
596 						get(value, key, __traits(getMember, ret, mem));
597 					} else {
598 						__traits(getMember, ret, mem) = get(value);
599 					}
600 					break sw;
601 			}}
602 			default:
603 				throw new KeyNotHandled(
604 					key == noPlatform
605 						? format("The json dud format does not know a key '%s'.",
606 							key)
607 						: format("The json dud format does not know a key '%s'."
608 							~ " Without platform '%s'", key, noPlatform)
609 				);
610 		}
611 	}
612 	return ret;
613 }
614 
615 JSONValue packageDescriptionToJ(const PackageDescription pkg) {
616 	JSONValue ret;
617 	static foreach(mem; FieldNameTuple!PackageDescription) {{
618 		enum Mem = JSONName!mem;
619 		alias put = JSONPut!mem;
620 		alias MemType = typeof(__traits(getMember, PackageDescription, mem));
621 		static if(is(MemType : Nullable!Args, Args...)) {
622 			if(!__traits(getMember, pkg, mem).isNull()) {
623 				JSONValue tmp = put(__traits(getMember, pkg, mem).get());
624 				if(tmp.type != JSONType.null_) {
625 					ret[Mem] = tmp;
626 				}
627 			}
628 		} else {
629 			static if(isPlatfromDependend!MemType) {
630 				put(__traits(getMember, pkg, mem), Mem, ret);
631 			} else {
632 				JSONValue tmp = put(__traits(getMember, pkg, mem));
633 				if(tmp.type != JSONType.null_) {
634 					ret[Mem] = tmp;
635 				}
636 			}
637 		}
638 	}}
639 	return ret;
640 }
641 
642 JSONValue packageDescriptionsToJ(const PackageDescription[string] pkgs) {
643 	return pkgs.empty
644 		? JSONValue.init
645 		: JSONValue(pkgs.byValue().map!(it => packageDescriptionToJ(it)).array);
646 }
647 
648 //
649 // BuildRequirement
650 //
651 
652 BuildRequirement jGetBuildRequirement(ref JSONValue jv) {
653 	string s = jGetString(jv);
654 	return to!BuildRequirement(s);
655 }
656 
657 void jGetBuildRequirements(ref JSONValue jv, string key,
658 		ref BuildRequirements requirements)
659 {
660 	typeCheck(jv, [JSONType.array]);
661 
662 	Platform[] platforms = keyToPlatform(key);
663 	BuildRequirement[] tmp = jv.arrayNoRef()
664 		.map!(it => jGetBuildRequirement(it))
665 		.array;
666 
667 	requirements.platforms ~= BuildRequirementPlatform(tmp, platforms);
668 }
669 
670 void buildRequirementsToJ(const BuildRequirements brs, string key,
671 		ref JSONValue ret)
672 {
673 	typeCheck(ret, [JSONType.object, JSONType.null_]);
674 
675 	if(brs.platforms.empty) {
676 		return;
677 	}
678 
679 	brs.platforms
680 		.each!(br => ret[platformKeyToS(key, br.platforms)]
681 				= JSONValue(br.requirements.map!(it => to!string(it)).array));
682 }
683 
684 //
685 // string[string][Platform[]]
686 //
687 
688 void jGetStringAA(ref JSONValue jv, string key, ref SubConfigs ret) {
689 	typeCheck(jv, [JSONType.object]);
690 	immutable(Platform[]) platforms = keyToPlatform(key);
691 
692 	string[string] tmp;
693 	foreach(pkg, value; jv.objectNoRef()) {
694 		if(platforms.empty) {
695 			ret.unspecifiedPlatform[pkg] = value.str();
696 		} else {
697 			tmp[pkg] = value.str();
698 		}
699 	}
700 	if(!platforms.empty) {
701 		ret.configs[platforms] = tmp;
702 	}
703 }
704 
705 void stringAAToJ(const SubConfigs aa, const string key, ref JSONValue ret) {
706 	JSONValue unspecific;
707 	aa.unspecifiedPlatform.byKeyValue()
708 		.each!(it => unspecific[it.key] = it.value);
709 
710 	if(!aa.unspecifiedPlatform.empty) {
711 		ret["subConfigurations"] = unspecific;
712 	}
713 
714 	foreach(plt, value; aa.configs) {
715 		JSONValue tmp;
716 		foreach(pkg, ver; value) {
717 			tmp[pkg] =ver;
718 		}
719 		if(!value.empty) {
720 			string k = platformKeyToS("subConfigurations", plt);
721 			ret[k] = tmp;
722 		}
723 	}
724 }
725 
726 //
727 // ToolchainRequirement
728 //
729 
730 Toolchain jGetToolchain(string s) {
731 	return to!Toolchain(s);
732 }
733 
734 ToolchainRequirement jGetToolchainRequirement(ref JSONValue jv) {
735 	typeCheck(jv, [JSONType..string]);
736 	const string s = jv.str;
737 	return s == "no"
738 		? ToolchainRequirement(true, VersionRange.init)
739 		: ToolchainRequirement(false, parseVersionRange(s).get());
740 }
741 
742 void insertInto(const Toolchain tc, const ToolchainRequirement tcr,
743 		ref ToolchainRequirement[Toolchain] ret)
744 {
745 	import dud.pkgdescription.duplicate : dup;
746 	enforce!ConflictingInput(tc !in ret, format(
747 			"'%s' is already in '%s'", tc, ret));
748 	ret[tc] = dup(tcr);
749 }
750 
751 void jGetToolchainRequirement(ref JSONValue jv, string key,
752 		ref ToolchainRequirement[Toolchain] ret)
753 {
754 	typeCheck(jv, [JSONType.object]);
755 	jv.objectNoRef()
756 		.byKeyValue()
757 		.map!(it => tuple(it.key.jGetToolchain(),
758 					jGetToolchainRequirement(it.value)))
759 		.each!(tup => insertInto(tup[0], tup[1], ret));
760 }
761 
762 string toolchainToString(const ToolchainRequirement tcr) {
763 	return tcr.no ? "no" : tcr.version_.toString();
764 }
765 
766 void toolchainRequirementToJ(const ToolchainRequirement[Toolchain] tcrs,
767 		const string key, ref JSONValue ret)
768 {
769 	if(tcrs.empty) {
770 		return;
771 	}
772 	typeCheck(ret, [JSONType.object, JSONType.null_]);
773 
774 	JSONValue[string] map;
775 	foreach(keyF, value; tcrs) {
776 		map[to!string(keyF)] = toolchainToString(value);
777 	}
778 	ret["toolchainRequirements"] = map;
779 }
780 
781 //
782 // Helper
783 //
784 
785 void typeCheck(const JSONValue got, const JSONType[] exp,
786 		const string file = __FILE__, const size_t line = __LINE__)
787 {
788 	assert(!exp.empty);
789 	if(!canFind(exp, got.type)) {
790 		throw new WrongTypeJSON(
791 			exp.length == 1
792 				? format("Expected a JSONValue of type '%s' but got '%s'",
793 					exp.front, got.type)
794 				: format("Expected a JSONValue of types [%(%s, %)]' but got '%s'",
795 					exp, got.type),
796 			file, line);
797 	}
798 }
799 
800 string splitOutKey(string key) {
801 	const ptrdiff_t dash = key.indexOf('-', 1);
802 	const string noPlatform = dash == -1 ? key : key[0 .. dash];
803 	return noPlatform;
804 }
805 
806 unittest {
807 	assert(splitOutKey("hello-posix") == "hello");
808 	assert(splitOutKey("-hello-posix") == "-hello");
809 	assert(splitOutKey("-hello") == "-hello");
810 }