tier 0.1.4

Rust configuration library for layered TOML, env, and CLI settings
# 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

## Quick Start

The smallest useful setup is defaults plus a TOML file:

```rust,no_run
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/`](./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

```rust,no_run
# #[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.

```rust
# #[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.

```rust,no_run
# #[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:

```rust
# #[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:

```rust
# #[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:

```rust
# #[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.

```rust
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