Skip to main content

Crate tier

Crate tier 

Source
Expand description

§tier

tier is a Rust configuration library for typed, layered application config.

It is designed for projects that want one serde config type fed by code defaults, TOML files, environment variables, and CLI overrides, without falling back to untyped value trees.

By default, tier only enables TOML file support. derive, clap, schema, watch, json, and yaml are opt-in features.

Use tier when you want:

  1. a Rust config library built around serde types
  2. predictable layered config from defaults, files, env, and CLI
  3. source tracing and validation instead of silent config drift
  4. optional schema, docs, and reload support without a heavy default feature set

§Feature Flags

  • toml: TOML file parsing and commented TOML examples
  • derive: #[derive(TierConfig)] metadata generation
  • clap: reusable config flags and diagnostics commands
  • schema: JSON Schema, env docs, and machine-readable reports
  • watch: native filesystem watcher backend
  • json: JSON file parsing
  • yaml: YAML file parsing

§Feature Map

  • Loading: ConfigLoader, FileSource, EnvSource, ArgsSource
  • Metadata: ConfigMetadata, FieldMetadata, TierConfig
  • Diagnostics: ConfigReport, doctor(), explain(), audit_report()
  • Schema and docs: json_schema_*, annotated_json_schema_*, config_example_*, EnvDocOptions
  • Reload: ReloadHandle, PollingWatcher, NativeWatcher

§Input Semantics

  • Env values and --set key=value overrides are string-first inputs
  • Primitive targets such as bool, integers, floats, and Option<T> are coerced during deserialization
  • Use explicit JSON syntax for arrays, objects, or quoted strings when you need structured inline values

§Checked Paths

For refactor-heavy code, tier exposes checked path macros that still feed the same runtime APIs:

use serde::{Deserialize, Serialize};
use tier::{ConfigMetadata, FieldMetadata};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
    proxy: ProxyConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProxyConfig {
    no_proxy: Vec<String>,
}

let metadata = ConfigMetadata::from_fields([
    FieldMetadata::new(tier::path!(AppConfig.proxy.no_proxy)),
]);

assert!(metadata.field("proxy.no_proxy").is_some());

§Typed Patches

If your application already has typed override structs, derive TierPatch and apply them directly instead of maintaining a serializable shadow layer.

use serde::{Deserialize, Serialize};
use tier::{ConfigLoader, Patch, TierPatch};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
    port: u16,
    token: Option<String>,
}

#[derive(Debug, Default, TierPatch)]
struct CliPatch {
    port: Option<u16>,
    #[tier(path_expr = tier::path!(AppConfig.token))]
    token: Patch<Option<String>>,
}

let loaded = ConfigLoader::new(AppConfig {
        port: 3000,
        token: Some("default".to_owned()),
    })
    .patch(
        "typed-cli",
        &CliPatch {
            port: Some(8080),
            token: Patch::set(None),
        },
    )?
    .load()?;

assert_eq!(loaded.port, 8080);
assert_eq!(loaded.token, None);

With both derive and clap, the same typed CLI struct can be applied as the last layer through ConfigLoader::clap_overrides(&parsed_cli), and path_expr = tier::path!(...) keeps CLI-to-config mappings checked during refactors.

§Structured Env Values

tier can decode common operational env formats without forcing applications to pre-normalize them into JSON.

use serde::{Deserialize, Serialize};
use tier::{ConfigLoader, EnvDecoder, EnvSource};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct AppConfig {
    no_proxy: Vec<String>,
}

let loaded = ConfigLoader::new(AppConfig { no_proxy: Vec::new() })
    .env_decoder("no_proxy", EnvDecoder::Csv)
    .env(EnvSource::from_pairs([(
        "APP__NO_PROXY",
        "localhost,127.0.0.1,.internal.example.com",
    )]).prefix("APP"))
    .load()?;

assert_eq!(
    loaded.no_proxy,
    vec![
        "localhost".to_owned(),
        "127.0.0.1".to_owned(),
        ".internal.example.com".to_owned(),
    ]
);

You can also bridge standard operational env names directly:

