# Premortem Project Documentation

This document contains Premortem-specific documentation for Claude. General development guidelines are in `~/.claude/CLAUDE.md`.

## Overview

Premortem is a configuration library that performs a premortem on your app's config—finding all the ways it could die from bad config before it ever runs. It uses stillwater's functional patterns for error accumulation and composable validation.

## Error Handling

### Core Rules
- **Production code**: Never use `unwrap()` or `panic!()` - use Result types and `?` operator
- **Test code**: May use `unwrap()` and `panic!()` for test failures
- **Static patterns**: Compile-time constants (like regex) may use `expect()`

### Error Types
- Configuration: `ConfigError`, `ConfigErrors` (NonEmptyVec-based)
- Validation: `ConfigValidation<T>` (alias for `Validation<T, ConfigErrors>`)
- Source errors: `SourceErrorKind`

### Error Accumulation
Premortem uses stillwater's `Validation` type to collect ALL configuration errors, not just the first:

```rust
use premortem::{ConfigError, ConfigErrors, ConfigValidation};
use stillwater::Validation;

fn validate_config(config: &AppConfig) -> ConfigValidation<()> {
    let mut errors = Vec::new();

    if config.port == 0 {
        errors.push(ConfigError::ValidationError {
            path: "port".to_string(),
            source_location: None,
            value: Some("0".to_string()),
            message: "port must be non-zero".to_string(),
        });
    }

    if config.host.is_empty() {
        errors.push(ConfigError::ValidationError {
            path: "host".to_string(),
            source_location: None,
            value: Some("".to_string()),
            message: "host cannot be empty".to_string(),
        });
    }

    match ConfigErrors::from_vec(errors) {
        Some(errs) => Validation::Failure(errs),
        None => Validation::Success(()),
    }
}
```

**Benefits**: Users see ALL config problems at once, not one at a time.

## Architecture

### Pure Core, Imperative Shell

Premortem follows stillwater's architectural pattern:

- **Pure Core**: Value merging, deserialization, validation are pure functions
- **Imperative Shell**: I/O operations use the `ConfigEnv` trait for dependency injection

```
src/
├── config.rs    # Config and ConfigBuilder (builder pattern)
├── error.rs     # ConfigError, ConfigErrors, ConfigValidation
├── value.rs     # Value enum for intermediate representation
├── source.rs    # Source trait and ConfigValues container
├── env.rs       # ConfigEnv trait and MockEnv for testing
└── validate.rs  # Validate trait for custom validation
```

### ConfigEnv Trait (Testable I/O)

All I/O is abstracted through the `ConfigEnv` trait:

```rust
use premortem::{Config, ConfigEnv, MockEnv, RealEnv};

// Production: uses real file system and environment
let config = Config::<AppConfig>::builder()
    .source(Toml::file("config.toml"))
    .build();  // Uses RealEnv internally

// Testing: uses mock file system and environment
let env = MockEnv::new()
    .with_file("config.toml", r#"host = "localhost"\nport = 8080"#)
    .with_env("APP_DEBUG", "true");

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

## Config Builder Pattern

### Basic Usage

```rust
use premortem::{Config, Validate, ConfigValidation};
use serde::Deserialize;
use stillwater::Validation;

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

impl Validate for AppConfig {
    fn validate(&self) -> ConfigValidation<()> {
        if self.port > 0 {
            Validation::Success(())
        } else {
            Validation::fail_with(ConfigError::ValidationError {
                path: "port".to_string(),
                source_location: None,
                value: Some(self.port.to_string()),
                message: "port must be positive".to_string(),
            })
        }
    }
}

let config = Config::<AppConfig>::builder()
    .source(Toml::file("config.toml"))
    .source(Env::new().prefix("APP"))
    .build()?;
```

### Source Layering

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

```rust
// 1. Base defaults
// 2. File config overrides defaults
// 3. Environment variables override file config
let config = Config::<AppConfig>::builder()
    .source(Defaults::new())
    .source(Toml::file("config.toml"))
    .source(Env::new().prefix("APP"))
    .build()?;
