settings_loader 1.0.0

Opinionated configuration settings load mechanism for Rust applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# Implementation Plan - Task sl-h8h - Phase 1

**Epic**: sl-h8h - Explicit Configuration Layering API  
**Phase**: 1 (Explicit Layering)  
**Status**: Ready for Implementation  
**Total Subtasks**: 8 (strict dependency chain)  
**Total Tests**: 25 pre-written (tests/layer_builder_tests.rs)  
**Target**: v0.16.0

---

## ⚠️ RELEASE CONSTRAINT: NO RELEASE UNTIL ALL 7 PHASES COMPLETE

**CRITICAL**: No release to users until all 7 phases are complete and approved.

Single release model (v0.22.0 after Phase 7):
- Phase 1 (this task) → Development ✅ COMPLETE
- Phases 2-7 → Development and testing (no intermediate releases)
- All 7 phases → Final Gate 3 Integration Review
- **v0.22.0** → ONLY release, published when all phases ready

**NO intermediate releases** (v0.16.0, v0.17.0, etc.) to production. 
All phases developed, tested, and approved together before any release.

---

## Overview

Phase 1 implements the Explicit Configuration Layering API - allows applications to compose configuration sources with clear, explicit precedence. Later layers override earlier layers.

Follows **RED → GREEN → REFACTOR** TDD cycle:
- **RED** (sl-bru): Create types, tests fail with stub implementations
- **GREEN** (sl-bv4 through sl-5h9): Implement features incrementally
- **INTEGRATION** (sl-apw, sl-mvm): Connect to existing SettingsLoader
- **CLEANUP** (sl-uda): Documentation and exports
- **VALIDATION** (sl-6pk): All tests pass, quality gates verified

---

## What Gets Built

**New Type**: `ConfigLayer` enum (5 variants)
- `Path(PathBuf)` - Explicit file path
- `EnvVar(String)` - Config path from environment variable
- `EnvSearch { env: Environment, dirs: Vec<PathBuf> }` - Environment-directed search
- `Secrets(PathBuf)` - Secrets file
- `EnvVars { prefix: String, separator: String }` - System environment variables

**New Type**: `LayerBuilder` struct
- Fluent API for composing layers
- `new()`, `with_path()`, `with_env_var()`, `with_secrets()`, `with_env_vars()`
- Query methods: `layers()`, `layer_count()`, `is_empty()`, `has_layers()`
- Test helpers: `has_path_layer()`, `has_env_var_layer()`, etc.
- Core: `build()``ConfigBuilder<DefaultState>`

**Enhanced Trait**: `LoadingOptions`
- New optional method: `build_layers(builder: LayerBuilder) -> LayerBuilder`
- Default impl: returns builder unchanged (backward compatible)

**Modified Trait**: `SettingsLoader`
- `load()` checks for explicit layers first
- Falls back to implicit layering if no layers defined
- 100% backward compatible

---

## 8 Subtasks with Dependencies

### PHASE1.1: Core Types [sl-bru] RED

**Files**: Create `src/layer.rs`

```rust
#[derive(Debug, Clone)]
pub enum ConfigLayer {
    Path(PathBuf),
    EnvVar(String),
    EnvSearch { env: Environment, dirs: Vec<PathBuf> },
    Secrets(PathBuf),
    EnvVars { prefix: String, separator: String },
}

pub struct LayerBuilder {
    layers: Vec<ConfigLayer>,
}
```

**Tests**: 1-6 compile, fail (RED phase)

**Acceptance**: ✅ COMPLETE
- [x] `src/layer.rs` created with ConfigLayer enum (5 variants)
- [x] LayerBuilder struct with Vec<ConfigLayer> storage
- [x] Debug, Clone traits derived
- [x] Tests 1-6 passing (not red, implemented)

**Blocking**: Blocks sl-bv4

---

### PHASE1.2: Builder Methods [sl-bv4] GREEN

**File**: `src/layer.rs`

Implement fluent builder interface:
```rust
impl LayerBuilder {
    pub fn new() -> Self { ... }
    pub fn with_path(mut self, path: impl AsRef<Path>) -> Self { ... }
    pub fn with_env_var(mut self, var_name: &str) -> Self { ... }
    pub fn with_secrets(mut self, path: impl AsRef<Path>) -> Self { ... }
    pub fn with_env_vars(mut self, prefix: &str, separator: &str) -> Self { ... }
    
    pub fn layers(&self) -> &[ConfigLayer] { ... }
    pub fn layer_count(&self) -> usize { ... }
    pub fn is_empty(&self) -> bool { ... }
    
    // Test helpers
    pub fn has_path_layer(&self) -> bool { ... }
    pub fn has_env_var_layer(&self, name: &str) -> bool { ... }
    pub fn has_secrets_layer(&self) -> bool { ... }
    pub fn has_env_vars_layer(&self, prefix: &str, sep: &str) -> bool { ... }
}
```