use serde::{Deserialize, Serialize};
use tier::{ConfigLoader, EnvDecoder, EnvSource};

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct AppConfig {
    proxy: ProxyConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct ProxyConfig {
    url: Option<String>,
    no_proxy: Vec<String>,
}

let loaded = ConfigLoader::new(AppConfig::default())
    .env(
        EnvSource::from_pairs([
            ("HTTP_PROXY", "http://proxy.internal:8080"),
            ("NO_PROXY", "localhost,.svc.internal"),
        ])
        .with_fallback("HTTP_PROXY", "proxy.url")
        .with_fallback_decoder("NO_PROXY", "proxy.no_proxy", EnvDecoder::Csv),
    )
    .load()?;

assert_eq!(loaded.proxy.url.as_deref(), Some("http://proxy.internal:8080"));
assert_eq!(loaded.proxy.no_proxy, vec!["localhost", ".svc.internal"]);

For formats that are not covered by the built-in EnvDecoder variants, use ConfigLoader::env_decoder_with(...) to keep application-specific parsing in the config layer instead of pre-normalizing env vars outside tier.

§Leaf Enums

Leaf enum fields can opt out of nested metadata requirements with #[tier(leaf)] on the containing field:

use serde::{Deserialize, Serialize};
use tier::TierConfig;

#[derive(Debug, Clone, Serialize, Deserialize)]
enum Backend {
    Memory,
    Redis,
}

#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct AppConfig {
    #[tier(env = "APP_BACKEND", leaf)]
    backend: Backend,
}

§Quick Start

The smallest useful setup is defaults plus a TOML file:

use serde::{Deserialize, Serialize};
use tier::ConfigLoader;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
    port: u16,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self { port: 3000 }
    }
}

let loaded = ConfigLoader::new(AppConfig::default())
    .file("config/app.toml")
    .load()?;

assert!(loaded.report().doctor().contains("Sources:"));

§Examples

The crate ships with focused examples under examples/:

  • basic.rs: defaults + env + CLI layering
  • manual-metadata.rs: explicit ConfigMetadata
  • derive.rs: derive metadata and declarative validation
  • schema.rs: schema, env docs, and commented TOML examples
  • clap.rs: embedding TierCli
  • reload.rs: polling reload
  • application.rs: a fuller application setup

§Core Types

  • ConfigLoader<T> builds a deterministic pipeline from defaults, files, env, CLI, and custom layers.
  • ConfigMetadata carries env names, aliases, secrets, examples, merge rules, and declared validations.
  • LoadedConfig<T> returns the final typed value with a ConfigReport.
  • ReloadHandle<T> reuses the same loader closure for polling or native file watching.

§Highlights

  • Typed loading with deterministic merge order and unknown field governance
  • Metadata-driven env mapping, secret handling, and validation
  • Field-level tracing, doctor output, and machine-readable audit/report data
  • Optional schema/docs export, commented TOML examples, clap, and reload

§Example

use serde::{Deserialize, Serialize};
use tier::{ArgsSource, ConfigLoader, EnvSource, Secret, TierConfig, ValidationErrors};

#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct AppConfig {
    server: ServerConfig,
    db: DbConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct ServerConfig {
    #[tier(
        env = "APP_SERVER_HOST",
        doc = "IP address or hostname to bind",
        example = "0.0.0.0"
    )]
    host: String,
    #[tier(deprecated = "use server.bind_port instead")]
    port: u16,
}

#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct DbConfig {
    password: Secret<String>,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            server: ServerConfig {
                host: "127.0.0.1".into(),
                port: 3000,
            },
            db: DbConfig {
                password: Secret::new("secret".into()),
            },
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let loaded = ConfigLoader::new(AppConfig::default())
        .derive_metadata()
        .file("config/default.toml")
        .optional_file("config/{profile}.toml")
        .env(EnvSource::prefixed("APP"))
        .args(ArgsSource::from_env())
        .profile("prod")
        .validator("port-range", |config| {
            if config.server.port == 0 {
                return Err(ValidationErrors::from_message(
                    "server.port",
                    "port must be greater than zero",
                ));
            }
            Ok(())
        })
        .load()?;

    println!("{}", loaded.report().doctor());
    Ok(())
}

derive_metadata() applies metadata generated by TierConfig, including env names, aliases, secret handling, serde(default) awareness, merge strategies, declared validation rules, env docs, and deprecation warnings.

§Declarative Validation

tier supports metadata-driven field and cross-field validation alongside custom validator hooks. Declared rules feed the loader, schema annotations, env docs, and commented TOML examples from the same metadata source.

use serde::{Deserialize, Serialize};
use tier::{ConfigError, ConfigLoader, TierConfig};

#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct AppConfig {
    #[tier(non_empty, min_length = 3)]
    service_name: String,
    #[tier(min = 1, max = 65535)]
    port: u16,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            service_name: "api".to_owned(),
            port: 8080,
        }
    }
}

