Confique

repository·main·Indexed 42 days ago

https://github.com/lukaskalbertodt/confique

A lightweight, type-safe, and layered configuration management library for Rust based on serde. It allows developers to define configuration schemas using the `Config` derive macro, load values from multiple sources (environment variables, TOML, YAML, JSON5), and automatically generate configuration templates. Version 0.4.0.

Tokens
6.8K
Snippets
24
Records
30
Agent score
50%

What's inside Confique

  1. Generate configuration templates

    main

    Confique can automatically generate configuration templates in various formats. These templates include field descriptions (from doc comments), default values, and environment variable information. This allows you to provide users with a starting configuration file without duplicating documentation.

    Templates can be generated using the following pattern for supported formats:

    • TOML: toml::template::<Conf>()
    • YAML: yaml::template::<Conf>()
    • JSON5: json5::template::<Conf>()
    // Example of what a generated TOML template might look like:
    // # Port to listen on.
    // #
    // # Can also be specified via
    // # environment variable `PORT`.
    // #
    // # Default value: 8080
    // #port = 8080
  2. Load layered configuration using the Builder pattern

    main

    To load your configuration, use the .builder() method provided by your Config struct. You can layer multiple sources, and later sources will override earlier ones. Supported sources include:

    • .env(): Loads values from environment variables.
    • .file("path"): Loads values from a file (supports TOML, YAML, and JSON5).

    Call .load()? at the end of the builder chain to attempt to construct the configuration object.

    let config = Conf::builder()
        .env()
        .file("example-app.toml")
        .file("/etc/example-app/config.toml")
        .load()?;
  3. Understand the Meta and Field hierarchy

    main

    Confique uses a recursive metadata model to describe configuration structures:

    • Meta: The root description of a configuration struct. Contains name, doc (comments), and fields.
    • Field: Describes a single member of a struct. Contains name, doc, and a kind.
    • FieldKind: Defines if a field is a Leaf (a terminal value) or Nested (another struct described by its own Meta).
    • LeafKind: For leaf fields, distinguishes between Required (must be provided or has a default Expr) and Optional (an Option<_> type).
  4. Define configuration with the `Config` derive macro

    main

    To use Confique, define your configuration as one or more structs and annotate them with #[derive(Config)].

    Field Types

    • Leaf fields: These contain actual values and must implement serde::Deserialize.
    • Nested fields: Use #[config(nested)] to include other types that also implement Config. Note that Option<_> is not allowed for nested fields.
    • Optional fields: Use Option<T> for fields that are not required. Non-optional fields must be provided during loading or they will cause an error.

    Field Attributes

    • #[config(default = <expr>)]: Sets a default value (e.g., 8080, "fox", [1, 2]).
    • #[config(env = "KEY")]: Maps the field to an environment variable.
    • #[config(validate = <path>)]: Adds a validation check using a function Fn(&T) -> Result<(), E>.
    • #[config(validate(<expr>, "msg"))]: A shorthand for simple boolean expressions.
    • #[config(deserialize_with = <path>)]: Similar to Serde's deserialize_with attribute.
    • #[config(parse_env = <path>)]: A custom function to parse environment variables (useful for complex types like lists).
    use confique::Config;
    
    #[derive(Config)]
    struct Conf {
        // A required value
        username: String,
    
        // An optional value
        welcome_message: Option<String>,
    
        // A required value with a default
        #[config(default = 8080)]
        port: u16,
    
        #[config(nested)]
        log: LogConf,
    }
    
    #[derive(Config)]
    struct LogConf {
        #[config(default = true)]
        stdout: bool,
        file: Option<std::path::PathBuf>,
    }
  5. Manually assemble configuration layers

    main

    For advanced control, you can manually assemble Layer types. A Layer is a version of your configuration where every field is optional (represented by Option<T>).

    Steps to manual assembly:

    1. Create or load individual layers (e.g., using File::new(...).load(), Layer::from_env(), or manual struct initialization).
    2. Combine layers using .with_fallback(fallback_layer) (where self has higher priority).
    3. Use Layer::default_values() as the final fallback.
    4. Convert the merged layer into the final config type using Config::from_layer(merged_layer).
    use confique::{Config, File, FileFormat, Layer};
    
    #[derive(Config)]
    struct Conf {
        foo: f32,
    }
    
    type ConfLayer = <Conf as Config>::Layer;
    
    // 1. Load from file
    let from_file: ConfLayer = File::with_format("/etc/foo/config", FileFormat::Toml)
        .required()
        .load()?;
    
    // 2. Create a manual layer
    let manual = ConfLayer {
        foo: Some(3.14),
    };
    
    // 3. Get defaults
    let defaults = ConfLayer::default_values();
    
    // 4. Merge and convert
    let merged = from_file.with_fallback(manual).with_fallback(defaults);
    let config = Conf::from_layer(merged)?;
  6. Configure supported file formats via Cargo features

    main

    Confique supports multiple file formats, but they are not enabled by default. You must enable the corresponding feature in your Cargo.toml to use them:

    • toml: Enables TOML support.
    • yaml: Enables YAML support.
    • json5: Enables JSON5 support.

    Example Cargo.toml entry:

    confique = { version = "0.4.0", features = ["toml"] }
  7. Define a configuration schema with the `Config` derive macro

    main

    Use the #[derive(Config)] macro on a struct to define your configuration schema. You can use the #[config(...)] attribute to specify how individual fields are loaded and validated:

    • env = "VAR_NAME": Maps the field to an environment variable.
    • default = <value>: Sets a default value if the field is not provided in any layer.
    • nested: Indicates that the field is another struct implementing Config.
    • validate(expression, "error message"): Adds a validation check. The expression must return a boolean.

    This approach ensures type safety, as the resulting configuration object contains fully parsed and validated values.

    use std::{net::IpAddr, path::PathBuf};
    use confique::Config;
    
    #[derive(Config)]
    struct Conf {
        /// Port to listen on.
        #[config(env = "PORT", default = 8080)]
        port: u16,
    
        /// Bind address.
        #[config(default = "127.0.0.1")]
        address: IpAddr,
    
        #[config(nested)]
        log: LogConf,
    }
    
    #[derive(Config)]
    struct LogConf {
        #[config(default = true)]
        stdout: bool,
    
        #[config(validate(file.is_absolute(), "log file requires absolute path"))]
        file: Option<PathBuf>,
    
        #[config(default = ["debug"])]
        ignored_modules: Vec<String>,
    }
  8. Configure configuration template formatting with FormatOptions

    main

    When generating a configuration template (a description of all possible configuration values and their defaults), you can control the output appearance using FormatOptions. This struct allows you to toggle documentation comments and adjust the spacing between fields.

    Available Options

    FieldTypeDescriptionDefault
    commentsboolWhether to include doc comments and information about required/default values.true
    env_keysboolIf true (and comments is true), fields with an env attribute will include a comment stating they can be specified via an environment variable.true
    leaf_field_gapOption<u8>Number of lines between leaf fields. If None, it defaults to 1 if comments is true, or 0 otherwise.None
    nested_field_gapu8Number of lines between nested fields.1
    let options = FormatOptions {
        comments: true,
        env_keys: true,
        leaf_field_gap: Some(2),
        nested_field_gap: 2,
    };
  9. Configure TOML template generation options

    main

    The FormatOptions struct allows you to control how the generated TOML template is formatted.

    Key fields include:

    • indent: A u8 value specifying the number of spaces used for indentation of nested tables. The default is 0.
    • general: A template::FormatOptions object containing non-TOML-specific formatting settings (such as comment toggling or gap spacing).
    use confique::toml::FormatOptions;
    
    let options = FormatOptions {
        indent: 4, // Use 4 spaces for nested tables
        general: Default::default(),
    };
  10. Configure JSON5 template generation options

    main

    The FormatOptions struct allows you to control the appearance of the generated JSON5 template.

    Fields

    • indent: u8 - The number of spaces used for indentation per level. Defaults to 2.
    • general: template::FormatOptions - Inherited options for general template formatting (e.g., controlling whether comments are included).

    To use the default settings, use FormatOptions::default().

    use confique::json5::FormatOptions;
    
    let options = FormatOptions {
        indent: 4,
        general: Default::default(),
    };
  11. Configure YAML template generation options

    main

    The FormatOptions struct allows you to customize how the YAML template is generated.

    Key options include:

    • indent: The number of spaces used for indentation. Defaults to 2.
    • general: A template::FormatOptions object used for non-YAML-specific formatting (e.g., controlling whether comments are included).
    use confique::yaml::FormatOptions;
    
    let options = FormatOptions {
        indent: 4,
        general: Default::default(), // Use default general options
    };
    
    // To disable comments (if supported by the general options):
    // let mut options = FormatOptions::default();
    // options.general.comments = false;
  12. Handling Confique errors

    main

    The Error type describes all errors that can occur when loading or parsing configuration in confique.

    Important Note on Error Display: By default, the Display and Debug implementations of Error do not include information from the underlying cause (Error::source). To see the full error chain, you should:

    1. Use a crate like anyhow or eyre to traverse the source-chain.
    2. Or, use the "alternate" flag # when printing (e.g., println!("{:#}", err)). Note that the alternate flag only prints the direct source; a full traversal is still preferred for complete debugging information.