**Tests**: Tests 1-9 pass (GREEN) ✅ PASSING

**Acceptance**: ✅ COMPLETE
- [x] All builder methods implemented
- [x] All query/helper methods work
- [x] Tests 1-9 passing
- [x] Fluent API chaining works

**Blocked by**: sl-bru  
**Blocks**: sl-nz1

---

### PHASE1.3: Build Implementation [sl-nz1] GREEN

**File**: `src/layer.rs`

Implement `LayerBuilder::build()` - the core:

```rust
pub fn build(self) -> Result<ConfigBuilder<DefaultState>, SettingsError> {
    let mut config = Config::builder();
    
    for layer in self.layers {
        config = match layer {
            ConfigLayer::Path(path) => {
                let abs_path = path.absolutize()?;
                config.add_source(
                    ConfigFile::from(abs_path.as_ref()).required(true)
                )
            }
            ConfigLayer::EnvVar(var_name) => {
                match std::env::var(&var_name) {
                    Ok(env_path) => {
                        let abs_path = Path::new(&env_path).absolutize()?;
                        config.add_source(
                            ConfigFile::from(abs_path.as_ref()).required(true)
                        )
                    }
                    Err(std::env::VarError::NotPresent) => config,  // Skip gracefully
                    Err(e) => return Err(e.into()),
                }
            }
            ConfigLayer::Secrets(path) => {
                let abs_path = path.absolutize()?;
                config.add_source(
                    ConfigFile::from(abs_path.as_ref()).required(true)
                )
            }
            ConfigLayer::EnvVars { prefix, separator } => {
                config.add_source(
                    config::Environment::default()
                        .prefix(&prefix)
                        .separator(&separator)
                        .try_parsing(true)
                )
            }
            ConfigLayer::EnvSearch { .. } => config,  // Defer to Phase 3
        };
    }
    
    Ok(config)
}
```

**Key Behavior**:
- Layer precedence via config crate's deep merge (later `.add_source()` wins)
- Path files MUST exist → error
- Secrets files MUST exist → error
- EnvVar missing → skip gracefully (NO error)
- Use `path_absolutize` for consistent path handling

**Tests**: Tests 10-22 pass (GREEN) ✅ PASSING - precedence, formats, errors, env vars

**Acceptance**: ✅ COMPLETE
- [x] ConfigLayer::Path loads files with absolutize
- [x] ConfigLayer::EnvVar resolves with graceful skip
- [x] ConfigLayer::Secrets requires file to exist
- [x] ConfigLayer::EnvVars integrates with config crate Environment
- [x] Layer precedence correct (later overrides earlier)
- [x] Error handling for missing Path/Secrets files
- [x] Tests 10-22 passing
- [x] All file formats work (YAML, JSON, TOML auto-detected)

**Blocked by**: sl-bv4  
**Blocks**: sl-5h9

---

### PHASE1.4: File Format Detection [sl-5h9] GREEN

**File**: `src/layer.rs`

No custom code needed. Leverage config crate's built-in format detection:
- `.yaml`, `.yml` → YAML
- `.json` → JSON
- `.toml` → TOML
- `.hjson` → HJSON
- `.ron` → RON

ConfigFile already detects from extension automatically.

**Tests**: Tests 12-15 pass (formats already working from Phase 1.3) ✅ PASSING

**Acceptance**: ✅ COMPLETE
- [x] YAML support verified
- [x] JSON support verified
- [x] TOML support verified
- [x] Format detection automatic
- [x] Tests 12-15 passing

**Blocked by**: sl-nz1  
**Blocks**: sl-apw

---

### PHASE1.5: SettingsLoader Integration [sl-apw] GREEN

**File**: `src/settings_loader.rs`

Modify `SettingsLoader::load()` to support explicit layering with fallback:

```rust
#[tracing::instrument(level = "info")]
fn load(options: &Self::Options) -> Result<Self, SettingsError>
where
    Self: DeserializeOwned,
{
    let initial_builder = LayerBuilder::new();
    let builder = options.build_layers(initial_builder);
    
    // If explicit layers defined, use them
    if builder.has_layers() {
        let config_builder = builder.build()?;
        let config = config_builder.build()?;
        return config.try_deserialize().map_err(Into::into);
    }
    
    // Fall back to implicit layering for backward compatibility
    Self::load_implicit(options)
}

// Extract current load() logic here
fn load_implicit(options: &Self::Options) -> Result<Self, SettingsError>
where
    Self: DeserializeOwned,
{
    // Current implementation moved here
    ...
}
```

