Skip to content

Commit a8aa234

Browse files
committed
Use configy library to parse dub.json file
This will provide a vastly superior UX to the users: - They will be warned about typos; - The type information provided will be more accurate and always include file/line infos (e.g. no more 'Invalid SemVer versions'); - We can expose a better recipe struct to library users on the long run, by separating the actual data DUB uses from what it reads; - We can be more descriptive about errors / intent, for example by using `SetInfo`;
1 parent 7ab2341 commit a8aa234

File tree

7 files changed

+361
-54
lines changed

7 files changed

+361
-54
lines changed

source/dub/commandline.d

+1-1
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ struct CommandLineHandler
286286

287287
// make the CWD package available so that for example sub packages can reference their
288288
// parent package.
289-
try dub.packageManager.getOrLoadPackage(NativePath(options.root_path));
289+
try dub.packageManager.getOrLoadPackage(NativePath(options.root_path), NativePath.init, false, StrictMode.Warn);
290290
catch (Exception e) { logDiagnostic("No valid package found in current working directory: %s", e.msg); }
291291

292292
return dub;

source/dub/internal/utils.d

+51
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import std.array : appender, array;
2020
import std.conv : to;
2121
import std.exception : enforce;
2222
import std.file;
23+
import std.format;
2324
import std.string : format;
2425
import std.process;
2526
import std.traits : isIntegral;
@@ -689,3 +690,53 @@ string getModuleNameFromFile(string filePath) {
689690
logDiagnostic("Get module name from path: %s", filePath);
690691
return getModuleNameFromContent(fileContent);
691692
}
693+
694+
/**
695+
* Compare two instances of the same type for equality,
696+
* providing a rich error message on failure.
697+
*
698+
* This function will recurse into composite types (struct, AA, arrays)
699+
* and compare element / member wise, taking opEquals into account,
700+
* to provide the most accurate reason why comparison failed.
701+
*/
702+
void deepCompare (T) (
703+
in T result, in T expected, string file = __FILE__, size_t line = __LINE__)
704+
{
705+
deepCompareImpl!T(result, expected, T.stringof, file, line);
706+
}
707+
708+
void deepCompareImpl (T) (
709+
in T result, in T expected, string path, string file, size_t line)
710+
{
711+
static if (is(T == struct) && !is(typeof(T.init.opEquals(T.init)) : bool))
712+
{
713+
static foreach (idx; 0 .. T.tupleof.length)
714+
deepCompareImpl(result.tupleof[idx], expected.tupleof[idx],
715+
format("%s.%s", path, __traits(identifier, T.tupleof[idx])),
716+
file, line);
717+
}
718+
else static if (is(T : KeyT[ValueT], KeyT, ValueT))
719+
{
720+
if (result.length != expected.length)
721+
throw new Exception(
722+
format("%s: AA has different number of entries (%s != %s): %s != %s",
723+
path, result.length, expected.length, result, expected),
724+
file, line);
725+
foreach (key, value; expected)
726+
{
727+
if (auto ptr = key in result)
728+
deepCompareImpl(*ptr, value, format("%s[%s]", path, key), file, line);
729+
else
730+
throw new Exception(
731+
format("Expected key %s[%s] not present in result. %s != %s",
732+
path, key, result, expected), file, line);
733+
}
734+
}
735+
else if (result != expected) {
736+
static if (is(T == struct) && is(typeof(T.init.opEquals(T.init)) : bool))
737+
path ~= ".opEquals";
738+
throw new Exception(
739+
format("%s: result != expected: %s != %s", path, result, expected),
740+
file, line);
741+
}
742+
}

source/dub/package_.d

+7-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import dub.internal.vibecompat.core.file;
2121
import dub.internal.vibecompat.data.json;
2222
import dub.internal.vibecompat.inet.path;
2323

