# Phase 2: Config — COMPLETED

## Goal
Load, validate, and expose server configuration so `main.rs` can wire subsystems together. Configuration is resolved from three layers (highest precedence first): environment variables > TOML file > compiled defaults. Config loading is **pure** — no filesystem side effects beyond reading the TOML file.

## Files
- `src/config.rs` — structs, defaults, loading, validation, env override application, `ConfigError` enum, `CliArgs`
- `src/main.rs` — CLI arg parsing + config loading + startup summary
- `config/config.example.toml` — fully commented reference config with two namespaces
- `Cargo.toml` — added `clap` dependency

## Completed Work

### config.rs — Structs & Derives
- [x] `Clone` added to all 5 config structs (`ServerConfig`, `TcpConfig`, `HttpConfig`, `StorageConfig`, `NamespaceConfig`)
- [x] `namespaces` field changed from `HashMap<String, NamespaceConfig>` to `HashMap<NamespaceId, NamespaceConfig>`
- [x] `#[serde(default)]` on `tcp`, `http`, `storage`, and `namespaces` fields of `ServerConfig`
- [x] `#[serde(default)]` at the struct level on `TcpConfig`, `HttpConfig`, `StorageConfig` — allows partial TOML sections (e.g. `[tcp]` with only `port` set)

### config.rs — Default Implementations
- [x] `TcpConfig::default()` — bind_address `"127.0.0.1"`, port `9876`
- [x] `HttpConfig::default()` — bind_address `"127.0.0.1"`, port `8080`
- [x] `StorageConfig::default()` — data_dir `"./data"`
- [x] `namespaces` defaults to empty `HashMap` — caught by validation as `NoNamespaces`

### config.rs — ConfigError Enum
- [x] `PortOutOfRange { field: String, port: u16 }`
- [x] `PortConflict { port: u16, bind_address: String }`
- [x] `NoNamespaces`
- [x] `InvalidNamespaceName(String)`
- [x] `ApiKeyTooShort { namespace: String, len: usize }`
- [x] `EmptyPermissions(String)`
- [x] `DuplicateApiKey(String)`
- [x] `InvalidEnvVar { var: String, reason: String }`
- [x] `Display` impl with human-readable messages
- [x] `PartialEq, Eq` for test assertions

### config.rs — Validation (`validate(&self) -> Vec<ConfigError>`)
- [x] Ports in `1024..=65535` (both TCP and HTTP)
- [x] No port+bind_address conflict between TCP and HTTP
- [x] At least one namespace required
- [x] Namespace names match `[a-zA-Z0-9_-]+` (char-level check, no regex crate)
- [x] API keys ≥ 24 characters
- [x] Non-empty permissions per namespace
- [x] No duplicate API keys across namespaces
- [x] Collects **all** errors — no short-circuiting

### config.rs — Environment Variable Overrides (`apply_env_overrides`)
- [x] Injectable closure `env: impl Fn(&str) -> Option<String>` for testability
- [x] `FAFB_TCP_ADDRESS` → `tcp.bind_address`
- [x] `FAFB_TCP_PORT` → `tcp.port` (parse errors produce `InvalidEnvVar`)
- [x] `FAFB_HTTP_ADDRESS` → `http.bind_address`
- [x] `FAFB_HTTP_PORT` → `http.port` (parse errors produce `InvalidEnvVar`)
- [x] `FAFB_DATA_DIR` → `storage.data_dir`

### config.rs — Loading (`ServerConfig::load(path: &Path) -> Result<Self>`)
- [x] Reads TOML file with `.map_err()` to wrap as `Error::Config` (NOT `Error::Storage` — avoids `#[from] std::io::Error` auto-conversion pitfall)
- [x] Deserializes via `toml::from_str`
- [x] Applies env overrides with `|key| std::env::var(key).ok()`
- [x] Runs validation
- [x] Merges all errors → if any, returns `Error::Config(joined messages with "; ")`

### config.rs — CLI Arguments
- [x] `CliArgs` struct with `clap::Parser` derive
- [x] `--config` / `-c` option as `Option<PathBuf>`

### main.rs
- [x] Parses CLI args via `CliArgs::parse()`
- [x] Resolves config path: explicit arg → default `config.toml`
- [x] Loads config, prints error and exits on failure
- [x] Prints startup summary: TCP/HTTP addresses, storage dir, namespace count with permissions

### config/config.example.toml
- [x] Fully commented with env var names
- [x] API keys fixed to 24+ characters (was 23 chars — below minimum)
- [x] Two namespace examples: `default` (full permissions) and `analytics` (Publish + Query only)

### 17 Unit Tests
- [x] `defaults_are_applied` — omitted sections get correct default values
- [x] `valid_full_config` — full TOML parses and validates successfully
- [x] `namespace_id_as_toml_key` — `NamespaceId` works as TOML map key
- [x] `reject_port_below_1024` — port 80 triggers `PortOutOfRange`
- [x] `reject_port_above_65535` — port 70000 is a deserialization error (u16 overflow), not a validation error
- [x] `reject_port_conflict` — same port + bind_address triggers `PortConflict`
- [x] `allow_same_port_different_bind` — same port, different bind address is OK
- [x] `reject_no_namespaces` — empty namespaces triggers `NoNamespaces`
- [x] `reject_invalid_namespace_name` — name with spaces/special chars triggers `InvalidNamespaceName`
- [x] `reject_api_key_too_short` — key < 24 chars triggers `ApiKeyTooShort`
- [x] `reject_empty_permissions` — empty permissions triggers `EmptyPermissions`
- [x] `reject_duplicate_api_key` — same key across namespaces triggers `DuplicateApiKey`
- [x] `env_override_addresses` — env overrides both bind addresses
- [x] `env_override_ports` — env overrides both ports
- [x] `env_override_data_dir` — env overrides storage data_dir
- [x] `env_override_invalid_port` — non-numeric port produces `InvalidEnvVar`
- [x] `load_valid_config_from_file` — end-to-end load from tempfile

## Post-completion Amendments

### QueueConfig added (plan 10)
A `QueueConfig` struct was added to `ServerConfig` with fields `max_depth`, `backpressure_policy`, `max_redelivery_attempts`, and `dlq_topic_suffix`. This provides a `[queue]` TOML section with defaults. `BackpressurePolicy` gained `serde::Deserialize`. See DD-014 and plan 10 for rationale.

### config.example.toml updated (plan 10)
Added `[queue]` section documenting all four queue engine tuning knobs.

## Design Decisions Made
- `namespaces` field uses `#[serde(default)]` to produce empty HashMap — omitting `[namespaces]` from TOML gives a clean `NoNamespaces` validation error instead of a cryptic deserialization error
- Sub-config structs use `#[serde(default)]` at the struct level (not just field level on `ServerConfig`) — allows partial sections like `[tcp]` with only `port` set
- `ConfigError` uses owned `String` fields (not `&'static str`) — more flexible for dynamic values like namespace names
- Env var names use shorter form: `FAFB_TCP_ADDRESS` (not `FAFB_TCP_BIND_ADDRESS`) for ergonomics
- Error messages joined with `"; "` (not `"\n"`) for single-line display compatibility
- `main.rs` falls back to `config.toml` without existence check — `load()` returns a clear file-read error if missing

## Test Summary
- 17 new unit tests in `src/config.rs`, all passing
- 30 total tests across codebase, all passing
- `cargo clippy` clean, `cargo fmt` clean
- `cargo run -- --config config/config.example.toml` loads and prints config summary