**Key Points**:
- Explicit layers take precedence
- If no explicit layers, use existing implicit layering
- 100% backward compatible

**Tests**: All existing tests pass (backward compatibility verified) ✅ PASSING

**Acceptance**: ✅ COMPLETE
- [x] `load()` modified to check explicit layers
- [x] `has_layers()` implemented on LayerBuilder
- [x] Fall back to `load_implicit()` if no layers
- [x] All existing tests still passing (8/8 in lib)
- [x] No changes to existing test fixtures

**Blocked by**: sl-5h9  
**Blocks**: sl-mvm

---

### PHASE1.6: LoadingOptions Enhancement [sl-mvm] GREEN

**File**: `src/lib.rs`

Add optional trait method with default implementation:

```rust
pub trait LoadingOptions: Sized {
    // ... existing methods ...
    
    /// Build explicit configuration layers.
    /// 
    /// Default implementation returns builder unchanged (backward compatible).
    /// Override to define explicit layer composition.
    fn build_layers(&self, builder: LayerBuilder) -> LayerBuilder {
        builder
    }
}
```

**Key Points**:
- New trait method with default implementation
- Zero breaking changes - all existing implementors work unchanged
- Apps can override to define explicit layers

**Tests**: All existing tests still pass ✅ PASSING

**Acceptance**: ✅ COMPLETE
- [x] LoadingOptions trait modified
- [x] `build_layers()` method added with default impl
- [x] All existing tests still pass (8/8)
- [x] No trait implementors require changes

**Blocked by**: sl-apw  
**Blocks**: sl-uda

---

### PHASE1.7: Documentation & Exports [sl-uda] GREEN

**File**: `src/lib.rs`

Export types from module:
```rust
pub mod layer;
pub use layer::{ConfigLayer, LayerBuilder};
```

Add comprehensive docs to `src/layer.rs`:
```rust
//! Explicit configuration layering API.
//!
//! Allows applications to compose configuration sources with clear, explicit 
//! precedence. Later layers override earlier layers.
//!
//! # Layer Types
//!
//! - `Path` - Load from explicit file path
//! - `EnvVar` - Load from path specified in environment variable
//! - `Secrets` - Load from secrets file
//! - `EnvVars` - Load from system environment variables with prefix
//!
//! # Example
//!
//! ```rust,ignore
//! use settings_loader::{LayerBuilder, SettingsLoader, LoadingOptions};
//!
//! impl LoadingOptions for MyOptions {
//!     fn build_layers(&self, builder: LayerBuilder) -> LayerBuilder {
//!         builder
//!             .with_path("config.yaml")
//!             .with_secrets("secrets.yaml")
//!             .with_env_vars("APP", "__")
//!     }
//! }
//!
//! let settings = MySettings::load(&options)?;
//! ```
```

Document each variant of ConfigLayer with examples.

**Tests**: Doc tests passing ✅ PASSING

**Acceptance**: ✅ COMPLETE
- [x] ConfigLayer exported from lib.rs
- [x] LayerBuilder exported from lib.rs
- [x] Module-level docs complete
- [x] ConfigLayer enum docs
- [x] LayerBuilder struct docs
- [x] Working examples in docs
- [x] Backward compatibility examples

**Blocked by**: sl-mvm  
**Blocks**: sl-6pk

---

### PHASE1.8: Validation & Quality [sl-6pk] REFACTOR

**Files**: All

Run comprehensive validation:

```bash
cargo fmt --all
cargo test --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check
```

Verify:
- All 25 new tests in layer_builder_tests.rs passing
- All existing tests passing (backward compatibility)
- Zero clippy warnings
- Code formatted
- Implicit layering behavior unchanged
- No modifications to existing test fixtures

**Tests**: All 25 new + all existing tests passing

**Acceptance**: ✅ COMPLETE
- [x] `cargo test --all` passes
  - [x] All 27 layer_builder_tests passing (25 original + 2 debug)
  - [x] All 8 existing lib tests passing
- [x] `cargo fmt --all` passes
- [x] `cargo clippy --all-targets --all-features` (0 code warnings)
- [x] `cargo check` passes
- [x] Implicit layering unchanged
- [x] Ready for code review (Gate 2)

**Blocked by**: sl-uda  
**Status**: ✅ READY FOR GATE 2 - CODE REVIEW

---

## Success Criteria

**Definition of Done**: ✅ ALL COMPLETE
- ✅ All 27 tests in `tests/layer_builder_tests.rs` passing (25 original + 2 debug tests)
- ✅ All existing tests still passing (backward compatibility: 8 tests in lib)
- ✅ 0 code clippy warnings (only cargo dependency warnings)
- ✅ Code formatted with `cargo fmt`
- ✅ Documentation complete with examples
- ✅ ConfigLayer and LayerBuilder exported from lib.rs
- ✅ LoadingOptions::build_layers() trait method added with default impl
- ✅ SettingsLoader::load() modified with explicit layer support and fallback

**Code Quality Gates**: ✅ ALL PASSED
- ✅ Zero code clippy warnings
- ✅ All 35 tests passing (27 new + 8 existing)
- ✅ Code formatted
- ✅ No unsafe code
- ✅ Documentation complete with working examples
- ✅ Backward compatible (all existing code works unchanged)

---

## Implementation Notes

1. **Don't reinvent**: Config crate already handles format detection via file extension - use it
2. **Path handling**: Use existing `path_absolutize` crate for consistent absolutization
3. **Graceful degradation**: EnvVar not set = skip layer (don't error)
4. **File validation**: Path and Secrets files must exist (required=true)
5. **Layer precedence**: Config crate's `.add_source()` order determines precedence - later adds override
6. **No new dependencies**: Phase 1 uses only existing dependencies
7. **Backward compatible**: Apps not implementing `build_layers()` continue to work unchanged
8. **TDD discipline**: Implement features to make tests pass, not the other way around

---

## File Structure After Completion

```
src/
  layer.rs                # NEW: ConfigLayer, LayerBuilder
  lib.rs                  # MODIFIED: Export types, add build_layers()
  settings_loader.rs      # MODIFIED: Explicit layers + fallback
  environment.rs          # UNCHANGED
  error.rs                # UNCHANGED
  internals/              # UNCHANGED
  common/                 # UNCHANGED

