Skip to content

Latest commit

 

History

History
362 lines (299 loc) · 10.1 KB

FAQ.md

File metadata and controls

362 lines (299 loc) · 10.1 KB

Common patterns while writing configurations

The following list covers common use-case encountered while writing a configuration.

When multiple ways to handle the same outcome exists, the recipes will list them by order of importance. For example, when considering having an optional field, first consider giving it an initializer, and only if the .init problem present itself, use @Optional. While the last option, using SetInfo, achieves the desired outcome, it should be avoided unless needed.

How do I ... ?

Allow fields in my YAML file that are not present in the struct (unknown fields)

By default, the library runs on "strict" mode, and any field found in the document that is not part of the struct definition will result in an error, indicating what fields are allowed for the section. This is the default as it prevent any unnoticed misconfiguration that would result from a typo to an optional field name. To disable strict parsing, simply pass StrictMode.Ignore as the optional parameter to either parseConfigFile, parseConfigString, or parseConfigFileSimple method. To notify the user without triggering an error, use StrictMode.Warn instead.

Make a field required

All fields are required by default, except for bool fields, which are always optional.

Make a field optional

Any field that has an initializer different from its .init value is considered optional, except for bool which is always optional.

For example, in the following configuration, dns is optional:

struct Config
{
    string dns = "8.8.8.8";
}

In some cases, the .init value is the desired default value, in which case @Optional can be used:

struct Config
{
    /// Default to 0, unlimited
    @Optional size_t connection_limit;
}

Finally, as a byproduct of its functionality, using SetInfo implicitly makes a field optional.

Known when a field is set

To know if a field of type T is set, use SetInfo!T in the field definition.

This can be useful in some special cases:

  • Two fields are mutually exclusive but at least one of them is required;
  • One or more fields need to have default values, but some extra verification needs to be taken if one is set;
  • One want to print a better error message when the program fails, depending on the configuration that was actually provided by the user (e.g. to differentiate a default value from a user-provided value which is identical);
struct Config
{
    /// Use a fixed set of peers
    SetInfo!(string[]) peer_list;

    /// Use a service discovery server
    SetInfo!(string) discovery_server;

    void validate () const
    {
        if (this.peer_list.set && this.discovery_server.set)
            throw new Exception("Both peer_list and discovery_server can't be set at the same time");
        if (!this.peer_list.set && !this.discovery_server.set)
            throw new Exception("Neither peer_list nor discovery_server have been set");
    }
}

Divide my configuration files into logical pieces

Use structs. Any field that is a struct is considered as a new section and will be recursed into:

struct Config
{
    GitConfig git;
    SSHConfig ssh = SSHConfig("/usr/bin/ssh");
}

struct GitConfig
{
    string path = "/usr/bin/git";
    @Optional string[] aliases;
}

struct SSHConfig
{
    string path;
    @Optional string[] default_switches;
    string user = "John Doe";
}

Sections are just regular fields, and all the approaches mentioned here work whether the field is a simple string or an object. For example, in the example above, both the git and ssh sections are optional, as GitConfig is purely optional, and the initializer to the ssh field provides a default value for the required ssh.path field.

Use a keyword, or a different name, in my config file

By default, this library will use the struct's field name for the key name in the YAML file. Sometimes it is not possible, e.g. when the desired name in the YAML file is a keyword in D. In this case, one can use the @Name(string) UDA:

struct Config
{
    @Name("delegate")
    string delegate_;
}

Have dynamic section names / Turn arrays into objects

With only static names YAML keys, some configurations can become a bit verbose. A common practice with YAML / JSON is to nest objects inside of objects, and use the name as a key, for example:

interfaces:
  eth0:
    ip: "192.168.0.1"
    private: true
  wlan0:
    ip: "1.2.3.4"

This can be achieved with this library without losing type safety, by recognizing that the above is syntax sugar for the following configuration:

  interfaces:
    - name: eth0
      ip: "192.168.0.1"
      private: true
    - name: wlan0
      ip: "1.2.3.4"

Using an array and the @Key(string) attribute, we can parse the first example:

struct Config
{
    @Key("name")
    InterfaceConfig interfaces;
}

struct InterfaceConfig
{
    string name;
    string ip;
    bool private;
}

Removing the @Key("name") attribute will instead parse the second example.

Read durations, such as delays or timeout

