[][src]Crate figment

Semi-hierarchical configuration so con-free, it's unreal.

use serde::Deserialize;
use figment::{Figment, providers::{Format, Toml, Json, Env}};

#[derive(Deserialize)]
struct Package {
    name: String,
    description: Option<String>,
    authors: Vec<String>,
    publish: Option<bool>,
    // ... and so on ...
}

#[derive(Deserialize)]
struct Config {
    package: Package,
    rustc: Option<String>,
    rustdoc: Option<String>,
    // ... and so on ...
}

let config: Config = Figment::new()
    .merge(Toml::file("Cargo.toml"))
    .merge(Env::prefixed("CARGO_"))
    .merge(Env::raw().only(&["RUSTC", "RUSTDOC"]))
    .join(Json::file("Cargo.json"))
    .extract()?;

Table of Contents

Overview

Figment is a library for declaring and combining configuration sources and extracting typed values from the combined sources. There are two prevailing concepts:

  • Providers: Types implementing the Provider trait, which implement a configuration source.
  • Figments: The Figment type, which combines providers via merge or join and allows typed extraction. Figments are also providers themselves.

Defining a configuration consists of constructing a Figment and merging or joining any number of Providers. Values for duplicate keys from a merged provider replace those from previous providers, while no replacement occurs for joined providers. Sources are read eagerly, immediately upon merging and joining.

The simplest useful figment has one provider. The figment below will use all environment variables prefixed with MY_APP_ as configuration values, after removing the prefix:

use figment::{Figment, providers::Env};

let figment = Figment::from(Env::prefixed("MY_APP_"));

Most figments will use more than one provider, merging and joining as necessary. The figment below reads App.toml, environment variables prefixed with APP_ and fills any holes (but does replace existing values) with values from App.json:

use figment::{Figment, providers::{Format, Toml, Json, Env}};

let figment = Figment::new()
    .merge(Toml::file("App.toml"))
    .merge(Env::prefixed("APP_"))
    .join(Json::file("App.json"));

Values can be extracted into any value that implements Deserialize. The Jail type allows for semi-sandboxed configuration testing. The example below showcases extraction and testing:

use serde::Deserialize;
use figment::{Figment, providers::{Format, Toml, Json, Env}};

#[derive(Debug, PartialEq, Deserialize)]
struct AppConfig {
    name: String,
    count: usize,
    authors: Vec<String>,
}

