Newer
Older
dub_jkp / source / configy / Attributes.d
/*******************************************************************************

    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 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 informations 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 dyaml.node;
    import configy.FieldRef : FieldRef;
    import configy.Read : Context, parseFieldImpl = 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 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;
}