diff --git a/source/dub/internal/configy/Attributes.d b/source/dub/internal/configy/Attributes.d index 7829064..474d33a 100644 --- a/source/dub/internal/configy/Attributes.d +++ b/source/dub/internal/configy/Attributes.d @@ -289,6 +289,19 @@ return Converter!RType(func); } +/******************************************************************************* + + Interface that is passed to `fromYAML` hook + + The `ConfigParser` exposes the raw YAML node (`see `node` method), + the path within the file (`path` method), and a simple ability to recurse + via `parseAs`. + + Params: + T = The type of the structure which defines a `fromYAML` hook + +*******************************************************************************/ + public interface ConfigParser (T) { import dub.internal.dyaml.node; @@ -301,7 +314,20 @@ /// Returns: current location we are parsing public string path () const @safe pure nothrow @nogc; - /// + /*************************************************************************** + + Parse this struct as another type + + This allows implementing union-like behavior, where a `struct` which + implements `fromYAML` can parse a simple representation as one type, + and one more advanced as another type. + + Params: + OtherType = The type to parse as + defaultValue = The instance to use as a default value for fields + + ***************************************************************************/ + public final auto parseAs (OtherType) (auto ref OtherType defaultValue = OtherType.init) { diff --git a/source/dub/internal/configy/FieldRef.d b/source/dub/internal/configy/FieldRef.d index 1af4b67..42bc135 100644 --- a/source/dub/internal/configy/FieldRef.d +++ b/source/dub/internal/configy/FieldRef.d @@ -118,7 +118,14 @@ static assert(FieldRefTuple!(FieldRefTuple!(Config3)[0].Type)[1].Name == "notStr2"); } -/// A pseudo `FieldRef` used for structs which are not fields (top-level) +/** + * A pseudo `FieldRef` used for structs which are not fields (top-level) + * + * Params: + * ST = Type for which this pseudo-FieldRef is + * DefaultName = A name to give to this FieldRef, default to `null`, + * but required to prevent forward references in `parseAs`. + */ package template StructFieldRef (ST, string DefaultName = null) { /// diff --git a/source/dub/internal/configy/Read.d b/source/dub/internal/configy/Read.d index 44afcf1..e23a037 100644 --- a/source/dub/internal/configy/Read.d +++ b/source/dub/internal/configy/Read.d @@ -21,14 +21,17 @@ To mark a field as optional even with its default value, use the `Optional` UDA: `@Optional int count = 0;`. - Converter: - Because config structs may contain complex types such as - a Phobos type, a user-defined `Amount`, or Vibe.d's `URL`, - one may need to apply a converter to a struct's field. - Converters are functions that take a YAML `Node` as argument - and return a type that is implicitly convertible to the field type - (usually just the field type). They offer the most power to users, - as they can inspect the YAML structure, but should be used as a last resort. + fromYAML: + Because config structs may contain complex types outside of the project's + control (e.g. a Phobos type, Vibe.d's `URL`, etc...) or one may want + the config format to be more dynamic (e.g. by exposing union-like behavior), + one may need to apply more custom logic than what Configy does. + For this use case, one can define a `fromYAML` static method in the type: + `static S fromYAML(scope ConfigParser!S parser)`, where `S` is the type of + the enclosing structure. Structs with `fromYAML` will have this method + called instead of going through the normal parsing rules. + The `ConfigParser` exposes the current path of the field, as well as the + raw YAML `Node` itself, allowing for maximum flexibility. Composite_Types: Processing starts from a `struct` at the top level, and recurse into @@ -400,8 +403,8 @@ fullyQualifiedName!T, strict == StrictMode.Warn ? strict.paint(Yellow) : strict.paintIf(!!strict, Green, Red)); - return node.parseMapping!(StructFieldRef!T)( - null, T.init, const(Context)(cmdln, strict), null); + return node.parseField!(StructFieldRef!T)( + null, T.init, const(Context)(cmdln, strict)); case NodeID.sequence: case NodeID.scalar: case NodeID.invalid: diff --git a/source/dub/internal/configy/Test.d b/source/dub/internal/configy/Test.d index a334b6d..daa8d5d 100644 --- a/source/dub/internal/configy/Test.d +++ b/source/dub/internal/configy/Test.d @@ -692,3 +692,127 @@ catch (Exception exc) assert(exc.toString() == `/dev/null(2:6): data.array[0]: Parsing failed!`); } + +/// Test for error message: Has to be versioned out, uncomment to check manually +unittest +{ + static struct Nested + { + int field1; + + private this (string arg) {} + } + + static struct Config + { + Nested nested; + } + + static struct Config2 + { + Nested nested; + alias nested this; + } + + version(none) auto c1 = parseConfigString!Config(null, null); + version(none) auto c2 = parseConfigString!Config2(null, null); +} + +/// Test support for `fromYAML` hook +unittest +{ + static struct PackageDef + { + string name; + @Optional string target; + int build = 42; + } + + static struct Package + { + string path; + PackageDef def; + + public static Package fromYAML (scope ConfigParser!Package parser) + { + if (parser.node.nodeID == NodeID.mapping) + return Package(null, parser.parseAs!PackageDef); + else + return Package(parser.parseAs!string); + } + } + + static struct Config + { + string name; + Package[] deps; + } + + auto c = parseConfigString!Config( +` +name: myPkg +deps: + - /foo/bar + - name: foo + target: bar + build: 24 + - name: fur + - /one/last/path +`, "/dev/null"); + assert(c.name == "myPkg"); + assert(c.deps.length == 4); + assert(c.deps[0] == Package("/foo/bar")); + assert(c.deps[1] == Package(null, PackageDef("foo", "bar", 24))); + assert(c.deps[2] == Package(null, PackageDef("fur", null, 42))); + assert(c.deps[3] == Package("/one/last/path")); +} + +/// Test top level hook (fromYAML / fromString) +unittest +{ + static struct Version1 { + uint fileVersion; + uint value; + } + + static struct Version2 { + uint fileVersion; + string str; + } + + static struct Config + { + uint fileVersion; + union { + Version1 v1; + Version2 v2; + } + static Config fromYAML (scope ConfigParser!Config parser) + { + static struct OnlyVersion { uint fileVersion; } + auto vers = parseConfig!OnlyVersion( + CLIArgs.init, parser.node, StrictMode.Ignore); + switch (vers.fileVersion) { + case 1: + return Config(1, parser.parseAs!Version1); + case 2: + Config conf = Config(2); + conf.v2 = parser.parseAs!Version2; + return conf; + default: + assert(0); + } + } + } + + auto v1 = parseConfigString!Config("fileVersion: 1\nvalue: 42", "/dev/null"); + auto v2 = parseConfigString!Config("fileVersion: 2\nstr: hello world", "/dev/null"); + + assert(v1.fileVersion == 1); + assert(v1.v1.fileVersion == 1); + assert(v1.v1.value == 42); + + assert(v2.fileVersion == 2); + assert(v2.v2.fileVersion == 2); + assert(v2.v2.str == "hello world"); +}