let error = ConfigLoader::new(AppConfig::default())
    .derive_metadata()
    .args(tier::ArgsSource::from_args([
        "app",
        "--set",
        r#"service_name="""#,
        "--set",
        "port=0",
    ]))
    .load()
    .expect_err("declared validation must fail");

assert!(matches!(error, ConfigError::DeclaredValidation { .. }));

§Reload

tier always includes ReloadHandle and a polling watcher. With watch enabled, it also exposes a native filesystem watcher. Reloads can emit structured diffs and events, and watchers can either keep running or stop after a failed reload.

use std::time::Duration;
use serde::{Deserialize, Serialize};
use tier::{ConfigError, ConfigLoader, ReloadEvent, ReloadHandle};

#[derive(Clone, Serialize, Deserialize)]
struct AppConfig {
    port: u16,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self { port: 3000 }
    }
}

fn main() -> Result<(), ConfigError> {
    let handle =
        ReloadHandle::new(|| ConfigLoader::new(AppConfig::default()).file("app.toml").load())?;
    let _events = handle.subscribe();
    let _summary = handle.reload_detailed()?;
    let watcher = handle.start_native(["app.toml"], Duration::from_millis(100))?;
    watcher.stop();
    Ok(())
}

§Schema Export

With schema enabled, tier can export a JSON Schema for a config type:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tier::json_schema_pretty;

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct AppConfig {
    port: u16,
}

let schema = json_schema_pretty::<AppConfig>();
assert!(schema.contains("\"type\": \"object\""));

§Clap Integration

With clap enabled, tier provides a reusable config flag group:

use clap::Parser;
use tier::TierCli;

#[derive(Debug, Parser)]
struct AppCli {
    #[command(flatten)]
    config: TierCli,
}

let cli = AppCli::parse_from(["app", "--validate-config"]);
assert!(matches!(cli.config.command(), tier::TierCliCommand::ValidateConfig));

let example = AppCli::parse_from(["app", "--print-config-example"]);
assert!(matches!(
    example.config.command(),
    tier::TierCliCommand::PrintConfigExample
));

§Environment Variable Docs

With schema enabled, tier can generate environment variable docs and annotated JSON Schema. When toml is also enabled, it can render a commented TOML example configuration:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tier::{
    EnvDocOptions, TierConfig, annotated_json_schema_pretty, config_example_toml, env_docs_json,
    env_docs_markdown,
};

#[derive(Debug, Serialize, Deserialize, JsonSchema, TierConfig)]
struct AppConfig {
    server: ServerConfig,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema, TierConfig)]
struct ServerConfig {
    #[tier(
        env = "APP_SERVER_PORT",
        doc = "Port used for incoming traffic",
        example = "8080"
    )]
    port: u16,
}

let docs = env_docs_markdown::<AppConfig>(&EnvDocOptions::prefixed("APP"));
assert!(docs.contains("APP_SERVER_PORT"));

let docs_json = env_docs_json::<AppConfig>(&EnvDocOptions::prefixed("APP"));
assert!(docs_json.is_array());

let schema = annotated_json_schema_pretty::<AppConfig>();
assert!(schema.contains("\"x-tier-env\""));

let example = config_example_toml::<AppConfig>();
assert!(example.contains("[server]"));

§Secrets

tier::Secret<T> is a strong typed wrapper for sensitive values. It redacts Debug and Display output, and with the schema feature it marks fields as writeOnly so the loader can auto-discover secret paths.

use serde::{Deserialize, Serialize};
use tier::Secret;

#[derive(Debug, Serialize, Deserialize)]
struct DbConfig {
    password: Secret<String>,
}

let password = Secret::new("super-secret".to_owned());
assert_eq!(format!("{password}"), "***redacted***");

Macros§

path
Builds a compile-time checked dot path from a config type.
path_pattern
Builds a compile-time checked wildcard path from a config type.

Structs§