figment::Jail::expect_with(|jail| {
    jail.create_file("App.toml", r#"
        name = "Just a TOML App!"
        count = 100
    "#)?;

    jail.create_file("App.json", r#"
        {
            "name": "Just a JSON App",
            "authors": ["figment", "developers"]
        }
    "#)?;

    jail.set_env("APP_COUNT", 250);

    // Sources are read _eagerly_: sources are read as soon as they are
    // merged/joined into a figment.
    let figment = Figment::new()
        .merge(Toml::file("App.toml"))
        .merge(Env::prefixed("APP_"))
        .join(Json::file("App.json"));

    let config: AppConfig = figment.extract()?;
    assert_eq!(config, AppConfig {
        name: "Just a TOML App!".into(),
        count: 250,
        authors: vec!["figment".into(), "developers".into()],
    });

    Ok(())
});

Metadata

Figment takes great care to propagate as much information as possible about configuration sources. All values extracted from a figment are tagged with an id for Metadata. The tag is preserved across merges, joins, and errors, which also include the path of the offending key. Precise tracking allows for rich error messages as well as "magic" values like RelativePathBuf, which automatically creates a path relative to the configuration file in which it was declared.

A Metadata consists of:

  • The name of the configuration source.
  • An "interpolater" that takes a path to a key and converts it into a provider-native key.
  • a Source specifying where the value was sourced from.

Along with the information in an Error, this means figment can produce rich error values and messages:

error: invalid type: found string "hi", expected u16
 --> key `debug.port` in TOML file App.toml

Extracting and Profiles

Providers always produce Dicts nested in Profiles. A profile is selected when extracting, and the dictionary corresponding to that profile is deserialized into the requested type. If no profile is selected, the Default profile is used.

There are two built-in profiles: the aforementioned default profile and the Global profile. As the name implies, the default profile contains default values for all profiles. The global profile also contains values that correspond to all profiles, but those values supersede values of any other profile except the global profile, even when another source is merged.

Some providers can be configured as nested, which allows top-level keys in dictionaries produced by the source to be treated as profiles. The following example showcases profiles and nesting:

use serde::Deserialize;
use figment::{Figment, providers::{Format, Toml, Json, Env}};

#[derive(Debug, PartialEq, Deserialize)]
struct Config {
    name: String,
}

impl Config {
    // Note the `nested` option on both `file` providers. This makes each
    // top-level dictionary act as a profile.
    fn figment() -> Figment {
        Figment::new()
            .merge(Toml::file("Base.toml").nested())
            .merge(Toml::file("App.toml").nested())
    }
}

figment::Jail::expect_with(|jail| {
    jail.create_file("Base.toml", r#"
        [default]
        name = "Base-Default"

        [debug]
        name = "Base-Debug"
    "#)?;

    // The default profile is used...by default.
    let config: Config = Config::figment().extract()?;
    assert_eq!(config, Config { name: "Base-Default".into(), });

    // A different profile can be selected with `select`.
    let config: Config = Config::figment().select("debug").extract()?;
    assert_eq!(config, Config { name: "Base-Debug".into(), });

    // Selecting non-existent profiles is okay as long as we have defaults.
    let config: Config = Config::figment().select("undefined").extract()?;
    assert_eq!(config, Config { name: "Base-Default".into(), });

    // Replace the previous `Base.toml`. This one has a `global` profile.
    jail.create_file("Base.toml", r#"
        [default]
        name = "Base-Default"

        [debug]
        name = "Base-Debug"

        [global]
        name = "Base-Global"
    "#)?;

    // Global values override all profile values.
    let config_def: Config = Config::figment().extract()?;
    let config_deb: Config = Config::figment().select("debug").extract()?;
    assert_eq!(config_def, Config { name: "Base-Global".into(), });
    assert_eq!(config_deb, Config { name: "Base-Global".into(), });

    // Merges from succeeding provides take precedence, even for globals.
    jail.create_file("App.toml", r#"
        [debug]
        name = "App-Debug"

        [global]
        name = "App-Global"
    "#)?;

    let config_def: Config = Config::figment().extract()?;
    let config_deb: Config = Config::figment().select("debug").extract()?;
    assert_eq!(config_def, Config { name: "App-Global".into(), });
    assert_eq!(config_deb, Config { name: "App-Global".into(), });

    Ok(())
});

Crate Feature Flags

To help with compilation times, types, modules, and providers are gated by features. They are:

featuregated namespacedescription
testJailSemi-sandboxed environment for testing.
magicvalue::magic"Magic" deserializable values.
envproviders::EnvEnvironment variable Provider.
tomlproviders::TomlTOML file/string Provider.
jsonproviders::JsonJSON file/string Provider.
yamlproviders::YamlYAML file/string Provider.

Built-In Providers

In addition to the four gated providers, figment provides the following providers out-of-the-box:

providerdescription
providers::SerializedSource from any Serialize type.
(impl AsRef<str>, impl Serialize)Source from a tuple ("key", value).
&T where T: ProviderA reference to any provider.

For Provider Authors

The Provider trait documentation details extensively how to implement a provider for Figment. For data format based providers, the Format trait allows for even simpler implementations.

For Library Authors

For libraries and frameworks that wish to expose customizable configuration, we encourage the following structure:

use serde::{Serialize, Deserialize};

use figment::{Figment, Provider, Error, Metadata, Profile};

// The library's required configuration.
#[derive(Debug, Deserialize, Serialize)]
struct Config { /* the library's required/expected values */ }

// The default configuration.
impl Default for Config {
    fn default() -> Self {
        Config { /* default values */ }
    }
}

impl Config {
    // Allow the configuration to be extracted from any `Provider`.
    fn from<T: Provider>(provider: T) -> Result<Config, Error> {
        Figment::from(provider).extract()
    }

    // Provide a default provider, a `Figment`.
    fn figment() -> Figment {
        use figment::providers::Env;

        // In reality, whatever the library desires.
        Figment::from(Config::default()).merge(Env::prefixed("APP_"))
    }
}

use figment::value::{Map, Dict};

// Make `Config` a provider itself for composability.
impl Provider for Config {
    fn metadata(&self) -> Metadata {
        Metadata::named("Library Config")
    }

    fn data(&self) -> Result<Map<Profile, Dict>, Error>  {
        figment::providers::Serialized::defaults(Config::default()).data()
    }

    fn profile(&self) -> Option<Profile> {
        // Optionally, a profile that's selected by default.
    }
}

This structure has the following properties:

  • The library provides a Config structure that clearly indicates which values the library requires.
  • Users can completely customize configuration via their own Provider.
  • The library's Config is itself a Provider for composability.
  • The library provides a Figment which it will use as the default configuration provider.

Config::from(Config::figment()) can be used as the library default while allowing complete customization of the configuration sources. Developers building on the library can base their figments on Config::default(), Config::figment(), both or neither.

For frameworks, a top-level structure should expose the Figment that was used to extract the Config, allowing other libraries making use of the framework to also extract values from the same Figment:

use figment::{Figment, Provider, Error};

struct App {
    /// The configuration.
    pub config: Config,
    /// The figment used to extract the configuration.
    pub figment: Figment,
}

impl App {
    pub fn new() -> Result<App, Error> {
        App::custom(Config::figment())
    }

    pub fn custom<T: Provider>(provider: T) -> Result<App, Error> {
        let figment = Figment::from(provider);
        Ok(App { config: Config::from(&figment)?, figment })
    }
}

For Application Authors

As an application author, you'll need to make at least the following decisions:

  1. The sources you'll accept configuration from.
  2. The precedence you'll apply to each source.
  3. Whether you'll use profiles or not.

For special sources, you may find yourself needing to implement a custom Provider. As with libraries, you'll likely want to provide default values where possible either by providing it to the figment or by using serde's defaults. Then, it's simply a matter of declaring a figment and extracting the configuration from it.

A reasonable starting point might be:

use serde::{Serialize, Deserialize};
use figment::{Figment, providers::{Env, Format, Toml, Serialized}};

#[derive(Deserialize, Serialize)]
struct Config {
    key: String,
    another: u32
}

impl Default for Config {
    fn default() -> Config {
        Config {
            key: "default".into(),
            another: 100,
        }
    }
}

Figment::from(Serialized::defaults(Config::default()))
    .merge(Toml::file("App.toml"))
    .merge(Env::prefixed("APP_"));

Tips

Some things to remember when working with Figment:

  • Merging and joining is eager: sources are read immediately. It's useful to define a function that returns a Figment.
  • The util modules contains helpful serialize and deserialize implementations for defining Config structures.
  • The Format trait makes implementing data-format based Providers straight-forward.
  • Magic values can significantly reduce the need to inspect a Figment directly.
  • Jail makes testing configurations straight-forward and much less error-prone.
  • Error may contain more than one error: iterate over it to retrieve all errors.

Modules

error

Error values produces when extracting configurations.

providers

Built-in Provider implementations for common sources.

util

Useful functions and macros for writing figments.

value

Value and friends: types representing valid configuration values.

Structs

Error

An error that occured while producing data or extracting a configuration.

Figment

Combiner of Providers for configuration value extraction.

Jailtest

A "sandboxed" environment with isolated env and file system namespace.

Metadata

Metadata about a configuration value: its source's name and location.

Profile

A configuration profile: effectively a case-insensitive string.

Enums

Source

The source for a configuration value.

Traits

Provider

Trait implemented by configuration source providers.