1 /**
2 	JSON format support for PackageRecipe
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, Matthias Dondorff
7 */
8 module dub.recipe.json;
9 
10 import dub.compilers.compiler;
11 import dub.dependency;
12 import dub.recipe.packagerecipe;
13 
14 import dub.internal.vibecompat.data.json;
15 
16 import std.algorithm : canFind, startsWith;
17 import std.conv : to;
18 import std.exception : enforce;
19 import std.range;
20 import std.string : format, indexOf;
21 import std.traits : EnumMembers;
22 
23 deprecated("Use the overload that takes a `PackageName` as 3rd argument")
24 void parseJson(ref PackageRecipe recipe, Json json, string parent)
25 {
26     const PackageName pname = parent ? PackageName(parent) : PackageName.init;
27 	parseJson(recipe, json, pname);
28 }
29 
30 void parseJson(ref PackageRecipe recipe, Json json, in PackageName parent = PackageName.init)
31 {
32 	foreach (string field, value; json) {
33 		switch (field) {
34 			default: break;
35 			case "name": recipe.name = value.get!string; break;
36 			case "version": recipe.version_ = value.get!string; break;
37 			case "description": recipe.description = value.get!string; break;
38 			case "homepage": recipe.homepage = value.get!string; break;
39 			case "authors": recipe.authors = deserializeJson!(string[])(value); break;
40 			case "copyright": recipe.copyright = value.get!string; break;
41 			case "license": recipe.license = value.get!string; break;
42 			case "configurations": break; // handled below, after the global settings have been parsed
43 			case "buildTypes":
44 				foreach (string name, settings; value) {
45 					BuildSettingsTemplate bs;
46 					bs.parseJson(settings, PackageName.init);
47 					recipe.buildTypes[name] = bs;
48 				}
49 				break;
50 			case "toolchainRequirements":
51 				recipe.toolchainRequirements.parseJson(value);
52 				break;
53 			case "-ddoxFilterArgs": recipe.ddoxFilterArgs = deserializeJson!(string[])(value); break;
54 			case "-ddoxTool": recipe.ddoxTool = value.get!string; break;
55 		}
56 	}
57 
58 	enforce(recipe.name.length > 0, "The package \"name\" field is missing or empty.");
59 
60 	const fullname = parent.toString().length
61 		? PackageName(parent.toString() ~ ":" ~ recipe.name)
62 		: PackageName(recipe.name);
63 
64 	// parse build settings
65 	recipe.buildSettings.parseJson(json, fullname);
66 
67 	if (auto pv = "configurations" in json) {
68 		foreach (settings; *pv) {
69 			ConfigurationInfo ci;
70 			ci.parseJson(settings, fullname);
71 			recipe.configurations ~= ci;
72 		}
73 	}
74 
75 	// parse any sub packages after the main package has been fully parsed
76 	if (auto ps = "subPackages" in json)
77 		recipe.parseSubPackages(fullname, ps.opt!(Json[]));
78 }
79 
80 Json toJson(const scope ref PackageRecipe recipe)
81 {
82 	auto ret = recipe.buildSettings.toJson();
83 	ret["name"] = recipe.name;
84 	if (!recipe.version_.empty) ret["version"] = recipe.version_;
85 	if (!recipe.description.empty) ret["description"] = recipe.description;
86 	if (!recipe.homepage.empty) ret["homepage"] = recipe.homepage;
87 	if (!recipe.authors.empty) ret["authors"] = serializeToJson(recipe.authors);
88 	if (!recipe.copyright.empty) ret["copyright"] = recipe.copyright;
89 	if (!recipe.license.empty) ret["license"] = recipe.license;
90 	if (!recipe.subPackages.empty) {
91 		Json[] jsonSubPackages = new Json[recipe.subPackages.length];
92 		foreach (i, subPackage; recipe.subPackages) {
93 			if (subPackage.path !is null) {
94 				jsonSubPackages[i] = Json(subPackage.path);
95 			} else {
96 				jsonSubPackages[i] = subPackage.recipe.toJson();
97 			}
98 		}
99 		ret["subPackages"] = jsonSubPackages;
100 	}
101 	if (recipe.configurations.length) {
102 		Json[] configs;
103 		foreach(config; recipe.configurations)
104 			configs ~= config.toJson();
105 		ret["configurations"] = configs;
106 	}
107 	if (recipe.buildTypes.length) {
108 		Json[string] types;
109 		foreach (name, settings; recipe.buildTypes)
110 			types[name] = settings.toJson();
111 		ret["buildTypes"] = types;
112 	}
113 	if (!recipe.toolchainRequirements.empty) {
114 		ret["toolchainRequirements"] = recipe.toolchainRequirements.toJson();
115 	}
116 	if (!recipe.ddoxFilterArgs.empty) ret["-ddoxFilterArgs"] = recipe.ddoxFilterArgs.serializeToJson();
117 	if (!recipe.ddoxTool.empty) ret["-ddoxTool"] = recipe.ddoxTool;
118 	return ret;
119 }
120 
121 private void parseSubPackages(ref PackageRecipe recipe, in PackageName parent, Json[] subPackagesJson)
122 {
123 	enforce(!parent.sub, format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.",
124 		parent, parent.main));
125 
126 	recipe.subPackages = new SubPackage[subPackagesJson.length];
127 	foreach (i, subPackageJson; subPackagesJson) {
128 		// Handle referenced Packages
129 		if(subPackageJson.type == Json.Type..string) {
130 			string subpath = subPackageJson.get!string;
131 			recipe.subPackages[i] = SubPackage(subpath, PackageRecipe.init);
132 		} else {
133 			PackageRecipe subinfo;
134 			subinfo.parseJson(subPackageJson, parent);
135 			recipe.subPackages[i] = SubPackage(null, subinfo);
136 		}
137 	}
138 }
139 
140 private void parseJson(ref ConfigurationInfo config, Json json, in PackageName pname)
141 {
142 	foreach (string name, value; json) {
143 		switch (name) {
144 			default: break;
145 			case "name":
146 				config.name = value.get!string;
147 				enforce(!config.name.empty, "Configurations must have a non-empty name.");
148 				break;
149 			case "platforms": config.platforms = deserializeJson!(string[])(value); break;
150 		}
151 	}
152 
153 	enforce(!config.name.empty, "Configuration is missing a name.");
154 	config.buildSettings.parseJson(json, pname);
155 }
156 
157 private Json toJson(const scope ref ConfigurationInfo config)
158 {
159 	auto ret = config.buildSettings.toJson();
160 	ret["name"] = config.name;
161 	if (config.platforms.length) ret["platforms"] = serializeToJson(config.platforms);
162 	return ret;
163 }
164 
165 private void parseJson(ref BuildSettingsTemplate bs, Json json, in PackageName pname)
166 {
167 	foreach(string name, value; json)
168 	{
169 		auto idx = indexOf(name, "-");
170 		string basename, suffix;
171 		if( idx >= 0 ) { basename = name[0 .. idx]; suffix = name[idx + 1 .. $]; }
172 		else basename = name;
173 		switch(basename){
174 			default: break;
175 			case "dependencies":
176 				foreach (string pkg, verspec; value) {
177 					if (pkg.startsWith(":")) {
178 						enforce(!pname.sub.length,
179 							"Short-hand packages syntax not allowed within " ~
180 							"sub packages: %s -> %s".format(pname, pkg));
181 						pkg = pname.toString() ~ pkg;
182 					}
183 					enforce(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once." );
184 					bs.dependencies[pkg] = Dependency.fromJson(verspec);
185 					if (verspec.type == Json.Type.object)
186 						bs.dependencies[pkg].settings.parseJson(verspec, pname);
187 				}
188 				break;
189 			case "systemDependencies":
190 				bs.systemDependencies = value.get!string;
191 				break;
192 			case "targetType":
193 				enforce(suffix.empty, "targetType does not support platform customization.");
194 				bs.targetType = value.get!string.to!TargetType;
195 				break;
196 			case "targetPath":
197 				enforce(suffix.empty, "targetPath does not support platform customization.");
198 				bs.targetPath = value.get!string;
199 				break;
200 			case "targetName":
201 				enforce(suffix.empty, "targetName does not support platform customization.");
202 				bs.targetName = value.get!string;
203 				break;
204 			case "workingDirectory":
205 				enforce(suffix.empty, "workingDirectory does not support platform customization.");
206 				bs.workingDirectory = value.get!string;
207 				break;
208 			case "mainSourceFile":
209 				enforce(suffix.empty, "mainSourceFile does not support platform customization.");
210 				bs.mainSourceFile = value.get!string;
211 				break;
212 			case "subConfigurations":
213 				enforce(suffix.empty, "subConfigurations does not support platform customization.");
214 				bs.subConfigurations = deserializeJson!(string[string])(value);
215 				break;
216 			case "dflags": bs.dflags[suffix] = deserializeJson!(string[])(value); break;
217 			case "lflags": bs.lflags[suffix] = deserializeJson!(string[])(value); break;
218 			case "libs": bs.libs[suffix] = deserializeJson!(string[])(value); break;
219 			case "files":
220 			case "sourceFiles": bs.sourceFiles[suffix] = deserializeJson!(string[])(value); break;
221 			case "sourcePaths": bs.sourcePaths[suffix] = deserializeJson!(string[])(value); break;
222 			case "cSourcePaths": bs.cSourcePaths[suffix] = deserializeJson!(string[])(value); break;
223 			case "sourcePath": bs.sourcePaths[suffix] ~= [value.get!string]; break; // deprecated
224 			case "excludedSourceFiles": bs.excludedSourceFiles[suffix] = deserializeJson!(string[])(value); break;
225 			case "injectSourceFiles": bs.injectSourceFiles[suffix] = deserializeJson!(string[])(value); break;
226 			case "copyFiles": bs.copyFiles[suffix] = deserializeJson!(string[])(value); break;
227 			case "extraDependencyFiles": bs.extraDependencyFiles[suffix] = deserializeJson!(string[])(value); break;
228 			case "versions": bs.versions[suffix] = deserializeJson!(string[])(value); break;
229 			case "debugVersions": bs.debugVersions[suffix] = deserializeJson!(string[])(value); break;
230 			case "-versionFilters": bs.versionFilters[suffix] = deserializeJson!(string[])(value); break;
231 			case "-debugVersionFilters": bs.debugVersionFilters[suffix] = deserializeJson!(string[])(value); break;
232 			case "importPaths": bs.importPaths[suffix] = deserializeJson!(string[])(value); break;
233 			case "cImportPaths": bs.cImportPaths[suffix] = deserializeJson!(string[])(value); break;
234 			case "stringImportPaths": bs.stringImportPaths[suffix] = deserializeJson!(string[])(value); break;
235 			case "preGenerateCommands": bs.preGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
236 			case "postGenerateCommands": bs.postGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
237 			case "preBuildCommands": bs.preBuildCommands[suffix] = deserializeJson!(string[])(value); break;
238 			case "postBuildCommands": bs.postBuildCommands[suffix] = deserializeJson!(string[])(value); break;
239 			case "preRunCommands": bs.preRunCommands[suffix] = deserializeJson!(string[])(value); break;
240 			case "postRunCommands": bs.postRunCommands[suffix] = deserializeJson!(string[])(value); break;
241 			case "environments": bs.environments[suffix] = deserializeJson!(string[string])(value); break;
242 			case "buildEnvironments": bs.buildEnvironments[suffix] = deserializeJson!(string[string])(value); break;
243 			case "runEnvironments": bs.runEnvironments[suffix] = deserializeJson!(string[string])(value); break;
244 			case "preGenerateEnvironments": bs.preGenerateEnvironments[suffix] = deserializeJson!(string[string])(value); break;
245 			case "postGenerateEnvironments": bs.postGenerateEnvironments[suffix] = deserializeJson!(string[string])(value); break;
246 			case "preBuildEnvironments": bs.preBuildEnvironments[suffix] = deserializeJson!(string[string])(value); break;
247 			case "postBuildEnvironments": bs.postBuildEnvironments[suffix] = deserializeJson!(string[string])(value); break;
248 			case "preRunEnvironments": bs.preRunEnvironments[suffix] = deserializeJson!(string[string])(value); break;
249 			case "postRunEnvironments": bs.postRunEnvironments[suffix] = deserializeJson!(string[string])(value); break;
250 			case "buildRequirements":
251 				Flags!BuildRequirement reqs;
252 				foreach (req; deserializeJson!(string[])(value))
253 					reqs |= to!BuildRequirement(req);
254 				bs.buildRequirements[suffix] = reqs;
255 				break;
256 			case "buildOptions":
257 				Flags!BuildOption options;
258 				foreach (opt; deserializeJson!(string[])(value))
259 					options |= to!BuildOption(opt);
260 				bs.buildOptions[suffix] = options;
261 				break;
262 		}
263 	}
264 }
265 
266 private Json toJson(const scope ref BuildSettingsTemplate bs)
267 {
268 	static string withSuffix (string pre, string post)
269 	{
270 		if (!post.length)
271 			return pre;
272 		return pre ~ "-" ~ post;
273 	}
274 
275 	auto ret = Json.emptyObject;
276 	if( bs.dependencies !is null ){
277 		auto deps = Json.emptyObject;
278 		foreach( pack, d; bs.dependencies )
279 			deps[pack] = d.toJson();
280 		ret["dependencies"] = deps;
281 	}
282 	if (bs.systemDependencies !is null) ret["systemDependencies"] = bs.systemDependencies;
283 	if (bs.targetType != TargetType.autodetect) ret["targetType"] = bs.targetType.to!string();
284 	if (!bs.targetPath.empty) ret["targetPath"] = bs.targetPath;
285 	if (!bs.targetName.empty) ret["targetName"] = bs.targetName;
286 	if (!bs.workingDirectory.empty) ret["workingDirectory"] = bs.workingDirectory;
287 	if (!bs.mainSourceFile.empty) ret["mainSourceFile"] = bs.mainSourceFile;
288 	if (bs.subConfigurations.length > 0) ret["subConfigurations"] = serializeToJson(bs.subConfigurations);
289 	foreach (suffix, arr; bs.dflags) ret[withSuffix("dflags", suffix)] = serializeToJson(arr);
290 	foreach (suffix, arr; bs.lflags) ret[withSuffix("lflags", suffix)] = serializeToJson(arr);
291 	foreach (suffix, arr; bs.libs) ret[withSuffix("libs", suffix)] = serializeToJson(arr);
292 	foreach (suffix, arr; bs.sourceFiles) ret[withSuffix("sourceFiles", suffix)] = serializeToJson(arr);
293 	foreach (suffix, arr; bs.sourcePaths) ret[withSuffix("sourcePaths", suffix)] = serializeToJson(arr);
294 	foreach (suffix, arr; bs.cSourcePaths) ret[withSuffix("cSourcePaths", suffix)] = serializeToJson(arr);
295 	foreach (suffix, arr; bs.excludedSourceFiles) ret[withSuffix("excludedSourceFiles", suffix)] = serializeToJson(arr);
296 	foreach (suffix, arr; bs.injectSourceFiles) ret[withSuffix("injectSourceFiles", suffix)] = serializeToJson(arr);
297 	foreach (suffix, arr; bs.copyFiles) ret[withSuffix("copyFiles", suffix)] = serializeToJson(arr);
298 	foreach (suffix, arr; bs.extraDependencyFiles) ret[withSuffix("extraDependencyFiles", suffix)] = serializeToJson(arr);
299 	foreach (suffix, arr; bs.versions) ret[withSuffix("versions", suffix)] = serializeToJson(arr);
300 	foreach (suffix, arr; bs.debugVersions) ret[withSuffix("debugVersions", suffix)] = serializeToJson(arr);
301 	foreach (suffix, arr; bs.versionFilters) ret[withSuffix("-versionFilters", suffix)] = serializeToJson(arr);
302 	foreach (suffix, arr; bs.debugVersionFilters) ret[withSuffix("-debugVersionFilters", suffix)] = serializeToJson(arr);
303 	foreach (suffix, arr; bs.importPaths) ret[withSuffix("importPaths", suffix)] = serializeToJson(arr);
304 	foreach (suffix, arr; bs.cImportPaths) ret[withSuffix("cImportPaths", suffix)] = serializeToJson(arr);
305 	foreach (suffix, arr; bs.stringImportPaths) ret[withSuffix("stringImportPaths", suffix)] = serializeToJson(arr);
306 	foreach (suffix, arr; bs.preGenerateCommands) ret[withSuffix("preGenerateCommands", suffix)] = serializeToJson(arr);
307 	foreach (suffix, arr; bs.postGenerateCommands) ret[withSuffix("postGenerateCommands", suffix)] = serializeToJson(arr);
308 	foreach (suffix, arr; bs.preBuildCommands) ret[withSuffix("preBuildCommands", suffix)] = serializeToJson(arr);
309 	foreach (suffix, arr; bs.postBuildCommands) ret[withSuffix("postBuildCommands", suffix)] = serializeToJson(arr);
310 	foreach (suffix, arr; bs.preRunCommands) ret[withSuffix("preRunCommands", suffix)] = serializeToJson(arr);
311 	foreach (suffix, arr; bs.postRunCommands) ret[withSuffix("postRunCommands", suffix)] = serializeToJson(arr);
312 	foreach (suffix, aa; bs.environments) ret[withSuffix("environments", suffix)] = serializeToJson(aa);
313 	foreach (suffix, aa; bs.buildEnvironments) ret[withSuffix("buildEnvironments", suffix)] = serializeToJson(aa);
314 	foreach (suffix, aa; bs.runEnvironments) ret[withSuffix("runEnvironments", suffix)] = serializeToJson(aa);
315 	foreach (suffix, aa; bs.preGenerateEnvironments) ret[withSuffix("preGenerateEnvironments", suffix)] = serializeToJson(aa);
316 	foreach (suffix, aa; bs.postGenerateEnvironments) ret[withSuffix("postGenerateEnvironments", suffix)] = serializeToJson(aa);
317 	foreach (suffix, aa; bs.preBuildEnvironments) ret[withSuffix("preBuildEnvironments", suffix)] = serializeToJson(aa);
318 	foreach (suffix, aa; bs.postBuildEnvironments) ret[withSuffix("postBuildEnvironments", suffix)] = serializeToJson(aa);
319 	foreach (suffix, aa; bs.preRunEnvironments) ret[withSuffix("preRunEnvironments", suffix)] = serializeToJson(aa);
320 	foreach (suffix, aa; bs.postRunEnvironments) ret[withSuffix("postRunEnvironments", suffix)] = serializeToJson(aa);
321 	foreach (suffix, arr; bs.buildRequirements) {
322 		string[] val;
323 		foreach (i; [EnumMembers!BuildRequirement])
324 			if (arr & i) val ~= to!string(i);
325 		ret[withSuffix("buildRequirements", suffix)] = serializeToJson(val);
326 	}
327 	foreach (suffix, arr; bs.buildOptions) {
328 		string[] val;
329 		foreach (i; [EnumMembers!BuildOption])
330 			if (arr & i) val ~= to!string(i);
331 		ret[withSuffix("buildOptions", suffix)] = serializeToJson(val);
332 	}
333 	return ret;
334 }
335 
336 private void parseJson(ref ToolchainRequirements tr, Json json)
337 {
338 	foreach (string name, value; json)
339 		tr.addRequirement(name, value.get!string);
340 }
341 
342 private Json toJson(const scope ref ToolchainRequirements tr)
343 {
344 	auto ret = Json.emptyObject;
345 	if (tr.dub != VersionRange.Any) ret["dub"] = serializeToJson(tr.dub);
346 	if (tr.frontend != VersionRange.Any) ret["frontend"] = serializeToJson(tr.frontend);
347 	if (tr.dmd != VersionRange.Any) ret["dmd"] = serializeToJson(tr.dmd);
348 	if (tr.ldc != VersionRange.Any) ret["ldc"] = serializeToJson(tr.ldc);
349 	if (tr.gdc != VersionRange.Any) ret["gdc"] = serializeToJson(tr.gdc);
350 	return ret;
351 }
352 
353 unittest {
354 	import std.string: strip, outdent;
355 	static immutable json = `
356 		{
357 			"name": "projectname",
358 			"environments": {
359 				"Var1": "env"
360 			},
361 			"buildEnvironments": {
362 				"Var2": "buildEnv"
363 			},
364 			"runEnvironments": {
365 				"Var3": "runEnv"
366 			},
367 			"preGenerateEnvironments": {
368 				"Var4": "preGenEnv"
369 			},
370 			"postGenerateEnvironments": {
371 				"Var5": "postGenEnv"
372 			},
373 			"preBuildEnvironments": {
374 				"Var6": "preBuildEnv"
375 			},
376 			"postBuildEnvironments": {
377 				"Var7": "postBuildEnv"
378 			},
379 			"preRunEnvironments": {
380 				"Var8": "preRunEnv"
381 			},
382 			"postRunEnvironments": {
383 				"Var9": "postRunEnv"
384 			}
385 		}
386 	`.strip.outdent;
387 	auto jsonValue = parseJsonString(json);
388 	PackageRecipe rec1;
389 	parseJson(rec1, jsonValue);
390 	PackageRecipe rec;
391 	// verify that all fields are serialized properly
392 	parseJson(rec, rec1.toJson());
393 
394 	assert(rec.name == "projectname");
395 	assert(rec.buildSettings.environments == ["": ["Var1": "env"]]);
396 	assert(rec.buildSettings.buildEnvironments == ["": ["Var2": "buildEnv"]]);
397 	assert(rec.buildSettings.runEnvironments == ["": ["Var3": "runEnv"]]);
398 	assert(rec.buildSettings.preGenerateEnvironments == ["": ["Var4": "preGenEnv"]]);
399 	assert(rec.buildSettings.postGenerateEnvironments == ["": ["Var5": "postGenEnv"]]);
400 	assert(rec.buildSettings.preBuildEnvironments == ["": ["Var6": "preBuildEnv"]]);
401 	assert(rec.buildSettings.postBuildEnvironments == ["": ["Var7": "postBuildEnv"]]);
402 	assert(rec.buildSettings.preRunEnvironments == ["": ["Var8": "preRunEnv"]]);
403 	assert(rec.buildSettings.postRunEnvironments == ["": ["Var9": "postRunEnv"]]);
404 }