Skip to content

Commit db9741d

Browse files
committed
feat(recipe): Add support YAML recipe
This makes dub recognize `dub.yaml` as a configuration file format. The code is very similar to the JSON one, as Dub has been using the YAML parser for multiple years. It allows us to use a configuration format that is in widespread use and is more human-oriented than JSON.
1 parent b4a73be commit db9741d

8 files changed

Lines changed: 165 additions & 11 deletions

File tree

build-files.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,6 @@ source/dub/recipe/json.d
9393
source/dub/recipe/packagerecipe.d
9494
source/dub/recipe/selection.d
9595
source/dub/recipe/sdl.d
96+
source/dub/recipe/yaml.d
9697
source/dub/semver.d
9798
source/dub/version_.d

source/dub/commandline.d

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ class InitCommand : Command {
12201220
if (m_nonInteractive) return;
12211221

12221222
enum free_choice = true;
1223-
fmt = select("a package recipe format", !free_choice, fmt.to!string, "sdl", "json").to!PackageFormat;
1223+
fmt = select("a package recipe format", !free_choice, fmt.to!string, "sdl", "json", "yaml").to!PackageFormat;
12241224
auto author = p.authors.join(", ");
12251225
while (true) {
12261226
// Tries getting the name until a valid one is given.
@@ -2961,7 +2961,7 @@ class ConvertCommand : Command {
29612961

29622962
override void prepare(scope CommandArgs args)
29632963
{
2964-
args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", " json, sdl"]);
2964+
args.getopt("f|format", &m_format, ["Specifies the target package recipe format. Possible values:", " json, sdl, yaml"]);
29652965
args.getopt("s|stdout", &m_stdout, ["Outputs the converted package recipe to stdout instead of writing to disk."]);
29662966
}
29672967

source/dub/dub.d

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1476,7 +1476,7 @@ class Dub {
14761476
14771477
Params:
14781478
destination_file_ext = The file extension matching the desired
1479-
format. Possible values are "json" or "sdl".
1479+
format. Possible values are "json", "sdl", or "yaml".
14801480
print_only = Print the converted recipe instead of writing to disk
14811481
*/
14821482
void convertRecipe(string destination_file_ext, bool print_only = false)

source/dub/package_.d

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import std.typecons : Nullable;
3535
/// Lists the supported package recipe formats.
3636
enum PackageFormat {
3737
json, /// JSON based, using the ".json" file extension
38-
sdl /// SDLang based, using the ".sdl" file extension
38+
sdl, /// SDLang based, using the ".sdl" file extension
39+
yaml, /// YAML based, using either `.yaml` or `.yml` extension
3940
}
4041

4142
struct FilenameAndFormat {
@@ -45,9 +46,11 @@ struct FilenameAndFormat {
4546

4647
/// Supported package descriptions in decreasing order of preference.
4748
static immutable FilenameAndFormat[] packageInfoFiles = [
48-
{"dub.json", PackageFormat.json},
49-
{"dub.sdl", PackageFormat.sdl},
50-
{"package.json", PackageFormat.json}
49+
{ "dub.json", PackageFormat.json },
50+
{ "dub.sdl", PackageFormat.sdl },
51+
{ "dub.yaml", PackageFormat.yaml }, // Official extension
52+
{ "dub.yml", PackageFormat.yaml }, // Common alternative extension
53+
{ "package.json", PackageFormat.json },
5154
];
5255

5356
/// Returns a list of all recognized package recipe file names in descending order of precedence.

source/dub/recipe/io.d

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,11 @@ PackageRecipe parsePackageRecipe(string contents, string filename,
9090
PackageRecipe ret;
9191

9292
ret.name = default_package_name;
93-
94-
if (filename.endsWith(".json"))
95-
{
93+
if (filename.endsWith(".yaml") || filename.endsWith(".yml")) {
94+
// Warn users about unused field, but don't error for forward-compatibility
95+
ret = parseConfigString!PackageRecipe(contents, filename, StrictMode.Warn);
96+
fixDependenciesNames(ret.name, ret);
97+
} else if (filename.endsWith(".json")) {
9698
try {
9799
ret = parseConfigString!PackageRecipe(contents, filename, mode);
98100
fixDependenciesNames(ret.name, ret);
@@ -245,11 +247,15 @@ void serializePackageRecipe(R)(ref R dst, const scope ref PackageRecipe recipe,
245247
import dub.internal.vibecompat.data.json : writeJsonString;
246248
import dub.recipe.json : toJson;
247249
import dub.recipe.sdl : toSDL;
250+
import dub.recipe.yaml : toYAML;
248251

249252
if (filename.endsWith(".json"))
250253
dst.writeJsonString!(R, true)(toJson(recipe));
251254
else if (filename.endsWith(".sdl"))
252255
toSDL(recipe).toSDLDocument(dst);
256+
else if (filename.endsWith(".yaml") || filename.endsWith(".yml")) {
257+
toJson(recipe).toYAML(dst);
258+
}
253259
else assert(false, "writePackageRecipe called with filename with unknown extension: "~filename);
254260
}
255261

source/dub/recipe/yaml.d

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*******************************************************************************
2+
3+
YAML serialization helper
4+
5+
*******************************************************************************/
6+
7+
module dub.recipe.yaml;
8+
9+
import dub.internal.vibecompat.data.json;
10+
11+
import std.algorithm;
12+
import std.array : appender, Appender;
13+
import std.bigint;
14+
import std.format;
15+
import std.range;
16+
17+
package string toYAML (Json json) {
18+
auto sb = appender!string();
19+
serializeHelper(json, sb, 0);
20+
return sb.data;
21+
}
22+
23+
package void toYAML (R) (Json json, ref R dst) {
24+
serializeHelper(json, dst, 0);
25+
}
26+
27+
private void serializeHelper (R) (Json value, ref R dst, size_t indent, bool skipFirstIndent = false) {
28+
final switch (value.type) {
29+
case Json.Type.object:
30+
foreach (fieldName; FieldOrder) {
31+
if (auto ptr = fieldName in value) {
32+
serializeField(dst, fieldName, *ptr, skipFirstIndent ? 0 : indent);
33+
skipFirstIndent = false;
34+
}
35+
}
36+
foreach (string key, fieldValue; value) {
37+
if (FieldOrder.canFind(key)) continue;
38+
serializeField(dst, key, fieldValue, skipFirstIndent ? 0 : indent);
39+
skipFirstIndent = false;
40+
}
41+
break;
42+
case Json.Type.array:
43+
foreach (size_t idx, element; value) {
44+
formattedWrite(dst, "%*.*0$s- ", indent, ` `);
45+
46+
if (element.isScalar) {
47+
serializeHelper(element, dst, 0);
48+
} else {
49+
serializeHelper(element, dst, indent + 2, true);
50+
}
51+
}
52+
break;
53+
case Json.Type.string:
54+
formattedWrite(dst, `"%s"`, value.get!string);
55+
break;
56+
case Json.Type.bool_:
57+
dst.put(value.get!bool ? "true" : "false");
58+
break;
59+
case Json.Type.null_:
60+
dst.put("null");
61+
break;
62+
case Json.Type.int_:
63+
formattedWrite(dst, "%s", value.get!long);
64+
break;
65+
case Json.Type.bigInt:
66+
formattedWrite(dst, "%s", value.get!BigInt);
67+
break;
68+
case Json.Type.float_:
69+
formattedWrite(dst, "%s", value.get!double);
70+
break;
71+
case Json.Type.undefined:
72+
break;
73+
}
74+
if (value.isScalar)
75+
dst.put("\n");
76+
}
77+
78+
private void serializeField (R) (ref R dst, string key, Json fieldValue, size_t indent) {
79+
formattedWrite(dst, "%*.*0$s%s:", indent, ` `, key);
80+
if (fieldValue.isScalar) {
81+
dst.put(" ");
82+
serializeHelper(fieldValue, dst, 0);
83+
} else {
84+
dst.put("\n");
85+
serializeHelper(fieldValue, dst, indent + 2);
86+
}
87+
}
88+
89+
private bool isScalar(Json value) {
90+
return value.type != Json.Type.object && value.type != Json.Type.array;
91+
}
92+
93+
/// To get a better formatted YAML out of the box
94+
private immutable FieldOrder = [
95+
"name", "description", "homepage", "authors", "copyright", "license",
96+
"toolchainRequirements", "mainSourceFile", "dependencies", "configurations",
97+
];

source/dub/test/others.d

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,50 @@ unittest
120120
dub.loadPackage();
121121
assert(dub.project.hasAllDependencies());
122122
}
123+
124+
// Ensure that dub recognizes `dub.yaml`
125+
unittest
126+
{
127+
scope dubJSON = new TestDub((scope Filesystem fs) {
128+
fs.writeFile(TestDub.ProjectPath ~ "dub.json", `{"name":"json"}`);
129+
fs.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "sdl"`);
130+
fs.writeFile(TestDub.ProjectPath ~ "dub.yaml", `name: yaml`);
131+
fs.writeFile(TestDub.ProjectPath ~ "dub.yml", `name: yml`);
132+
fs.writeFile(TestDub.ProjectPath ~ "package.json", `{"name":"package"}`);
133+
});
134+
dubJSON.loadPackage();
135+
assert(dubJSON.project.name() == "json");
136+
137+
scope dubSDL = dubJSON.newTest((scope Filesystem fs) {
138+
fs.removeFile(TestDub.ProjectPath ~ "dub.json");
139+
});
140+
dubSDL.loadPackage();
141+
assert(dubSDL.project.name() == "sdl");
142+
143+
scope dubYAML = dubSDL.newTest((scope Filesystem fs) {
144+
fs.removeFile(TestDub.ProjectPath ~ "dub.sdl");
145+
});
146+
dubYAML.loadPackage();
147+
assert(dubYAML.project.name() == "yaml");
148+
149+
scope dubYML = dubYAML.newTest((scope Filesystem fs) {
150+
fs.removeFile(TestDub.ProjectPath ~ "dub.yaml");
151+
});
152+
dubYML.loadPackage();
153+
assert(dubYML.project.name() == "yml");
154+
155+
scope dubPackageJSON = dubYML.newTest((scope Filesystem fs) {
156+
fs.removeFile(TestDub.ProjectPath ~ "dub.yml");
157+
});
158+
dubPackageJSON.loadPackage();
159+
assert(dubPackageJSON.project.name() == "package");
160+
161+
scope dubNothing = dubPackageJSON.newTest((scope Filesystem fs) {
162+
fs.removeFile(TestDub.ProjectPath ~ "package.json");
163+
});
164+
try {
165+
dubNothing.loadPackage();
166+
assert(0, "dubNothing should have thrown");
167+
} catch (Exception exc)
168+
assert(exc.message().canFind("No package file found in"));
169+
}

test/0-init-interactive.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function runTest {
2626
# sdl package format
2727
runTest '1\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl
2828
# select package format out of bounds
29-
runTest '3\n1\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl
29+
runTest '4\n1\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl
3030
# select package format not numeric, but in list
3131
runTest 'sdl\ntest\ndesc\nauthor\ngpl\ncopy\n\n' 0-init-interactive.dub.sdl
3232
# selected value not numeric and not in list

0 commit comments

Comments
 (0)