# procenv

**Note: This file was generated by Claude**

A Rust derive macro for declarative environment variable configuration with miette based error diagnostics.

[![Crates.io](https://img.shields.io/crates/v/procenv.svg)](https://crates.io/crates/procenv)
[![Documentation](https://docs.rs/procenv/badge.svg)](https://docs.rs/procenv)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

## Why procenv?

Most configuration crates stop at the first error. **procenv shows you everything that's wrong at once.**

```
procenv::multiple_errors

  × 3 configuration error(s) occurred

Error: missing required environment variable: DATABASE_URL
  help: set DATABASE_URL in your environment or .env file

Error: missing required environment variable: API_KEY
  help: set API_KEY in your environment or .env file

Error: failed to parse PORT: expected u16, got "not_a_number"
  help: expected a valid u16
```

## Features

| Feature                     | Description                                                      |
| --------------------------- | ---------------------------------------------------------------- |
| **Error Accumulation**      | Shows ALL config errors at once—no more fix-one-run-again cycles |
| **Diagnostics**             | Uses `miette` for error codes, help text, and source spans       |
| **File Config Support**     | Load from TOML, JSON, or YAML with precise error locations       |
| **Source Attribution**      | Know exactly where each config value came from                   |
| **Secret Masking**          | Sensitive values hidden in Debug output and error messages       |
| **CLI Integration**         | Built-in `clap` support for command-line overrides               |
| **.env.example Generation** | Auto-generate documentation for your config                      |
| **Profile Support**         | Environment-specific defaults (dev, staging, prod)               |

## Quick Start

```rust
use procenv::EnvConfig;

#[derive(EnvConfig)]
struct Config {
    #[env(var = "DATABASE_URL")]
    database_url: String,

    #[env(var = "PORT", default = "8080")]
    port: u16,

    #[env(var = "API_KEY", optional)]
    api_key: Option<String>,

    #[env(var = "SECRET_TOKEN", secret)]
    secret: String,
}

fn main() -> Result<(), procenv::Error> {
    let config = Config::from_env()?;
    println!("Server starting on port {}", config.port);
    Ok(())
}
```

## Installation

```toml
[dependencies]
procenv = "0.1"

# Add validation support
procenv = { version = "0.1", features = ["validator"] }

# Minimal install (no file configs, no CLI)
procenv = { version = "0.1", default-features = false, features = ["dotenv", "secrecy"] }
```

### Feature Flags

| Feature     | Description                                                    |
| ----------- | -------------------------------------------------------------- |
| `dotenv`    | Load `.env` files automatically (enabled by default)           |
| `secrecy`   | `SecretString` support for sensitive data (enabled by default) |
| `file-all`  | TOML, JSON, and YAML file configuration (enabled by default)   |
| `clap`      | CLI argument integration (enabled by default)                  |
| `toml`      | TOML file support only                                         |
| `yaml`      | YAML file support only                                         |
| `validator` | Field validation support                                       |

## Attribute Reference

### Field Attributes

```rust
#[derive(EnvConfig)]
struct Config {
    // Required field - errors if missing
    #[env(var = "DATABASE_URL")]
    database_url: String,

    // Default value - used when env var is missing
    #[env(var = "PORT", default = "8080")]
    port: u16,

    // Optional field - becomes None if missing
    #[env(var = "API_KEY", optional)]
    api_key: Option<String>,

    // Secret field - masked in errors and Debug output
    #[env(var = "SECRET_TOKEN", secret)]
    secret: String,

    // JSON/complex types via serde
    #[env(var = "ALLOWED_HOSTS", format = "json")]
    hosts: Vec<String>,

    // CLI argument override
    #[env(var = "VERBOSE", arg = "verbose", short = 'v')]
    verbose: bool,
}
```

### Struct Attributes

```rust
#[derive(EnvConfig)]
#[env_config(
    prefix = "APP_",                    // Prefix all env vars
    dotenv,                             // Load .env file
    profile_env = "APP_ENV",            // Profile selection var
    profiles = ["dev", "staging", "prod"]
)]
struct Config {
    #[env(var = "DATABASE_URL")]
    #[profile(
        dev = "postgres://localhost/dev",
        staging = "postgres://staging/app",
        prod = "postgres://prod/app"
    )]
    database_url: String,
}
```

## File Configuration

Load configuration from files with environment variable overrides:

```rust
use procenv::{EnvConfig, ConfigBuilder};
use serde::Deserialize;

// Using the derive macro with file support
#[derive(EnvConfig, Deserialize)]
#[env_config(
    prefix = "APP_",
    file_optional = "config.toml",
    dotenv
)]
struct Config {
    #[env(var = "DATABASE_URL")]
    database_url: String,

    #[env(var = "PORT", default = "8080")]
    port: u16,

    #[env(var = "DEBUG", default = "false")]
    debug: bool,
}

// Load with layered priority: defaults < file < env
let config = Config::from_config()?;

// Or with source attribution
let (config, sources) = Config::from_config_with_sources()?;
```

### Using ConfigBuilder Directly

For more control, use `ConfigBuilder`:

```rust
let config: Config = ConfigBuilder::new()
    .file("config.toml")                    // Base configuration
    .file_optional("config.local.toml")     // Local overrides (gitignored)
    .env_prefix("APP_")                     // Environment overrides
    .env_separator("__")                    // Custom separator (default: "_")
    .build()?;

// With origin tracking
let (config, origins) = ConfigBuilder::new()
    .file("config.toml")
    .env_prefix("APP_")
    .build_with_origins()?;
```

### File Error Diagnostics

Type mismatches show the exact location in your config file:

```
   ╭─[config.toml:5:8]
 4 │ host = "localhost"
 5 │ port = "not_a_number"
   ·        ───────┬──────
   ·               ╰── invalid type: string "not_a_number", expected u16
   ╰────
  help: check that the value matches the expected type
```

## Source Attribution

Track where each configuration value originated:

```rust
// For environment-based loading
let (config, sources) = Config::from_env_with_sources()?;

// For file-based loading (with nested field support)
let (config, sources) = Config::from_config_with_sources()?;

println!("{}", sources);
```

Output:

```
Configuration Source:
--------------------------------------------------
  database_url    <- Environment variable [DATABASE_URL]
  port            <- Config file (config.toml) [PORT]
  api_key         <- .env file [API_KEY]
  debug           <- Default value [DEBUG]
  database.host   <- Config file (config.toml) [database.host]
  database.port   <- Config file (config.toml) [database.port]
```

Sources include: `Environment`, `ConfigFile`, `DotenvFile`, `Profile`, `Default`, `Cli`, and `NotSet`.

**Key features:**
- Nested fields are individually attributed (e.g., `database.host`, `database.port`)
- `.env` files are distinguished from OS environment variables
- Full attribution across all loading methods (`from_env`, `from_config`, `from_args`)

## .env.example Generation

Auto-generate documentation for your configuration:

```rust
println!("{}", Config::env_example());
```

Output:

```bash
# Configuration for MyApp

# Database connection URL (required)
DATABASE_URL=

# Server port (default: 8080)
# PORT=8080

# API key for external service (optional)
# API_KEY=

# Authentication token [SECRET]
SECRET_TOKEN=
```

## CLI Integration

Combine environment variables with command-line arguments:

```rust
#[derive(EnvConfig)]
struct Config {
    #[env(var = "PORT", default = "8080", arg = "port", short = 'p')]
    port: u16,

    #[env(var = "VERBOSE", arg = "verbose", short = 'v')]
    verbose: bool,
}

// Priority: CLI args > Environment > Defaults
let config = Config::from_args()?;
```

```bash
# All equivalent:
PORT=3000 ./myapp
./myapp --port 3000
./myapp -p 3000
```

## Nested Configuration

Compose configurations with the `flatten` attribute:

```rust
#[derive(EnvConfig)]
struct DatabaseConfig {
    #[env(var = "HOST", default = "localhost")]
    host: String,

    #[env(var = "PORT", default = "5432")]
    port: u16,
}

#[derive(EnvConfig)]
#[env_config(prefix = "APP_")]
struct Config {
    #[env(flatten, prefix = "DB_")]
    database: DatabaseConfig,  // Reads APP_DB_HOST, APP_DB_PORT
}
```

## Error Handling

All errors implement `miette::Diagnostic` for formatted terminal output:

```rust
match Config::from_env() {
    Ok(config) => run(config),
    Err(e) => {
        // Pretty-printed with colors, context, and help text
        eprintln!("{:?}", miette::Report::from(e));
        std::process::exit(1);
    }
}
```

## Comparison

| Feature                 | procenv | figment | config-rs | envy |
| ----------------------- | ------- | ------- | --------- | ---- |
| Error accumulation      | **Yes** | No      | No        | No   |
| miette diagnostics      | **Yes** | No      | No        | No   |
| .env.example generation | **Yes** | No      | No        | No   |
| CLI integration         | **Yes** | No      | No        | No   |
| Compile-time derive     | Yes     | No      | No        | Yes  |
| File configs            | Yes     | Yes     | Yes       | No   |
| Secret masking          | Yes     | No      | No        | No   |
| Source attribution      | Yes     | Yes     | Yes       | No   |

## Examples

```bash
# Basic usage with error accumulation
cargo run --example basic

# Source attribution
cargo run --example source_attribution

# File configuration
cargo run --example file_config --features file-all

# Type mismatch error spans
cargo run --example test_type_mismatch --features file-all
```

## Project Structure

```
procenv/
├── crates/
│   ├── procenv/           # Main crate (runtime + re-exports)
│   │   ├── src/
│   │   │   ├── lib.rs     # Error types, Source enum
│   │   │   └── file.rs    # ConfigBuilder, file parsing
│   │   ├── examples/
│   │   └── tests/
│   └── procenv_macro/     # Proc-macro implementation
│       └── src/
│           ├── lib.rs     # Macro entry point
│           ├── parse.rs   # Attribute parsing
│           ├── field.rs   # Field code generation
│           └── expand.rs  # Macro expansion
└── PROGRESS.md            # Development roadmap
```

## Generated Methods

The `#[derive(EnvConfig)]` macro generates these methods:

| Method                       | Description                                        |
| ---------------------------- | -------------------------------------------------- |
| `from_env()`                 | Load from environment variables                    |
| `from_env_with_sources()`    | Load with source attribution                       |
| `from_config()`              | Load from files + env (requires `file` feature)    |
| `from_config_with_sources()` | File loading with source attribution               |
| `from_args()`                | Load from CLI args + env (requires `clap` feature) |
| `env_example()`              | Generate `.env.example` content                    |
| `sources()`                  | Get source attribution only                        |

## Development Status

**Phase A.0 (Correctness Sprint): COMPLETE** ✅

All correctness issues have been resolved:

- ✅ Error accumulation with miette diagnostics
- ✅ Source attribution for all loading methods
- ✅ Profile support with correct source tracking
- ✅ File configs with type mismatch spans
- ✅ `from_config()` and `from_config_with_sources()` with full attribution
- ✅ CLI parse errors show target types
- ✅ Nested field attribution (flattened fields individually attributed)
- ✅ Dotenv provenance tracking (`.env` vs OS environment distinguished)
- ✅ CLI/nested config source fidelity

See [PROGRESS.md](PROGRESS.md) for the full roadmap.

## AI-Assisted Development

Parts of this project—including tests, documentation (this document), and commit messages—were generated with assistance from [Claude](https://claude.ai). And were not fully reviewed.

## License

MIT