24+
import configy.Read : StrictMode;
25+
2426
import std.algorithm;
2527
import std.array;
2628
import std.conv;
@@ -169,8 +171,11 @@ class Package {
169171
version_override = Optional version to associate to the package
170172
instead of the one declared in the package recipe, or the one
171173
determined by invoking the VCS (GIT currently).
174+
mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
172175
*/
173-
static Package load(NativePath root, NativePath recipe_file = NativePath.init, Package parent = null, string version_override = "")
176+
static Package load(NativePath root, NativePath recipe_file = NativePath.init,
177+
Package parent = null, string version_override = "",
178+
StrictMode mode = StrictMode.Ignore)
174179
{
175180
import dub.recipe.io;
176181

@@ -181,7 +186,7 @@ class Package {
181186
.format(root.toNativeString(),
182187
packageInfoFiles.map!(f => cast(string)f.filename).join("/")));
183188

184-
auto recipe = readPackageRecipe(recipe_file, parent ? parent.name : null);
189+
auto recipe = readPackageRecipe(recipe_file, parent ? parent.name : null, mode);
185190

186191
auto ret = new Package(recipe, root, parent, version_override);
187192
ret.m_infoFile = recipe_file;

source/dub/packagemanager.d

+10-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import dub.internal.vibecompat.data.json;
1414
import dub.internal.vibecompat.inet.path;
1515
import dub.internal.logging;
1616
import dub.package_;
17+
import dub.recipe.io;
18+
import configy.Exceptions;
19+
public import configy.Read : StrictMode;
1720

1821
import std.algorithm : countUntil, filter, sort, canFind, remove;
1922
import std.array;
@@ -254,17 +257,19 @@ class PackageManager {
254257
path = NativePath to the root directory of the package
255258
recipe_path = Optional path to the recipe file of the package
256259
allow_sub_packages = Also return a sub package if it resides in the given folder
260+
mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
257261
258262
Returns: The packages loaded from the given path
259263
Throws: Throws an exception if no package can be loaded
260264
*/
261-
Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init, bool allow_sub_packages = false)
265+
Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init,
266+
bool allow_sub_packages = false, StrictMode mode = StrictMode.Ignore)
262267
{
263268
path.endsWithSlash = true;
264269
foreach (p; getPackageIterator())
265270
if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path)))
266271
return p;
267-
auto pack = Package.load(path, recipe_path);
272+
auto pack = Package.load(path, recipe_path, null, null, mode);
268273
addPackages(m_temporaryPackages, pack);
269274
return pack;
270275
}
@@ -790,6 +795,9 @@ class PackageManager {
790795
}
791796
if (!p) p = Package.load(pack_path, packageFile);
792797
addPackages(m_packages, p);
798+
} catch (ConfigException exc) {
799+
// Confiy error message already include the path
800+
logError("Invalid recipe for local package: %S", exc);
793801
} catch( Exception e ){
794802
logError("Failed to load package in %s: %s", pack_path, e.msg);
795803
logDiagnostic("Full error: %s", e.toString().sanitize());

source/dub/project.d

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class Project {
6565
logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString());
6666
pack = new Package(PackageRecipe.init, project_path);
6767
} else {
68-
pack = package_manager.getOrLoadPackage(project_path, packageFile);
68+
pack = package_manager.getOrLoadPackage(project_path, packageFile, false, StrictMode.Warn);
6969
}
7070

7171
this(package_manager, pack);

source/dub/recipe/io.d

+105-7
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
module dub.recipe.io;
99

1010
import dub.recipe.packagerecipe;
11+
import dub.internal.logging;
1112
import dub.internal.vibecompat.inet.path;
12-
13+
import configy.Read;
1314

1415
/** Reads a package recipe from a file.
1516
@@ -18,16 +19,20 @@ import dub.internal.vibecompat.inet.path;
1819
Params:
1920
filename = NativePath of the package recipe file
2021
parent_name = Optional name of the parent package (if this is a sub package)
22+
mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
2123
2224
Returns: Returns the package recipe contents
2325
Throws: Throws an exception if an I/O or syntax error occurs
2426
*/
25-
PackageRecipe readPackageRecipe(string filename, string parent_name = null)
27+
PackageRecipe readPackageRecipe(
28+
string filename, string parent_name = null, StrictMode mode = StrictMode.Ignore)
2629
{
27-
return readPackageRecipe(NativePath(filename), parent_name);
30+
return readPackageRecipe(NativePath(filename), parent_name, mode);
2831
}
32+
2933
/// ditto
30-
PackageRecipe readPackageRecipe(NativePath filename, string parent_name = null)
34+
PackageRecipe readPackageRecipe(
35+
NativePath filename, string parent_name = null, StrictMode mode = StrictMode.Ignore)
3136
{
3237
import dub.internal.utils : stripUTF8Bom;
3338
import dub.internal.vibecompat.core.file : openFile, FileMode;
@@ -40,7 +45,7 @@ PackageRecipe readPackageRecipe(NativePath filename, string parent_name = null)
4045
text = stripUTF8Bom(cast(string)f.readAll());
4146
}
4247

43-
return parsePackageRecipe(text, filename.toNativeString(), parent_name);
48+
return parsePackageRecipe(text, filename.toNativeString(), parent_name, null, mode);
4449
}
4550

