# premortem

> Know how your app will die—before it does.

[![Crates.io](https://img.shields.io/crates/v/premortem.svg)](https://crates.io/crates/premortem)
[![Downloads](https://img.shields.io/crates/d/premortem)](https://crates.io/crates/premortem)
[![CI](https://github.com/iepathos/premortem/actions/workflows/ci.yml/badge.svg)](https://github.com/iepathos/premortem/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-MIT)](LICENSE)

A configuration library that performs a **premortem** on your app's config—finding all the ways it would die before it ever runs.

## Why "premortem"?

The name is a bit tongue-in-cheek—but only a bit. Configuration errors are one of the leading causes of production outages. Bad config doesn't just cause bugs; it causes *incidents*, *pages*, and *3am debugging sessions*.

A **postmortem** is what you do *after* something dies—gathering everyone to analyze what went wrong. Traditional config libraries give you the postmortem experience:

```
$ ./myapp
Error: missing field `database.host`

$ ./myapp  # fixed it, try again
Error: invalid port value

$ ./myapp  # fixed that too
Error: pool_size must be positive

# Three deaths to find three problems
```

**premortem** gives you all the fatal issues upfront:

```
$ ./myapp
Configuration errors (3):
  [config.toml:8] missing required field 'database.host'
  [env:APP_PORT] value "abc" is not a valid integer
  [config.toml:10] 'pool_size' value -5 must be >= 1
```

One run. All errors. Know how your app would die—before it does.

Try it yourself: `cargo run --example error-demo`

## Features

- **Accumulate all errors** — Never stop at the first problem
- **Trace value origins** — Know exactly which source provided each value
- **Multi-source loading** — Files, environment, CLI args, remote sources
- **Holistic validation** — Type, range, format, cross-field, and business rules
- **Derive macro** — Declarative validation with `#[derive(Validate)]`
- **Hot reload** — Watch for config changes (optional feature)

## Quick Start

```rust
use premortem::{Config, Toml, Env, Validate};
use serde::Deserialize;

#[derive(Debug, Deserialize, Validate)]
struct AppConfig {
    #[validate(non_empty)]
    pub host: String,

    #[validate(range(1..=65535))]
    pub port: u16,

    #[validate(range(1..=100))]
    pub pool_size: u32,
}

fn main() {
    let config = Config::<AppConfig>::builder()
        .source(Toml::file("config.toml"))
        .source(Env::prefix("APP_"))
        .build()
        .unwrap_or_else(|errors| {
            eprintln!("Configuration errors ({}):", errors.len());
            for e in &errors {
                eprintln!("  {}", e);
            }
            std::process::exit(1);
        });

    println!("Starting server on {}:{}", config.host, config.port);
}
```

## Installation

```toml
[dependencies]
premortem = "0.6"
```

With optional features:

```toml
[dependencies]
premortem = { version = "0.6", features = ["json", "watch"] }
```

## Feature Flags

| Feature | Description |
|---------|-------------|
| `toml` | TOML file support (default) |
| `json` | JSON file support |
| `yaml` | YAML file support |
| `watch` | Hot reload / file watching |
| `remote` | Remote sources (planned) |
| `full` | All features |

## Examples

See the [`examples/`](./examples/) directory for runnable examples:

| Example | Description |
|---------|-------------|
| [error-demo](./examples/error-demo/) | Error output with source location tracking |
| [env-validation](./examples/env-validation/) | Required environment variables with error accumulation |
| [layered-config](./examples/layered-config/) | Multi-source config with value tracing |
| [layered](./examples/layered/) | Environment-based layered configuration |
| [basic](./examples/basic/) | Minimal configuration loading |
| [validation](./examples/validation/) | Comprehensive validation patterns |
| [testing](./examples/testing/) | Configuration testing with MockEnv |
| [tracing](./examples/tracing/) | Value origin tracing demonstration |
| [watch](./examples/watch/) | Hot reload with automatic file watching |
| [web-server](./examples/web-server/) | Axum web server configuration |
| [yaml](./examples/yaml/) | YAML configuration file loading |

Run an example:

```bash
cargo run --example error-demo
cargo run --example layered-config
cargo run --example yaml --features yaml
```

## Documentation

- [Common Patterns](./docs/PATTERNS.md) — Layered config, secrets, nested structs
- [Testing Guide](./docs/TESTING.md) — Testing with MockEnv

## Core Concepts

### Source Layering

Sources are applied in order, with later sources overriding earlier ones:

```rust
let config = Config::<AppConfig>::builder()
    .source(Defaults::from(AppConfig::default()))  // Lowest priority
    .source(Toml::file("config.toml"))
    .source(Env::prefix("APP_"))                   // Highest priority
    .build()?;
```

### Required Environment Variables

Mark environment variables as required at the source level with error accumulation:

```rust
let config = Config::<AppConfig>::builder()
    .source(
        Env::prefix("APP_")
            .require_all(&["JWT_SECRET", "DATABASE_URL", "API_KEY"])
    )
    .build()?;
```

All missing required variables are reported together:

```
Configuration errors (3):
  [env:APP_JWT_SECRET] Missing required field: jwt.secret
  [env:APP_DATABASE_URL] Missing required field: database.url
  [env:APP_API_KEY] Missing required field: api.key
```

This separates **presence validation** (does the variable exist?) from **value validation** (does it meet constraints?).

### Testable I/O

All I/O is abstracted through `ConfigEnv`, enabling testing with `MockEnv`:

```rust
let env = MockEnv::new()
    .with_file("config.toml", "port = 8080")
    .with_env("APP_HOST", "localhost");

let config = Config::<AppConfig>::builder()
    .source(Toml::file("config.toml"))
    .source(Env::prefix("APP_"))
    .build_with_env(&env)?;
```

### Value Tracing

Debug where configuration values came from:

```rust
let traced = Config::<AppConfig>::builder()
    .source(Defaults::from(AppConfig::default()))
    .source(Toml::file("config.toml"))
    .source(Env::prefix("APP_"))
    .build_traced()?;

// Check what was overridden
for path in traced.overridden_paths() {
    let trace = traced.trace(path).unwrap();
    println!("{}: {:?} from {}", path, trace.final_value.value, trace.final_value.source);
}
```

## License

MIT Glen Baker <iepathos@gmail.com>