ArgsSource
CLI override source definition.
AuditReport
Machine-readable audit payload including traces for every resolved path.
ConfigChange
A single redacted configuration change observed during reload.
ConfigLoader
Builder for layered configuration loading.
ConfigMetadata
Structured metadata describing configuration fields.
ConfigReport
Post-load diagnostics including source traces, warnings, and redacted output helpers.
DeprecatedField
Information about a deprecated configuration path used during loading.
DoctorReport
Machine-readable summary of a loaded configuration report.
EnvDocEntryschema
A single schema-derived environment variable documentation row.
EnvDocOptionsschema
Options controlling schema-derived environment variable documentation.
EnvDocsReportschema
Versioned machine-readable environment documentation payload.
EnvSource
Environment variable source definition.
Explanation
Full resolution trace for a single configuration path.
FieldMetadata
Metadata for a single configuration path.
FileSource
File-backed configuration source definition.
Layer
Custom serializable configuration layer.
LoadedConfig
Loaded configuration plus its diagnostic report.
NativeWatcherwatch
Handle for a background native filesystem watcher.
PollingWatcher
Handle for a background polling watcher.
ReloadFailure
Structured details about a rejected reload attempt.
ReloadHandle
Thread-safe holder for the active configuration and reload logic.
ReloadOptions
Options controlling watcher-side reload behavior.
ReloadSummary
Structured summary of a successful reload attempt.
ReportSummary
Aggregate counts for a machine-readable configuration report.
ResolutionStep
One source contribution recorded for a configuration path.
Secret
Strongly typed secret wrapper.
SourceTrace
Human-readable description of where a configuration value came from.
TierCliclap
Reusable clap flag group for tier configuration loading and diagnostics.
TraceAudit
Structured audit details for a single resolved path.
UnknownField
Information about an unknown configuration path discovered during loading.
ValidationError
A single validation failure returned by a validator hook.
ValidationErrors
A collection of validation failures returned by a validator hook.
ValidationValue
Scalar value used by declarative validation rules and conditions.

Enums§

ConfigError
Errors returned while building, loading, validating, or inspecting configuration.
ConfigWarning
Non-fatal issues surfaced while loading configuration.
EnvDecoder
Built-in decoders for structured environment variable values.
FileFormat
Supported on-disk configuration file formats.
MergeStrategy
Strategy applied when multiple layers write to the same configuration path.
Patch
Sparse patch field wrapper used by typed override structs.
ReloadEvent
Structured event emitted after each successful or rejected reload attempt.
ReloadFailurePolicy
Policy applied when a background watcher encounters a reload failure.
SourceKind
Kind of source that contributed configuration values.
TierCliCommandclap
High-level action requested by TierCli.
UnknownFieldPolicy
Policy applied when unknown configuration paths are discovered.
ValidationCheck
Cross-field declarative validation applied to the final normalized configuration.
ValidationNumber
Numeric bound used by declarative validation rules.
ValidationRule
Declarative validation rule applied to a single configuration path.

Constants§

ENV_DOCS_FORMAT_VERSIONschema
Stable version tag for machine-readable environment documentation payloads.
REPORT_FORMAT_VERSION
Stable version tag for machine-readable doctor and audit reports.

Traits§

JsonSchemaschema
Re-export of schemars::JsonSchema used by tier schema helpers. A type which can be described as a JSON Schema document.
TierMetadata
Metadata produced for a configuration type.
TierPatch
Trait implemented by typed sparse override structures.

Functions§

annotated_json_schema_forschema
Exports the JSON Schema for a configuration type annotated with tier metadata.
annotated_json_schema_prettyschema
Exports the annotated JSON Schema for a configuration type as pretty JSON.
config_example_forschema
Generates a machine-readable example configuration value from schema and metadata.
config_example_prettyschema
Renders the generated example configuration as pretty JSON.
config_example_tomlschema and toml
Renders the generated example configuration as commented TOML.
env_docs_forschema
Generates environment variable documentation rows from a configuration schema.
env_docs_jsonschema
Renders schema-derived environment variable documentation as machine-readable JSON.
env_docs_json_prettyschema
Renders machine-readable environment variable documentation as pretty JSON.
env_docs_markdownschema
Renders schema-derived environment variable documentation as Markdown.
env_docs_reportschema
Renders versioned machine-readable environment variable documentation.
env_docs_report_jsonschema
Renders versioned environment variable documentation as JSON.
env_docs_report_json_prettyschema
Renders versioned environment variable documentation as pretty JSON.
json_schema_forschema
Exports the JSON Schema for a configuration type.
json_schema_prettyschema
Exports the JSON Schema for a configuration type as pretty JSON.

Derive Macros§

JsonSchemaschema
Re-export of schemars::JsonSchema used by tier schema helpers. Derive macro for JsonSchema trait.
TierConfigderive
Derives tier::TierMetadata for nested configuration structs.
TierPatchderive
Derives tier::TierPatch for typed sparse override structs.