4651
/** Parses an in-memory package recipe.
@@ -55,12 +60,13 @@ PackageRecipe readPackageRecipe(NativePath filename, string parent_name = null)
5560
package)
5661
default_package_name = Optional default package name (if no package name
5762
is found in the recipe this value will be used)
63+
mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
5864
5965
Returns: Returns the package recipe contents
6066
Throws: Throws an exception if an I/O or syntax error occurs
6167
*/
6268
PackageRecipe parsePackageRecipe(string contents, string filename, string parent_name = null,
63-
string default_package_name = null)
69+
string default_package_name = null, StrictMode mode = StrictMode.Ignore)
6470
{
6571
import std.algorithm : endsWith;
6672
import dub.compilers.buildsettings : TargetType;
@@ -72,7 +78,46 @@ PackageRecipe parsePackageRecipe(string contents, string filename, string parent
7278

7379
ret.name = default_package_name;
7480

75-
if (filename.endsWith(".json")) parseJson(ret, parseJsonString(contents, filename), parent_name);
81+
if (filename.endsWith(".json"))
82+
{
83+
try {
84+
ret = parseConfigString!PackageRecipe(contents, filename, mode);
85+
fixDependenciesNames(ret.name, ret);
86+
} catch (ConfigException exc) {
87+
logWarn("Your `dub.json` file use non-conventional features that are deprecated");
88+
logWarn("Please adjust your `dub.json` file as those warnings will turn into errors in dub v1.40.0");
89+
logWarn("Error was: %s", exc);
90+
// Fallback to JSON parser
91+
ret = PackageRecipe.init;
92+
parseJson(ret, parseJsonString(contents, filename), parent_name);
93+
} catch (Exception exc) {
94+
logWarn("Your `dub.json` file use non-conventional features that are deprecated");
95+
logWarn("This is most likely due to duplicated keys.");
96+
logWarn("Please adjust your `dub.json` file as those warnings will turn into errors in dub v1.40.0");
97+
logWarn("Error was: %s", exc);
98+
// Fallback to JSON parser
99+
ret = PackageRecipe.init;
100+
parseJson(ret, parseJsonString(contents, filename), parent_name);
101+
}
102+
// `debug = ConfigFillerDebug` also enables verbose parser output
103+
debug (ConfigFillerDebug)
104+
{
105+
import std.stdio;
106+
107+
PackageRecipe jsonret;
108+
parseJson(jsonret, parseJsonString(contents, filename), parent_name);
109+
if (ret != jsonret)
110+
{
111+
writeln("Content of JSON and YAML parsing differ for file: ", filename);
112+
writeln("-------------------------------------------------------------------");
113+
writeln("JSON (excepted): ", jsonret);
114+
writeln("-------------------------------------------------------------------");
115+
writeln("YAML (actual ): ", ret);
116+
writeln("========================================");
117+
ret = jsonret;
118+
}
119+
}
120+
}
76121
else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent_name, filename);
77122
else assert(false, "readPackageRecipe called with filename with unknown extension: "~filename);
78123

@@ -194,3 +239,56 @@ void serializePackageRecipe(R)(ref R dst, const scope ref PackageRecipe recipe,
194239
toSDL(recipe).toSDLDocument(dst);
195240
else assert(false, "writePackageRecipe called with filename with unknown extension: "~filename);
196241
}
242+
243+
unittest {
244+
import std.format;
245+
import dub.dependency;
246+
import dub.internal.utils : deepCompare;
247+
248+
static void success (string source, in PackageRecipe expected, size_t line = __LINE__) {
249+
const result = parseConfigString!PackageRecipe(source, "dub.json");
250+
deepCompare(result, expected, __FILE__, line);
251+
}
252+
253+
static void error (string source, string expected, size_t line = __LINE__) {
254+
try
255+
{
256+
auto result = parseConfigString!PackageRecipe(source, "dub.json");
257+
assert(0,
258+
format("[%s:%d] Exception should have been thrown but wasn't: %s",
259+
__FILE__, line, result));
260+
}
261+
catch (Exception exc)
262+
assert(exc.toString() == expected,
263+
format("[%s:%s] result != expected: '%s' != '%s'",
264+
__FILE__, line, exc.toString(), expected));
265+
}
266+
267+
alias YAMLDep = typeof(BuildSettingsTemplate.dependencies[string.init]);
268+
const PackageRecipe expected1 =
269+
{
270+
name: "foo",
271+
buildSettings: {
272+
dependencies: RecipeDependencyAA([
273+
"repo": YAMLDep(Dependency(Repository(
274+
"git+https://github.com/dlang/dmd",
275+
"09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2",
276+
))),
277+
"path": YAMLDep(Dependency(NativePath("/foo/bar/jar/"))),
278+
"version": YAMLDep(Dependency(VersionRange.fromString("~>1.0"))),
279+
"version2": YAMLDep(Dependency(Version("4.2.0"))),
280+
])},
281+
};
282+
success(
283+
`{ "name": "foo", "dependencies": {
284+
"repo": { "repository": "git+https://github.com/dlang/dmd",
285+
"version": "09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2" },
286+
"path": { "path": "/foo/bar/jar/" },
287+
"version": { "version": "~>1.0" },
288+
"version2": "4.2.0"
289+
}}`, expected1);
290+
291+
292+
error(`{ "name": "bar", "dependencies": {"bad": { "repository": "git+https://github.com/dlang/dmd" }}}`,
293+
"dub.json(0:41): dependencies[bad]: Need to provide a commit hash in 'version' field with 'repository' dependency");
294+
}

0 commit comments

Comments
 (0)