Just use core.time : Duration, it is natively supported:

import core.time;

struct Config
{
    Duration timeout = 10.seconds;
}

In the config file, any of the usual Duration units can be used, as if the Duration field was a section:

timeout:
  days:     1
  minutes: 10
  seconds: 30

The fields are additive, so the timeout in this case is 1 day, 10 minutes and 30 seconds, or 87030 seconds.

Implement complex types that are not composite types of simple types

The library recognizes three possible ways where a field of type T, where T is a struct, can be constructed:

  • The T has a static fromString method which accepts a single argument that is a string-like type (e.g. scope const char[] or just string), and returns an instance of a type that implicitly converts to T;
  • T has an explicitly-defined constructor that accepts, as a single parameter, a string-like type;
  • The field has a @Converter attribute;

If more than one option exists, the Converter will be preferred, followed by the fromString and finally the constructor. Otherwise, the library will default to field-wise construction.

The recommended way to implement a complex type is to use fromString. For example, the following parses a SysTime:

struct Config
{
    TimeConfig time;
}

struct TimeConfig
{
    import std.datetime;

    SysTime time;

    static TimeConfig fromString (string arg)
    {
        return TimeConfig(SysTime.fromSimpleString(arg));
    }
}

Which will parse the following YAML file:

time: '2010-Dec-22 17:22:01'

Implement validation for a field

The recommended way is to use a type that implement fromString, even if the type is natively supported:

import std.conv;

struct Config
{
    string name;
    Percentage percent;
}

struct Percentage
{
    ubyte value;

    static Percentage fromString (string arg)
    {
        auto v = arg.to!ubyte;
        if (v > 100)
          throw new Exception("Percentage cannot be over 100");
        return Percentage(v);
    }
}

The main benefit of using this method is that Exception will be caught by the config parser and re-thrown with field file / line information. So the above, when provided the following config file:

name: "Gary"
percentage: 142

Will result in the following error:

config.yaml(1:12): percentage: Percentage cannot be over 100

Note: There is currently an OB1 error in the line number (it should be (2:12)), this bug will need to be fixed in D-YAML.

Implement validation for a section

If validation of individual fields is not enough and the section as a whole needs to be validated, one can implement a void validate() const method which throws an exception in the event of a validation failure. The library will rethrow this Exception with the file/line information pointing to the section itself, and not any individual field.

Limit the set of acceptable values / Use an enum

Configy uses std.conv : to for converting enum, which means they always get converted to their symbolic name. The following two types will behave the same and expect values APAC, EMEA, and Americas in the YAML file.

enum Location : string {
  APAC     = "APAC",
  EMEA     = "EMEA",
  Americas = "North America",
}

enum Location2 {
  APAC,
  EMEA,
  Americas,
}

Symbolic names, unlike enum values, must be unique - Hence why they are not taken into account. To work around this, one may use configy.attributes : Only, which accepts a list of strings. Location would then be expressed as Only!(["APAC", "EMEA", "North America"]) instead. It is also trivial to implement such a type if a project has specific needs.

Implement really custom logic that Configy doesn't support

Use the fromYAML static method:

struct Service { string name; }
struct ServiceConfig {
    Only!(["service"]) type;
    string name;
}

struct Job { string position; }
struct JobConfig {
    Only!(["job"]) type;
    string worker;
}

// Top level configuration
struct Config {
    SumType!(ServiceConfig, JobConfig) conf;

    static Config fromYAML(scope ConfigParser!Config parser) {
        const typeN = "type" in parser.node;
        // This will point to the start of the file / section
        enforce(typeN, "Missing required 'type' property");
        const typeS = (*typeN).as!string;

        // This is the main bit: We still have distinct configurations depending on
        // the `type` discriminant, hence why we have declared them above.
        if (typeS == "job") {
            return Config(typeof(Config.conf)(parser.parseAs!JobConfig));
        } else if (typeS == "service") {
            return Config(typeof(Config.conf)(parser.parseAs!ServiceConfig));
        } else {
            throw new Exception(
                "'%s' is not a valid value for 'type', expected one of: 'service', 'job'"
                .format(typeS));
        }
    }
}

Example YAML that this will accept:

type: job
worker: k8s
type: service
name: dns

However, the following would fail and mention that name is not a valid member:

type: job
name: WRONG