diff --git a/build-files.txt b/build-files.txt index e1ba691..59a0b51 100644 --- a/build-files.txt +++ b/build-files.txt @@ -1,6 +1,7 @@ source/app.d source/configy/Attributes.d source/configy/Exceptions.d +source/configy/FieldRef.d source/configy/Read.d source/configy/Utils.d source/dub/commandline.d diff --git a/source/configy/Attributes.d b/source/configy/Attributes.d index 29a515f..b485823 100644 --- a/source/configy/Attributes.d +++ b/source/configy/Attributes.d @@ -292,7 +292,8 @@ public interface ConfigParser (T) { import dyaml.node; - import configy.Read : Context, parseFieldImpl = parseField, FieldRef; + import configy.FieldRef : FieldRef; + import configy.Read : Context, parseFieldImpl = parseField; /// Returns: the node being processed public inout(Node) node () inout @safe pure nothrow @nogc; diff --git a/source/configy/Exceptions.d b/source/configy/Exceptions.d index 41f0e1e..5e1f98c 100644 --- a/source/configy/Exceptions.d +++ b/source/configy/Exceptions.d @@ -374,8 +374,9 @@ scope SinkType sink, in FormatSpec!char spec) const scope @trusted { - // Here we break the type system too, calling a `@system` function, - // but since it takes no parameter, why would it not be `@safe` ? - sink(this.next.message()); + if (auto dyn = cast(ConfigException) this.next) + dyn.toString(sink, spec); + else + sink(this.next.message); } } diff --git a/source/configy/FieldRef.d b/source/configy/FieldRef.d new file mode 100644 index 0000000..8d84236 --- /dev/null +++ b/source/configy/FieldRef.d @@ -0,0 +1,196 @@ +/******************************************************************************* + + Implement a template to keep track of a field references + + Passing field references by `alias` template parameter creates many problem, + and is extremely cumbersome to work with. Instead, we pass an instance of + a `FieldRef` around, which also contains structured information. + + Copyright: + Copyright (c) 2019-2022 BOSAGORA Foundation + All rights reserved. + + License: + MIT License. See LICENSE for details. + +*******************************************************************************/ + +module configy.FieldRef; + +// Renamed imports as the names exposed by `FieldRef` shadow the imported ones. +import configy.Attributes : CAName = Name, CAOptional = Optional, SetInfo; + +import std.meta; +import std.traits; + +/******************************************************************************* + + A reference to a field in a `struct` + + The compiler sometimes rejects passing fields by `alias`, or complains about + missing `this` (meaning it tries to evaluate the value). Sometimes, it also + discards the UDAs. + + To prevent this from happening, we always pass around a `FieldRef`, + which wraps the parent struct type (`T`), the name of the field + as `FieldName`, and other informations. + + To avoid any issue, eponymous usage is also avoided, hence the reference + needs to be accessed using `Ref`. + +*******************************************************************************/ + +package template FieldRef (alias T, string name, bool forceOptional = false) +{ + /// The reference to the field + public alias Ref = __traits(getMember, T, name); + + /// Type of the field + public alias Type = typeof(Ref); + + /// The name of the field in the struct itself + public alias FieldName = name; + + /// The name used in the configuration field (taking `@Name` into account) + static if (hasUDA!(Ref, CAName)) + { + static assert (getUDAs!(Ref, CAName).length == 1, + "Field `" ~ fullyQualifiedName!(Ref) ~ + "` 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); + + /// Evaluates to `true` if this field is to be considered optional + /// (does not need to be present in the YAML document) + public enum Optional = forceOptional || + hasUDA!(Ref, CAOptional) || + is(immutable(Type) == immutable(bool)) || + is(Type : SetInfo!FT, FT) || + (Default != Type.init); +} + +unittest +{ + import configy.Attributes : Name; + + static struct Config1 + { + int integer2 = 42; + @Name("notStr2") + @(42) string str2; + } + + static struct Config2 + { + Config1 c1dup = { 42, "Hello World" }; + string message = "Something"; + } + + static struct Config3 + { + Config1 c1; + int integer; + string str; + Config2 c2 = { c1dup: { integer2: 69 } }; + } + + static assert(is(FieldRef!(Config3, "c2").Type == Config2)); + static assert(FieldRef!(Config3, "c2").Default != Config2.init); + static assert(FieldRef!(Config2, "message").Default == Config2.init.message); + alias NFR1 = FieldRef!(Config3, "c2"); + alias NFR2 = FieldRef!(NFR1.Ref, "c1dup"); + alias NFR3 = FieldRef!(NFR2.Ref, "integer2"); + alias NFR4 = FieldRef!(NFR2.Ref, "str2"); + static assert(hasUDA!(NFR4.Ref, int)); + + static assert(FieldRefTuple!(Config3)[1].Name == "integer"); + static assert(FieldRefTuple!(FieldRefTuple!(Config3)[0].Type)[1].Name == "notStr2"); +} + +/// A pseudo `FieldRef` used for structs which are not fields (top-level) +package template StructFieldRef (ST) +{ + /// + public enum Ref = ST.init; + + /// + public alias Type = ST; + + /// + public enum Default = ST.init; + + /// + public enum Optional = false; +} + +/// A pseudo `FieldRef` for nested types (e.g. arrays / associative arrays) +package template NestedFieldRef (ElemT, alias FR) +{ + /// + public enum Ref = ElemT.init; + /// + public alias Type = ElemT; + /// + public enum Name = FR.Name; + /// + public enum FieldName = FR.FieldName; + /// Element or keys are never optional + public enum Optional = false; + +} + +/// Get a tuple of `FieldRef` from a `struct` +package template FieldRefTuple (T) +{ + static assert(is(T == struct), + "Argument " ~ T.stringof ~ " to `FieldRefTuple` should be a `struct`"); + + /// + static if (__traits(getAliasThis, T).length == 0) + public alias FieldRefTuple = staticMap!(Pred, FieldNameTuple!T); + else + { + /// Tuple of strings of aliased fields + /// As of DMD v2.100.0, only a single alias this is supported in D. + private immutable AliasedFieldNames = __traits(getAliasThis, T); + static assert(AliasedFieldNames.length == 1, "Multiple `alias this` are not supported"); + + // Ignore alias to functions (if it's a property we can't do anything) + static if (isSomeFunction!(__traits(getMember, T, AliasedFieldNames))) + public alias FieldRefTuple = staticMap!(Pred, FieldNameTuple!T); + else + { + /// "Base" field names minus aliased ones + private immutable BaseFields = Erase!(AliasedFieldNames, FieldNameTuple!T); + static assert(BaseFields.length == FieldNameTuple!(T).length - 1); + + public alias FieldRefTuple = AliasSeq!( + staticMap!(Pred, BaseFields), + FieldRefTuple!(typeof(__traits(getMember, T, AliasedFieldNames)))); + } + } + + private alias Pred (string name) = FieldRef!(T, name); +} + +/// Returns: An alias sequence of field names, taking UDAs (`@Name` et al) into account +package alias FieldsName (T) = staticMap!(FieldRefToName, FieldRefTuple!T); + +/// Helper template for `staticMap` used for strict mode +private enum FieldRefToName (alias FR) = FR.Name; + +/// Dub extension +package enum IsPattern (alias FR) = FR.Pattern; +/// Dub extension +package alias Patterns (T) = staticMap!(FieldRefToName, Filter!(IsPattern, FieldRefTuple!T)); diff --git a/source/configy/Read.d b/source/configy/Read.d index 0cd230c..1e30d53 100644 --- a/source/configy/Read.d +++ b/source/configy/Read.d @@ -143,6 +143,7 @@ public import configy.Attributes; public import configy.Exceptions : ConfigException; import configy.Exceptions; +import configy.FieldRef; import configy.Utils; import dyaml.exception; @@ -375,7 +376,6 @@ cmdln = Command line arguments node = The root node matching `T` strict = Action to take when encountering unknown keys in the document - initPath = Unused Returns: An instance of `T` filled with the content of `node` @@ -387,7 +387,7 @@ *******************************************************************************/ public T parseConfig (T) ( - in CLIArgs cmdln, Node node, StrictMode strict = StrictMode.Error, string initPath = null) + in CLIArgs cmdln, Node node, StrictMode strict = StrictMode.Error) { static assert(is(T == struct), "`" ~ __FUNCTION__ ~ "` should only be called with a `struct` type as argument, not: `" ~ @@ -396,13 +396,12 @@ final switch (node.nodeID) { case NodeID.mapping: - dbgWrite("Parsing config '%s', strict: %s, initPath: %s", + dbgWrite("Parsing config '%s', strict: %s", fullyQualifiedName!T, strict == StrictMode.Warn ? - strict.paint(Yellow) : strict.paintIf(!!strict, Green, Red), - initPath.length ? initPath : "(none)"); + strict.paint(Yellow) : strict.paintIf(!!strict, Green, Red)); return node.parseMapping!(StructFieldRef!T)( - initPath, T.init, const(Context)(cmdln, strict), null); + null, T.init, const(Context)(cmdln, strict), null); case NodeID.sequence: case NodeID.scalar: case NodeID.invalid: @@ -437,15 +436,6 @@ private StrictMode strict; } -/// 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 mapping from `node` into an instance of `T` @@ -461,7 +451,6 @@ fieldDefaults = Default value for some fields, used for `Key` recursion *******************************************************************************/ -/// Parse a single mapping, recurse as needed private TLFR.Type parseMapping (alias TLFR) (Node node, string path, auto ref TLFR.Type defaultValue, in Context ctx, in Node[string] fieldDefaults) @@ -563,15 +552,7 @@ { static if (is(FR.Type : V[K], K, V)) { - static struct AAFieldRef - { - /// - private enum Ref = V.init; - /// - private alias Type = V; - private enum Optional = FR.Optional; - } - + alias AAFieldRef = NestedFieldRef!(V, FR); static assert(is(K : string), "Key type should be string-like"); } else @@ -766,21 +747,13 @@ if (node.nodeID != NodeID.mapping) throw new TypeConfigException(node, "mapping (associative array)", path); - static struct AAFieldRef - { - /// - private enum Ref = E.init; - /// - private alias Type = E; - } - // Note: As of June 2022 (DMD v2.100.0), associative arrays cannot // have initializers, hence their UX for config is less optimal. return node.mapping().map!( (Node.Pair pair) { return tuple( pair.key.get!K, - pair.value.parseField!(AAFieldRef)( + pair.value.parseField!(NestedFieldRef!(E, FR))( format("%s[%s]", path, pair.key.as!string), E.init, ctx)); }).assocArray(); @@ -818,20 +791,11 @@ if (node.nodeID != NodeID.sequence) throw new TypeConfigException(node, "sequence (array)", path); - // Only those two fields are used by `parseFieldImpl` - 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)( + kv => kv.value.parseField!(NestedFieldRef!(E, FR))( format("%s[%s]", path, kv.index), E.init, ctx)) .array(); } @@ -881,6 +845,8 @@ { try return exp; + catch (ConfigException exc) + throw exc; catch (Exception exc) throw new ConstructionException(exc, path, position, file, line); } @@ -994,114 +960,6 @@ } } -/******************************************************************************* - - A reference to a field in a `struct` - - The compiler sometimes rejects passing fields by `alias`, or complains about - missing `this` (meaning it tries to evaluate the value). Sometimes, it also - discards the UDAs. - - To prevent this from happening, we always pass around a `FieldRef`, - which wraps the parent struct type (`T`) and the name of the field (`name`). - - To avoid any issue, eponymous usage is also avoided, hence the reference - needs to be accessed using `Ref`. A convenience `Type` alias is provided, - as well as `Default`. - -*******************************************************************************/ - -package template FieldRef (alias T, string name, bool forceOptional = false) -{ - // Renamed imports as the names exposed by this template clash - // with what we import. - import configy.Attributes : CAName = Name, CAOptional = Optional; - - /// The reference to the field - public alias Ref = __traits(getMember, T, name); - - /// Type of the field - public alias Type = typeof(Ref); - - /// The name of the field in the struct itself - public alias FieldName = name; - - /// The name used in the configuration field (taking `@Name` into account) - static if (hasUDA!(Ref, CAName)) - { - static assert (getUDAs!(Ref, CAName).length == 1, - "Field `" ~ fullyQualifiedName!(Ref) ~ - "` 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); - - /// Evaluates to `true` if this field is to be considered optional - /// (does not need to be present in the YAML document) - public enum Optional = forceOptional || - hasUDA!(Ref, CAOptional) || - is(immutable(Type) == immutable(bool)) || - is(Type : SetInfo!FT, FT) || - (Default != Type.init); -} - -/// A pseudo `FieldRef` used for structs which are not fields (top-level) -private template StructFieldRef (ST) -{ - /// - public alias Type = ST; - - /// - public enum Default = ST.init; - - /// - public enum Optional = false; -} - -/// Get a tuple of `FieldRef` from a `struct` -private template FieldRefTuple (T) -{ - static assert(is(T == struct), - "Argument " ~ T.stringof ~ " to `FieldRefTuple` should be a `struct`"); - - /// - static if (__traits(getAliasThis, T).length == 0) - public alias FieldRefTuple = staticMap!(Pred, FieldNameTuple!T); - else - { - /// Tuple of strings of aliased fields - /// As of DMD v2.100.0, only a single alias this is supported in D. - private immutable AliasedFieldNames = __traits(getAliasThis, T); - static assert(AliasedFieldNames.length == 1, "Multiple `alias this` are not supported"); - - // Ignore alias to functions (if it's a property we can't do anything) - static if (isSomeFunction!(__traits(getMember, T, AliasedFieldNames))) - public alias FieldRefTuple = staticMap!(Pred, FieldNameTuple!T); - else - { - /// "Base" field names minus aliased ones - private immutable BaseFields = Erase!(AliasedFieldNames, FieldNameTuple!T); - static assert(BaseFields.length == FieldNameTuple!(T).length - 1); - - public alias FieldRefTuple = AliasSeq!( - staticMap!(Pred, BaseFields), - FieldRefTuple!(typeof(__traits(getMember, T, AliasedFieldNames)))); - } - } - - private alias Pred (string name) = FieldRef!(T, name); -} - /// Helper predicate private template NameIs (string searching) { @@ -1168,42 +1026,6 @@ private bool fieldValue; } -unittest -{ - static struct Config1 - { - int integer2 = 42; - @Name("notStr2") - @(42) string str2; - } - - static struct Config2 - { - Config1 c1dup = { 42, "Hello World" }; - string message = "Something"; - } - - static struct Config3 - { - Config1 c1; - int integer; - string str; - Config2 c2 = { c1dup: { integer2: 69 } }; - } - - static assert(is(FieldRef!(Config3, "c2").Type == Config2)); - static assert(FieldRef!(Config3, "c2").Default != Config2.init); - static assert(FieldRef!(Config2, "message").Default == Config2.init.message); - alias NFR1 = FieldRef!(Config3, "c2"); - alias NFR2 = FieldRef!(NFR1.Ref, "c1dup"); - alias NFR3 = FieldRef!(NFR2.Ref, "integer2"); - alias NFR4 = FieldRef!(NFR2.Ref, "str2"); - static assert(hasUDA!(NFR4.Ref, int)); - - static assert(FieldRefTuple!(Config3)[1].Name == "integer"); - static assert(FieldRefTuple!(FieldRefTuple!(Config3)[0].Type)[1].Name == "notStr2"); -} - /// 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)))); diff --git a/source/configy/Test.d b/source/configy/Test.d index b8137e7..a8a36a6 100644 --- a/source/configy/Test.d +++ b/source/configy/Test.d @@ -661,3 +661,34 @@ assert(c.ifaces.length == 2); assert(c.ifaces == [ Interface("eth0", "192.168.1.42"), Interface("lo", "127.0.0.42")]); } + +// Nested ConstructionException +unittest +{ + static struct WillFail + { + string name; + this (string value) @safe pure + { + throw new Exception("Parsing failed!"); + } + } + + static struct Container + { + WillFail[] array; + } + + static struct Config + { + Container data; + } + + try auto c = parseConfigString!Config(`data: + array: + - Not + - Working +`, "/dev/null"); + catch (Exception exc) + assert(exc.toString() == `/dev/null(2:6): data.array[0]: Parsing failed!`); +}