tier 0.1.3

Rust configuration library for layered TOML, env, and CLI settings
docs.rs failed to build tier-0.1.3
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: tier-0.1.10

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

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

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:"));
# Ok::<(), tier::ConfigError>(())

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

# #[cfg(feature = "derive")] {
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.

# #[cfg(feature = "derive")] {
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.

# #[cfg(all(feature = "toml", feature = "watch"))] {
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:

# #[cfg(feature = "schema")] {
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:

# #[cfg(feature = "clap")] {
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));

# #[cfg(feature = "schema")] {
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:

# #[cfg(all(feature = "schema", feature = "derive", feature = "toml"))] {
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***");

Status

This crate focuses on typed layered loading, metadata, diagnostics, validation, schema/docs output, and reload support.

Deliberately out of scope in the current crate line:

  1. remote configuration backends
  2. derive-driven full CLI generation for application-specific flags