diff --git a/source/configy/Attributes.d b/source/configy/Attributes.d index a3bcbe0..04316a1 100644 --- a/source/configy/Attributes.d +++ b/source/configy/Attributes.d @@ -108,6 +108,9 @@ { /// public string name; + + /// + public bool startsWith; } /******************************************************************************* @@ -260,10 +263,8 @@ public struct Converter (T) { - import dyaml.node; - /// - public alias ConverterFunc = T function (Node input); + public alias ConverterFunc = T function (scope ConfigParser!T context); /// public ConverterFunc converter; @@ -281,3 +282,29 @@ "Error: Converter needs to be of the return type of the field, not `void`"); return Converter!RType(func); } + +public interface ConfigParser (T) +{ + import dyaml.node; + import configy.Read : Context, parseFieldImpl, FieldRef; + + /// Returns: the node being processed + public inout(Node) node () inout @safe pure nothrow @nogc; + + /// Returns: current location we are parsing + public string path () const @safe pure nothrow @nogc; + + /// + public final auto parseField (string FieldName) () + { + static assert(__traits(hasMember, T, FieldName), + "`" ~ FieldName ~ "` is not a field of type `" ~ + fullyQualifiedName!T ~ "`"); + + alias FR = FieldRef!(T, FieldName); + return parseFieldImpl!(FR)(this.node(), this.path(), FR.Default, this.context()); + } + + /// Internal use only + protected const(Context) context () const @safe pure nothrow @nogc; +} diff --git a/source/configy/Read.d b/source/configy/Read.d index ddba413..0089072 100644 --- a/source/configy/Read.d +++ b/source/configy/Read.d @@ -431,7 +431,7 @@ } /// Used to pass around configuration -private struct Context +package struct Context { /// private CLIArgs cmdln; @@ -443,9 +443,13 @@ /// Helper template for `staticMap` used for strict mode private enum FieldRefToName (alias FR) = FR.Name; +private enum IsPattern (alias FR) = FR.Pattern; + /// Returns: An alias sequence of field names, taking UDAs (`@Name` et al) into account private alias FieldsName (T) = staticMap!(FieldRefToName, FieldRefTuple!T); +private alias Patterns (T) = staticMap!(FieldRefToName, Filter!(IsPattern, FieldRefTuple!T)); + /// Parse a single mapping, recurse as needed private T parseMapping (T) (Node node, string path, auto ref T defaultValue, in Context ctx, in Node[string] fieldDefaults) @@ -472,10 +476,19 @@ /// First, check that all the sections found in the mapping are present in the type /// If not, the user might have made a typo. immutable string[] fieldNames = [ FieldsName!T ]; - foreach (const ref Node key, const ref Node value; node) + immutable string[] patterns = [ Patterns!T ]; + FIELD: foreach (const ref Node key, const ref Node value; node) { - if (!fieldNames.canFind(key.as!string)) + const k = key.as!string; + if (!fieldNames.canFind(k)) { + foreach (p; patterns) + if (k.startsWith(p)) + // Require length because `0` would match `canFind` + // and we don't want to allow `$PATTERN-` + if (k[p.length .. $].length > 1 && k[p.length] == '-') + continue FIELD; + if (ctx.strict == StrictMode.Warn) { scope exc = new UnknownKeyConfigException( @@ -528,17 +541,55 @@ if (ctx.strict && FR.FieldName in node) throw new ConfigExceptionImpl("'Key' field is specified twice", path, FR.FieldName, node.startMark()); - return (*ptr).parseField!(FR)(path.addPath(FR.FieldName), default_, ctx) + return (*ptr).parseFieldImpl!(FR)(path.addPath(FR.FieldName), default_, ctx) .dbgWriteRet("Using value '%s' from fieldDefaults for field '%s'", FR.FieldName.paint(Cyan)); } + // This, `FR.Pattern`, and the field in `@Name` are special support for `dub` + static if (FR.Pattern) + { + static if (is(FR.Type : V[K], K, V)) + { + static struct AAFieldRef + { + /// + private enum Ref = V.init; + /// + private alias Type = V; + } + + static assert(is(K : string), "Key type should be string-like"); + } + else + static assert(0, "Cannot have pattern on non-AA field"); + + AAFieldRef.Type[string] result; + foreach (pair; node.mapping) + { + const key = pair.key.as!string; + if (!key.startsWith(FR.Name)) + continue; + string suffix = key[FR.Name.length .. $]; + if (suffix.length) + { + if (suffix[0] == '-') suffix = suffix[1 .. $]; + else continue; + } + + result[suffix] = pair.value.parseFieldImpl!(AAFieldRef)( + path.addPath(key), default_.get(key, AAFieldRef.Type.init), ctx); + } + bool hack = true; + if (hack) return result; + } + if (auto ptr = FR.Name in node) { dbgWrite("%s: YAML field is %s in node%s", FR.Name.paint(Cyan), "present".paint(Green), (FR.Name == FR.FieldName ? "" : " (note that field name is overriden)").paint(Yellow)); - return (*ptr).parseField!(FR)(path.addPath(FR.Name), default_, ctx) + return (*ptr).parseFieldImpl!(FR)(path.addPath(FR.Name), default_, ctx) .dbgWriteRet("Using value '%s' from YAML document for field '%s'", FR.FieldName.paint(Cyan)); } @@ -644,7 +695,7 @@ *******************************************************************************/ -private FR.Type parseField (alias FR) +package FR.Type parseFieldImpl (alias FR) (Node node, string path, auto ref FR.Type defaultValue, in Context ctx) { if (node.nodeID == NodeID.invalid) @@ -654,11 +705,17 @@ // to peel the type static if (is(FR.Type : SetInfo!FT, FT)) return FR.Type( - parseField!(FieldRef!(FR.Type, "value"))(node, path, defaultValue, ctx), + parseFieldImpl!(FieldRef!(FR.Type, "value"))(node, path, defaultValue, ctx), true); else static if (hasConverter!(FR.Ref)) - return wrapException(node.viaConverter!(FR), path, node.startMark()); + return wrapException(node.viaConverter!(FR)(path, ctx), path, node.startMark()); + + else static if (hasFromYAML!(FR.Type)) + { + scope impl = new ConfigParserImpl!(FR.Type)(node, path, ctx); + return wrapException(FR.Type.fromYAML(impl), path, node.startMark()); + } else static if (hasFromString!(FR.Type)) return wrapException(FR.Type.fromString(node.as!string), path, node.startMark()); @@ -705,7 +762,7 @@ (Node.Pair pair) { return tuple( pair.key.get!K, - pair.value.parseField!(AAFieldRef)( + pair.value.parseFieldImpl!(AAFieldRef)( format("%s[%s]", path, pair.key.as!string), E.init, ctx)); }).assocArray(); @@ -714,9 +771,6 @@ { static if (hasUDA!(FR.Ref, Key)) { - if (node.nodeID != NodeID.mapping) - throw new TypeConfigException(node, "mapping (object)", path); - static assert(getUDAs!(FR.Ref, Key).length == 1, "`" ~ fullyQualifiedName!(FR.Ref) ~ "` field shouldn't have more than one `Key` attribute"); @@ -726,7 +780,11 @@ fullyQualifiedName!E ~ "`, not a sequence of `struct`"); string key = getUDAs!(FR.Ref, Key)[0].name; - return node.mapping().map!( + + if (node.nodeID != NodeID.mapping && node.nodeID != NodeID.sequence) + throw new TypeConfigException(node, "mapping (object) or sequence", path); + + if (node.nodeID == NodeID.mapping) return node.mapping().map!( (Node.Pair pair) { if (pair.value.nodeID != NodeID.mapping) throw new TypeConfigException( @@ -739,28 +797,25 @@ E.init, ctx, key.length ? [ key: pair.key ] : null); }).array(); } - else + if (node.nodeID != NodeID.sequence) + throw new TypeConfigException(node, "sequence (array)", path); + + // Only those two fields are used by `parseFieldImpl` + static struct ArrayFieldRef { - if (node.nodeID != NodeID.sequence) - throw new TypeConfigException(node, "sequence (array)", path); - - // Only those two fields are used by `parseField` - static struct ArrayFieldRef - { - /// - private enum Ref = E.init; - /// - private alias Type = E; - } - - // We pass `E.init` as default value as it is not going to be used: - // Either there is something in the YAML document, and that will be - // converted, or `sequence` will not iterate. - return node.sequence.enumerate.map!( - kv => kv.value.parseField!(ArrayFieldRef)( - format("%s[%s]", path, kv.index), E.init, ctx)) - .array(); + /// + private enum Ref = E.init; + /// + private alias Type = E; } + + // We pass `E.init` as default value as it is not going to be used: + // Either there is something in the YAML document, and that will be + // converted, or `sequence` will not iterate. + return node.sequence.enumerate.map!( + kv => kv.value.parseFieldImpl!(ArrayFieldRef)( + format("%s[%s]", path, kv.index), E.init, ctx)) + .array(); } else { @@ -902,7 +957,7 @@ /// Provided a field reference `FR` which is known to have at least one converter, /// perform basic checks and return the value after applying the converter. -private auto viaConverter (alias FR) (Node node) +private auto viaConverter (alias FR) (Node node, string path, in Context context) { enum Converters = getUDAs!(FR.Ref, Converter); static assert (Converters.length, @@ -911,7 +966,39 @@ static assert(Converters.length == 1, "Field `" ~ FR.FieldName ~ "` cannot have more than one `Converter`"); - return Converters[0].converter(node); + + scope impl = new ConfigParserImpl!(FR.Type)(node, path, context); + return Converters[0].converter(impl); +} + +private final class ConfigParserImpl (T) : ConfigParser!T +{ + private Node node_; + private string path_; + private const(Context) context_; + + /// Ctor + public this (Node n, string p, const Context c) scope @safe pure nothrow @nogc + { + this.node_ = n; + this.path_ = p; + this.context_ = c; + } + + public final override inout(Node) node () inout @safe pure nothrow @nogc + { + return this.node_; + } + + public final override string path () const @safe pure nothrow @nogc + { + return this.path_; + } + + protected final override const(Context) context () const @safe pure nothrow @nogc + { + return this.context_; + } } /******************************************************************************* @@ -931,7 +1018,7 @@ *******************************************************************************/ -private template FieldRef (alias T, string name, bool forceOptional = false) +package template FieldRef (alias T, string name, bool forceOptional = false) { // Renamed imports as the names exposed by this template clash // with what we import. @@ -954,9 +1041,14 @@ "` cannot have more than one `Name` attribute"); public immutable Name = getUDAs!(Ref, CAName)[0].name; + + public immutable Pattern = getUDAs!(Ref, CAName)[0].startsWith; } else + { public immutable Name = FieldName; + public immutable Pattern = false; + } /// Default value of the field (may or may not be `Type.init`) public enum Default = __traits(getMember, T.init, name); @@ -1109,6 +1201,9 @@ /// Evaluates to `true` if `T` is a `struct` with a default ctor private enum hasFieldwiseCtor (T) = (is(T == struct) && is(typeof(() => T(T.init.tupleof)))); +/// Evaluates to `true` if `T` has a static method that is designed to work with this library +private enum hasFromYAML (T) = is(typeof(T.fromYAML(ConfigParser!(T).init)) : T); + /// Evaluates to `true` if `T` has a static method that accepts a `string` and returns a `T` private enum hasFromString (T) = is(typeof(T.fromString(string.init)) : T); diff --git a/source/configy/Test.d b/source/configy/Test.d index 1b04e0a..cf28a8f 100644 --- a/source/configy/Test.d +++ b/source/configy/Test.d @@ -330,10 +330,10 @@ @Optional ThrowingFromString fromString; @Converter!int( - (Node value) { + (scope ConfigParser!int parser) { // We have to trick DMD a bit so that it infers an `int` return // type but doesn't emit a "Statement is not reachable" warning - if (value is Node.init || value !is Node.init ) + if (parser.node is Node.init || parser.node !is Node.init ) throw new Exception("You shall not pass"); return 42; }) @@ -625,3 +625,64 @@ assert(c.ifaces.length == 2); assert(c.ifaces == [ Interface("eth0", "192.168.1.42"), Interface("lo", "127.0.0.42")]); } + +unittest +{ + static struct Config + { + @Name("names", true) + string[][string] names_; + } + + auto c = parseConfigString!Config("names-x86:\n - John\n - Luca\nnames:\n - Marie", "/dev/null"); + assert(c.names_[null] == [ "Marie" ]); + assert(c.names_["x86"] == [ "John", "Luca" ]); +} + +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.parseField!"def"); + else + return Package(parser.parseField!"path"); + } + } + + 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")); +}