```

## Error Types Reference

### ConfigError Variants

| Variant | Description |
|---------|-------------|
| `SourceError` | A configuration source failed to load |
| `ParseError` | A field failed to parse to expected type |
| `MissingField` | A required field is missing |
| `ValidationError` | A validation rule failed |
| `CrossFieldError` | Cross-field validation failed |
| `UnknownField` | An unknown field was found |
| `NoSources` | No sources were provided to the builder |

### SourceErrorKind Variants

| Variant | Description |
|---------|-------------|
| `NotFound` | Source file was not found |
| `IoError` | Source file could not be read |
| `ParseError` | Source content could not be parsed |
| `ConnectionError` | Remote source failed to connect |
| `Other` | Other source-specific error |

## Feature Flags

```toml
[features]
default = ["toml", "derive"]
toml = []        # TOML file support
json = []        # JSON file support
yaml = []        # YAML file support (not yet implemented, see specs/001)
watch = []       # File watching for hot reload
remote = []      # Remote configuration sources (not yet implemented, see specs/002)
derive = []      # Derive macro for Validate trait
full = ["toml", "json", "yaml", "watch", "remote", "derive"]
```

## Testing Patterns

### MockEnv for Unit Tests

```rust
#[test]
fn test_config_loading() {
    let env = MockEnv::new()
        .with_file("config.toml", r#"
            [database]
            host = "localhost"
            port = 5432
        "#)
        .with_env("APP_DATABASE_HOST", "prod-db.example.com");

    let config = Config::<DbConfig>::builder()
        .source(Toml::file("config.toml"))
        .source(Env::new().prefix("APP"))
        .build_with_env(&env);

    assert!(config.is_ok());
    let config = config.unwrap();
    // Environment override wins
    assert_eq!(config.database.host, "prod-db.example.com");
}
```

### Testing Validation

```rust
#[test]
fn test_validation_accumulates_errors() {
    let env = MockEnv::new()
        .with_file("config.toml", r#"
            host = ""
            port = 0
        "#);

    let result = Config::<AppConfig>::builder()
        .source(Toml::file("config.toml"))
        .build_with_env(&env);

    assert!(result.is_err());
    let errors = result.unwrap_err();
    // Both validation errors should be present
    assert_eq!(errors.len(), 2);
}
```

### Testing Error Scenarios

```rust
#[test]
fn test_permission_denied() {
    let env = MockEnv::new()
        .with_unreadable_file("secrets.toml");

    let result = Config::<AppConfig>::builder()
        .source(Toml::file("secrets.toml"))
        .build_with_env(&env);

    assert!(result.is_err());
    // Check for SourceError with IoError kind
}

#[test]
fn test_missing_file() {
    let env = MockEnv::new()
        .with_missing_file("config.toml");

    let result = Config::<AppConfig>::builder()
        .source(Toml::file("config.toml"))
        .build_with_env(&env);

    assert!(result.is_err());
    // Check for SourceError with NotFound kind
}
```

## Stillwater Integration

Premortem uses these stillwater types:

| Type | Usage |
|------|-------|
| `Validation<T, E>` | Error accumulation for config errors |
| `NonEmptyVec<T>` | Guaranteed non-empty error lists |
| `Semigroup` | Combining errors from multiple sources |

### Re-exports

For convenience, premortem re-exports commonly used stillwater types:

```rust
pub use stillwater::{NonEmptyVec, Semigroup, Validation};
```

## Validation with Predicates

Premortem integrates with stillwater 0.13's composable predicates for elegant, reusable validation logic.

### Basic Usage

Use `validate_with_predicate()` for simple predicate-based validation with custom error messages:

```rust
use premortem::prelude::*;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
}

impl Validate for ServerConfig {
    fn validate(&self) -> ConfigValidation<()> {
        let validations = vec![
            validate_with_predicate(
                &self.host,
                "host",
                not_empty(),
                "host cannot be empty"
            ),
            validate_with_predicate(
                &self.port,
                "port",
                between(1, 65535),
                "port must be between 1 and 65535"
            ),
        ];

        Validation::all_vec(validations).map(|_| ())
    }
}
```

### Composition Patterns

Predicates compose elegantly using `.and()`, `.or()`, and `.not()`:

```rust
use premortem::prelude::*;

// Combine multiple predicates
let username_valid = not_empty()
    .and(len_between(3, 20))
    .and(starts_with(|c: char| c.is_alphabetic()));

validate_with_predicate(
    &username,
    "username",
    username_valid,
    "username must be 3-20 chars, non-empty, starting with letter"
)
```

### Available Predicates

#### String Predicates

| Predicate | Description |
|-----------|-------------|
| `not_empty()` | String must not be empty |
| `len_min(n)` | Minimum length |
| `len_max(n)` | Maximum length |
| `len_eq(n)` | Exact length |
| `len_between(min, max)` | Length in range (inclusive) |
| `starts_with(s)` | Starts with string/char/predicate |
| `ends_with(s)` | Ends with string/char/predicate |
| `contains(s)` | Contains substring |
| `is_alphabetic()` | All characters alphabetic |
| `is_alphanumeric()` | All characters alphanumeric |
| `is_numeric()` | All characters numeric |

#### Numeric Predicates

| Predicate | Description |
|-----------|-------------|
| `gt(n)` | Greater than |
| `ge(n)` | Greater than or equal |
| `lt(n)` | Less than |
| `le(n)` | Less than or equal |
| `eq(n)` | Equal to |
| `ne(n)` | Not equal to |
| `between(min, max)` | In range (inclusive) |
| `positive()` | Greater than zero |
| `negative()` | Less than zero |
| `non_negative()` | Greater than or equal to zero |

#### Collection Predicates

| Predicate | Description |
|-----------|-------------|
| `has_len(n)` | Exact collection length |
| `has_min_len(n)` | Minimum collection length |
| `has_max_len(n)` | Maximum collection length |
| `is_empty()` | Collection is empty |
| `is_not_empty()` | Collection is not empty |
| `all(predicate)` | All elements satisfy predicate |
| `any(predicate)` | Any element satisfies predicate |
| `contains_element(value)` | Collection contains value |

### Integration with validate_field

Convert predicates to validators using `from_predicate()`:

```rust
use premortem::prelude::*;
use premortem::validate::from_predicate;

impl Validate for AppConfig {
    fn validate(&self) -> ConfigValidation<()> {
        // Mix predicates with traditional validators
        let username_pred = from_predicate(
            not_empty().and(len_between(3, 20))
        );

        validate_field(&self.username, "username", &[&username_pred])
    }
}
```

### Advanced: Complex Composition

Build sophisticated validation rules by composing predicates:

```rust
use premortem::prelude::*;

// Password validation: 8-64 chars, contains digit and letter
let password_pred = len_between(8, 64)
    .and(any(|c: char| c.is_numeric()))
    .and(any(|c: char| c.is_alphabetic()));

validate_with_predicate(
    &password,
    "password",
    password_pred,
    "password must be 8-64 chars with letters and numbers"
)
```

### Benefits of Predicates

- **Composable**: Use `.and()`, `.or()`, `.not()` to build complex rules
- **Reusable**: Define once, use across multiple fields
- **Type-safe**: Compile-time guarantees
- **Zero-cost**: No runtime overhead
- **Tested**: Stillwater predicates are thoroughly tested with property-based tests

## Environment Variable Validation

Premortem provides ergonomic patterns for marking environment variables as required at the source level, with error accumulation for all missing variables.

### Required Environment Variables

Use `.require()` or `.require_all()` on the `Env` source to declare required environment variables:

```rust
use premortem::prelude::*;

// Mark individual variables as required
let config = Config::<AppConfig>::builder()
    .source(
        Env::prefix("APP_")
            .require("JWT_SECRET")
            .require("DATABASE_URL")
    )
    .build()?;

// Or mark multiple variables at once
let config = Config::<AppConfig>::builder()
    .source(
        Env::prefix("APP_")
            .require_all(&["JWT_SECRET", "DATABASE_URL", "API_KEY"])
    )
    .build()?;
```

**Key features**:
- Variable names specified WITHOUT prefix (prefix is added automatically)
- Missing variables cause `load()` to fail with accumulated errors
- ALL missing variables reported at once (not fail-fast)
- Error messages include full environment variable name with prefix

### Source-Level vs Value-Level Validation

Premortem separates two concerns:

1. **Source-level validation** (presence checking): Done during `Source::load()`
   - Use `.require()` on the `Env` source
   - Checks if environment variables exist
   - Fails before deserialization if missing

2. **Value-level validation** (constraints): Done after deserialization
   - Use `#[validate(...)]` attributes or `impl Validate`
   - Validates the parsed values meet constraints
   - Runs only if all required variables are present

```rust
#[derive(Debug, Deserialize, DeriveValidate)]
struct AppConfig {
    // Source-level: APP_JWT_SECRET must exist
    // Value-level: must be at least 32 characters
    #[validate(min_length(32))]
    jwt_secret: String,

    // Source-level: APP_DATABASE_URL must exist
    // No value-level validation
    database_url: String,

    // Source-level: APP_PORT must exist
    // Value-level: must be in valid port range
    #[validate(range(1..=65535))]
    port: u16,
}

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

### Error Messages

When required environment variables are missing, you get clear error messages:

```
Configuration error:
  [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
```

All missing variables are reported together, making it easy to fix multiple issues at once.

### Migration from Manual Validation

**Before** (90+ lines of imperative code):
```rust
impl Config {
    pub fn load<E: ConfigEnv>(env: &E) -> Result<Self, ConfigError> {
        let database_url = env
            .get_env("APP_DATABASE_URL")
            .ok_or_else(|| ConfigError("DATABASE_URL is required".to_string()))?;

        let jwt_secret = env
            .get_env("APP_JWT_SECRET")
            .ok_or_else(|| ConfigError("JWT_SECRET is required".to_string()))?;

        if jwt_secret.len() < 32 {
            return Err(ConfigError("JWT_SECRET must be at least 32 characters".to_string()));
        }

        let github_client_id = env
            .get_env("APP_GITHUB_CLIENT_ID")
            .ok_or_else(|| ConfigError("GITHUB_CLIENT_ID is required".to_string()))?;

        // ... 70+ more lines of repetitive code
    }
}
```

**After** (~15 lines of declarative code):
```rust
#[derive(Debug, Deserialize, DeriveValidate)]
struct Config {
    database_url: String,

    #[validate(min_length(32))]
    jwt_secret: String,

    github_client_id: String,
}

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

### Best Practices

1. **Use `.require_all()` for multiple variables**: More concise than chaining `.require()` calls

2. **Separate presence from validation**:
   - Source-level: Does the variable exist?
   - Value-level: Does the value meet constraints?

3. **Test with MockEnv**:
   ```rust
   #[test]
   fn test_missing_required_vars() {
       let env = MockEnv::new()
           .with_env("APP_JWT_SECRET", "secret");
       // Missing DATABASE_URL

       let result = Config::<AppConfig>::builder()
           .source(Env::prefix("APP_").require_all(&["JWT_SECRET", "DATABASE_URL"]))
           .build_with_env(&env);

       assert!(result.is_err());
       let errors = result.unwrap_err();
       assert_eq!(errors.len(), 1); // One missing variable
   }
   ```

4. **Clear error accumulation**: All missing variables are reported at once, helping users fix all issues in one go

## Common Patterns

### Custom Validation

```rust
impl Validate for DatabaseConfig {
    fn validate(&self) -> ConfigValidation<()> {
        use stillwater::Validation;

        let port_valid = if self.port > 0 && self.port < 65536 {
            Validation::Success(())
        } else {
            Validation::Failure(ConfigErrors::single(
                ConfigError::ValidationError { /* ... */ }
            ))
        };

        let host_valid = if !self.host.is_empty() {
            Validation::Success(())
        } else {
            Validation::Failure(ConfigErrors::single(
                ConfigError::ValidationError { /* ... */ }
            ))
        };

        // Combine validations - accumulates ALL errors
        port_valid.combine_with(host_valid, |_, _| ())
    }
}
```

### Source Location Tracking

```rust
// Track where config values came from
let loc = SourceLocation::new("config.toml")
    .with_line(10)
    .with_column(5);

// For environment variables
let loc = SourceLocation::env("APP_HOST");

// In error messages: "[config.toml:10:5] 'port': expected integer"
```

### Error Grouping

```rust
use premortem::group_by_source;

let errors: ConfigErrors = /* ... */;
let grouped = group_by_source(&errors);

for (source, errs) in grouped {
    println!("Errors in {}:", source);
    for err in errs {
        println!("  {}", err);
    }
}
```

## Development Commands

```bash
# Run tests
cargo test

# Run tests with all features
cargo test --all-features

# Check formatting
cargo fmt --check

# Run clippy
cargo clippy --all-features

# Build docs
cargo doc --all-features --no-deps
```

## Module Dependencies

```
stillwater (functional patterns)
    ↓
premortem
├── error.rs (uses Validation, NonEmptyVec, Semigroup)
├── config.rs (uses error, source, validate, env)
├── source.rs (uses value, error, env)
├── value.rs (standalone)
├── validate.rs (uses error)
└── env.rs (standalone)
```