tests/
  layer_builder_tests.rs  # Tests already exist (25 tests)
  
history/
  CONSOLIDATED_ROADMAP.md
  DESIGN.md
  IMPLEMENTATION_PLAN.md
  TEST_PLAN_SUMMARY.md
  MIGRATION_GUIDE.md
  sl-h8h_phase1_tdd_breakdown.md
```

---

## Progress Tracking

Track completion as each subtask finishes:

- [x] PHASE1.1 (sl-bru) - Core Types [RED] ✅ COMPLETE
- [x] PHASE1.2 (sl-bv4) - Builder Methods [GREEN] ✅ COMPLETE
- [x] PHASE1.3 (sl-nz1) - Build Logic [GREEN] ✅ COMPLETE
- [x] PHASE1.4 (sl-5h9) - Format Detection [GREEN] ✅ COMPLETE
- [x] PHASE1.5 (sl-apw) - SettingsLoader Integration [GREEN] ✅ COMPLETE
- [x] PHASE1.6 (sl-mvm) - LoadingOptions Trait [GREEN] ✅ COMPLETE
- [x] PHASE1.7 (sl-uda) - Documentation [GREEN] ✅ COMPLETE
- [x] PHASE1.8 (sl-6pk) - Validation [REFACTOR] ✅ COMPLETE → Ready for Gate 2

---

## Next Steps

1. Approve implementation plan
2. Create feature branch: `feat/phase1-explicit-layering`
3. Start PHASE1.1: Create `src/layer.rs` with ConfigLayer + LayerBuilder types
4. Run `cargo test layer_builder_tests` → Tests 1-6 fail (RED phase)
5. Continue through subtasks in dependency order
6. Run comprehensive validation in PHASE1.8
7. Request code review approval (Gate 2)

---

## Related Documents

- `history/CONSOLIDATED_ROADMAP.md` - Full 7-phase roadmap (v0.16.0 through v1.0.0)
- `history/DESIGN.md` - Phase 1 detailed design specification
- `history/sl-h8h_phase1_tdd_breakdown.md` - TDD task breakdown with test mapping
- `tests/layer_builder_tests.rs` - 25 pre-written tests (RED phase ready)
- `history/MIGRATION_GUIDE.md` - Migration guide for users of Phase 1 API
- `ref/architecture-proposal.md` - Original architecture vision
- `ref/turtle-consolidation.md` - Turtle use case analysis