/******************************************************************************* Define UDAs that can be applied to a configuration struct This module is stand alone (a leaf module) to allow importing the UDAs without importing the whole configuration parsing code. Copyright: Copyright (c) 2019-2022 BOSAGORA Foundation All rights reserved. License: MIT License. See LICENSE for details. *******************************************************************************/ module dub.internal.configy.Attributes; import std.traits; /******************************************************************************* An optional parameter with an initial value of `T.init` The config parser automatically recognize non-default initializer, so that the following: ``` public struct Config { public string greeting = "Welcome home"; } ``` Will not error out if `greeting` is not defined in the config file. However, this relies on the initializer of the field (`greeting`) being different from the type initializer (`string.init` is `null`). In some cases, the default value is also the desired initializer, e.g.: ``` public struct Config { /// Maximum number of connections. 0 means unlimited. public uint connections_limit = 0; } ``` In this case, one can add `@Optional` to the field to inform the parser. *******************************************************************************/ public struct Optional {} /******************************************************************************* Inform the config filler that this sequence is to be read as a mapping On some occasions, one might want to read a mapping as an array. One reason to do so may be to provide a better experience to the user, e.g. having to type: ``` interfaces: eth0: ip: "192.168.0.1" private: true wlan0: ip: "1.2.3.4" ``` Instead of the slightly more verbose: ``` interfaces: - name: eth0 ip: "192.168.0.1" private: true - name: wlan0 ip: "1.2.3.4" ``` The former would require to be expressed as an associative arrays. However, one major drawback of associative arrays is that they can't have an initializer, which makes them cumbersome to use in the context of the config filler. To remediate this issue, one may use `@Key("name")` on a field (here, `interfaces`) so that the mapping is flattened to an array. If `name` is `null`, the key will be discarded. *******************************************************************************/ public struct Key { /// public string name; } /******************************************************************************* Look up the provided name in the YAML node, instead of the field name. By default, the config filler will look up the field name of a mapping in the YAML node. If this is not desired, an explicit `Name` attribute can be given. This is especially useful for names which are keyword. ``` public struct Config { public @Name("delete") bool remove; } ``` *******************************************************************************/ public struct Name { /// public string name; /// public bool startsWith; } /// Short hand syntax public Name StartsWith(string name) @safe pure nothrow @nogc { return Name(name, true); } /******************************************************************************* A field which carries information about whether it was set or not Some configurations may need to know which fields were set explicitly while keeping defaults. An example of this is a `struct` where at least one field needs to be set, such as the following: ``` public struct ProtoDuration { public @Optional long weeks; public @Optional long days; public @Optional long hours; public @Optional long minutes; public long seconds = 42; public @Optional long msecs; public @Optional long usecs; public @Optional long hnsecs; public @Optional long nsecs; } ``` In this case, it would be impossible to know if any field was explicitly provided. Hence, the struct should be written as: ``` public struct ProtoDuration { public SetInfo!long weeks; public SetInfo!long days; public SetInfo!long hours; public SetInfo!long minutes; public SetInfo!long seconds = 42; public SetInfo!long msecs; public SetInfo!long usecs; public SetInfo!long hnsecs; public SetInfo!long nsecs; } ``` Note that `SetInfo` implies `Optional`, and supports default values. *******************************************************************************/ public struct SetInfo (T) { /*************************************************************************** Allow initialization as a field This sets the field as having been set, so that: ``` struct Config { SetInfo!Duration timeout; } Config myConf = { timeout: 10.minutes } ``` Will behave as if set explicitly. If this behavior is not wanted, pass `false` as second argument: ``` Config myConf = { timeout: SetInfo!Duration(10.minutes, false) } ``` ***************************************************************************/ public this (T initVal, bool isSet = true) @safe pure nothrow @nogc { this.value = initVal; this.set = isSet; } /// Underlying data public T value; /// alias value this; /// Whether this field was set or not public bool set; } /******************************************************************************* Provides a means to convert a field from a `Node` to a complex type When filling the config, it might be useful to store types which are not only simple `string` and integer, such as `URL`, `BigInt`, or any other library type not directly under the user's control. To allow reading those values from the config file, a `Converter` may be used. The converter will tell the `ConfigFiller` how to convert from `Node` to the desired type `T`. If the type is under the user's control, one can also add a constructor accepting a single string, or define the `fromString` method, both of which are tried if no `Converter` is found. For types not under the user's control, there might be different ways to parse the same type within the same struct, or neither the ctor nor the `fromString` method may be defined under that name. The exmaple below uses `parse` in place of `fromString`, for example. ``` /// Complex structure representing the age of a person based on its birthday public struct Age { /// public uint birth_year; /// public uint birth_month; /// public uint birth_day; /// Note that this will be picked up automatically if named `fromString` /// but this struct might be a library type. public static Age parse (string value) { /+ Magic +/ } } public struct Person { /// @Converter!Age((Node value) => Age.parse(value.as!string)) public Age age; } ``` Note that some fields may also be of multiple YAML types, such as DUB's `dependencies`, which is either a simple string (`"vibe-d": "~>1.0 "`), or an in its complex form (`"vibe-d": { "version": "~>1.0" }`). For those use cases, a `Converter` is the best approach. To avoid repeating the field type, a convenience function is provided: ``` public struct Age { public uint birth_year; public uint birth_month; public uint birth_day; public static Age parse (string value) { /+ Magic +/ } } public struct Person { /// Here `converter` will deduct the type from the delegate argument, /// and return an instance of `Converter`. Mind the case. @converter((Node value) => Age.parse(value.as!string)) public Age age; } ``` *******************************************************************************/ public struct Converter (T) { /// public alias ConverterFunc = T function (scope ConfigParser!T context); /// public ConverterFunc converter; } /// Ditto public auto converter (FT) (FT func) { static assert(isFunctionPointer!FT, "Error: Argument to `converter` should be a function pointer, not: " ~ FT.stringof); alias RType = ReturnType!FT; static assert(!is(RType == void), "Error: Converter needs to be of the return type of the field, not `void`"); return Converter!RType(func); } public interface ConfigParser (T) { import dub.internal.dyaml.node; import dub.internal.configy.FieldRef : StructFieldRef; import dub.internal.configy.Read : Context, parseField; /// 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 parseAs (OtherType) (auto ref OtherType defaultValue = OtherType.init) { alias TypeFieldRef = StructFieldRef!OtherType; return this.node().parseField!(TypeFieldRef)( this.path(), defaultValue, this.context()); } /// Internal use only protected const(Context) context () const @safe pure nothrow @nogc; }