1use std::collections::BTreeMap;
40use std::fmt;
41
42use crate::config::Config;
43use crate::error::SourceLocation;
44use crate::value::Value;
45
46#[derive(Debug, Clone)]
48pub struct TracedValue {
49 pub value: Value,
51 pub source: SourceLocation,
53 pub is_final: bool,
55}
56
57impl TracedValue {
58 pub fn new(value: Value, source: SourceLocation, is_final: bool) -> Self {
60 Self {
61 value,
62 source,
63 is_final,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct ValueTrace {
71 pub final_value: TracedValue,
73 pub history: Vec<TracedValue>,
75}
76
77impl ValueTrace {
78 pub fn new(mut history: Vec<TracedValue>) -> Option<Self> {
83 if history.is_empty() {
84 return None;
85 }
86
87 if let Some(last) = history.last_mut() {
89 last.is_final = true;
90 }
91
92 let final_value = history.last().cloned()?;
93
94 Some(Self {
95 final_value,
96 history,
97 })
98 }
99
100 pub fn was_overridden(&self) -> bool {
102 self.history.len() > 1
103 }
104
105 pub fn source_count(&self) -> usize {
107 self.history.len()
108 }
109}
110
111impl fmt::Display for ValueTrace {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 writeln!(
114 f,
115 "Final: {:?} (from {})",
116 self.final_value.value, self.final_value.source
117 )?;
118
119 if self.history.len() > 1 {
120 writeln!(f, "History:")?;
121 for val in &self.history {
122 let marker = if val.is_final { "→" } else { " " };
123 writeln!(f, " {} [{}] {:?}", marker, val.source, val.value)?;
124 }
125 }
126
127 Ok(())
128 }
129}
130
131#[derive(Debug)]
136pub struct TracedConfig<T> {
137 config: Config<T>,
138 traces: BTreeMap<String, ValueTrace>,
139}
140
141impl<T> TracedConfig<T> {
142 pub fn new(config: Config<T>, traces: BTreeMap<String, ValueTrace>) -> Self {
144 Self { config, traces }
145 }
146
147 pub fn value(&self) -> &T {
149 self.config.get()
150 }
151
152 pub fn config(&self) -> &Config<T> {
154 &self.config
155 }
156
157 pub fn into_inner(self) -> T {
159 self.config.into_inner()
160 }
161
162 pub fn into_config(self) -> Config<T> {
164 self.config
165 }
166
167 pub fn trace(&self, path: &str) -> Option<&ValueTrace> {
169 self.traces.get(path)
170 }
171
172 pub fn was_overridden(&self, path: &str) -> bool {
174 self.traces
175 .get(path)
176 .map(|t| t.history.len() > 1)
177 .unwrap_or(false)
178 }
179
180 pub fn traces(&self) -> impl Iterator<Item = (&str, &ValueTrace)> {
182 self.traces.iter().map(|(k, v)| (k.as_str(), v))
183 }
184
185 pub fn overridden_paths(&self) -> impl Iterator<Item = &str> {
187 self.traces
188 .iter()
189 .filter(|(_, t)| t.history.len() > 1)
190 .map(|(k, _)| k.as_str())
191 }
192
193 pub fn paths(&self) -> impl Iterator<Item = &str> {
195 self.traces.keys().map(|k| k.as_str())
196 }
197
198 pub fn trace_count(&self) -> usize {
200 self.traces.len()
201 }
202
203 pub fn trace_report(&self) -> String {
205 let mut report = String::new();
206
207 for (path, trace) in &self.traces {
208 report.push_str(&format!("{} = {:?}\n", path, trace.final_value.value));
209
210 for val in &trace.history {
211 let marker = if val.is_final { "✓" } else { "○" };
212 let override_note = if !val.is_final { " <- overridden" } else { "" };
213 report.push_str(&format!(
214 " {} [{}] {:?}{}\n",
215 marker, val.source, val.value, override_note
216 ));
217 }
218 report.push('\n');
219 }
220
221 report
222 }
223}
224
225impl<T> std::ops::Deref for TracedConfig<T> {
226 type Target = T;
227
228 fn deref(&self) -> &Self::Target {
229 self.config.get()
230 }
231}
232
233impl<T> AsRef<T> for TracedConfig<T> {
234 fn as_ref(&self) -> &T {
235 self.config.get()
236 }
237}
238
239#[derive(Debug, Default)]
243pub struct TraceBuilder {
244 values: BTreeMap<String, Vec<TracedValue>>,
247}
248
249impl TraceBuilder {
250 pub fn new() -> Self {
252 Self {
253 values: BTreeMap::new(),
254 }
255 }
256
257 pub fn add_value(&mut self, path: String, value: Value, source: SourceLocation) {
259 self.values
260 .entry(path)
261 .or_default()
262 .push(TracedValue::new(value, source, false));
263 }
264
265 pub fn build(self) -> BTreeMap<String, ValueTrace> {
267 self.values
268 .into_iter()
269 .filter_map(|(path, history)| ValueTrace::new(history).map(|trace| (path, trace)))
270 .collect()
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::config::Config;
278
279 #[test]
280 fn test_traced_value_new() {
281 let tv = TracedValue::new(
282 Value::String("localhost".to_string()),
283 SourceLocation::new("config.toml"),
284 false,
285 );
286
287 assert_eq!(tv.value.as_str(), Some("localhost"));
288 assert_eq!(tv.source.source, "config.toml");
289 assert!(!tv.is_final);
290 }
291
292 #[test]
293 fn test_value_trace_new() {
294 let history = vec![
295 TracedValue::new(
296 Value::String("default".to_string()),
297 SourceLocation::new("defaults"),
298 false,
299 ),
300 TracedValue::new(
301 Value::String("override".to_string()),
302 SourceLocation::new("config.toml"),
303 false,
304 ),
305 ];
306
307 let trace = ValueTrace::new(history).unwrap();
308
309 assert_eq!(trace.final_value.value.as_str(), Some("override"));
310 assert!(trace.final_value.is_final);
311 assert!(trace.was_overridden());
312 assert_eq!(trace.source_count(), 2);
313 }
314
315 #[test]
316 fn test_value_trace_single_source() {
317 let history = vec![TracedValue::new(
318 Value::Integer(8080),
319 SourceLocation::new("config.toml"),
320 false,
321 )];
322
323 let trace = ValueTrace::new(history).unwrap();
324
325 assert!(!trace.was_overridden());
326 assert_eq!(trace.source_count(), 1);
327 }
328
329 #[test]
330 fn test_value_trace_empty_history() {
331 let trace = ValueTrace::new(vec![]);
332 assert!(trace.is_none());
333 }
334
335 #[test]
336 fn test_value_trace_display() {
337 let history = vec![
338 TracedValue::new(
339 Value::String("localhost".to_string()),
340 SourceLocation::new("defaults"),
341 false,
342 ),
343 TracedValue::new(
344 Value::String("prod-db".to_string()),
345 SourceLocation::new("env:DB_HOST"),
346 true,
347 ),
348 ];
349
350 let trace = ValueTrace::new(history).unwrap();
351 let display = format!("{}", trace);
352
353 assert!(display.contains("Final:"));
354 assert!(display.contains("prod-db"));
355 assert!(display.contains("History:"));
356 }
357
358 #[test]
359 fn test_traced_config_basic() {
360 #[allow(dead_code)]
361 #[derive(Debug)]
362 struct TestConfig {
363 host: String,
364 port: i64,
365 }
366
367 let config = Config::new(TestConfig {
368 host: "localhost".to_string(),
369 port: 8080,
370 });
371
372 let mut traces = BTreeMap::new();
373 traces.insert(
374 "host".to_string(),
375 ValueTrace::new(vec![TracedValue::new(
376 Value::String("localhost".to_string()),
377 SourceLocation::new("config.toml"),
378 false,
379 )])
380 .unwrap(),
381 );
382
383 let traced = TracedConfig::new(config, traces);
384
385 assert_eq!(traced.value().host, "localhost");
386 assert_eq!(traced.trace_count(), 1);
387 assert!(traced.trace("host").is_some());
388 assert!(traced.trace("nonexistent").is_none());
389 }
390
391 #[test]
392 fn test_traced_config_was_overridden() {
393 #[allow(dead_code)]
394 #[derive(Debug)]
395 struct TestConfig {
396 value: String,
397 }
398
399 let config = Config::new(TestConfig {
400 value: "final".to_string(),
401 });
402
403 let mut traces = BTreeMap::new();
404
405 traces.insert(
407 "single".to_string(),
408 ValueTrace::new(vec![TracedValue::new(
409 Value::String("only".to_string()),
410 SourceLocation::new("defaults"),
411 false,
412 )])
413 .unwrap(),
414 );
415
416 traces.insert(
418 "overridden".to_string(),
419 ValueTrace::new(vec![
420 TracedValue::new(
421 Value::String("first".to_string()),
422 SourceLocation::new("defaults"),
423 false,
424 ),
425 TracedValue::new(
426 Value::String("second".to_string()),
427 SourceLocation::new("config.toml"),
428 false,
429 ),
430 ])
431 .unwrap(),
432 );
433
434 let traced = TracedConfig::new(config, traces);
435
436 assert!(!traced.was_overridden("single"));
437 assert!(traced.was_overridden("overridden"));
438 assert!(!traced.was_overridden("nonexistent"));
439 }
440
441 #[test]
442 fn test_traced_config_overridden_paths() {
443 #[derive(Debug)]
444 struct TestConfig;
445
446 let config = Config::new(TestConfig);
447
448 let mut traces = BTreeMap::new();
449
450 traces.insert(
451 "a".to_string(),
452 ValueTrace::new(vec![TracedValue::new(
453 Value::Integer(1),
454 SourceLocation::new("defaults"),
455 false,
456 )])
457 .unwrap(),
458 );
459
460 traces.insert(
461 "b".to_string(),
462 ValueTrace::new(vec![
463 TracedValue::new(Value::Integer(1), SourceLocation::new("defaults"), false),
464 TracedValue::new(Value::Integer(2), SourceLocation::new("file"), false),
465 ])
466 .unwrap(),
467 );
468
469 traces.insert(
470 "c".to_string(),
471 ValueTrace::new(vec![
472 TracedValue::new(Value::Integer(1), SourceLocation::new("defaults"), false),
473 TracedValue::new(Value::Integer(2), SourceLocation::new("file"), false),
474 TracedValue::new(Value::Integer(3), SourceLocation::new("env"), false),
475 ])
476 .unwrap(),
477 );
478
479 let traced = TracedConfig::new(config, traces);
480
481 let overridden: Vec<&str> = traced.overridden_paths().collect();
482 assert_eq!(overridden.len(), 2);
483 assert!(overridden.contains(&"b"));
484 assert!(overridden.contains(&"c"));
485 }
486
487 #[test]
488 fn test_traced_config_trace_report() {
489 #[derive(Debug)]
490 struct TestConfig;
491
492 let config = Config::new(TestConfig);
493
494 let mut traces = BTreeMap::new();
495
496 traces.insert(
497 "database.host".to_string(),
498 ValueTrace::new(vec![
499 TracedValue::new(
500 Value::String("localhost".to_string()),
501 SourceLocation::new("defaults"),
502 false,
503 ),
504 TracedValue::new(
505 Value::String("prod-db".to_string()),
506 SourceLocation::new("env:DB_HOST"),
507 false,
508 ),
509 ])
510 .unwrap(),
511 );
512
513 traces.insert(
514 "database.port".to_string(),
515 ValueTrace::new(vec![TracedValue::new(
516 Value::Integer(5432),
517 SourceLocation::new("config.toml"),
518 false,
519 )])
520 .unwrap(),
521 );
522
523 let traced = TracedConfig::new(config, traces);
524 let report = traced.trace_report();
525
526 assert!(report.contains("database.host"));
527 assert!(report.contains("prod-db"));
528 assert!(report.contains("<- overridden"));
529 assert!(report.contains("database.port"));
530 assert!(report.contains("5432"));
531 assert!(report.contains("✓"));
532 assert!(report.contains("○"));
533 }
534
535 #[test]
536 fn test_trace_builder() {
537 let mut builder = TraceBuilder::new();
538
539 builder.add_value(
540 "host".to_string(),
541 Value::String("localhost".to_string()),
542 SourceLocation::new("defaults"),
543 );
544 builder.add_value(
545 "host".to_string(),
546 Value::String("prod".to_string()),
547 SourceLocation::new("env"),
548 );
549 builder.add_value(
550 "port".to_string(),
551 Value::Integer(8080),
552 SourceLocation::new("defaults"),
553 );
554
555 let traces = builder.build();
556
557 assert_eq!(traces.len(), 2);
558 assert!(traces.get("host").unwrap().was_overridden());
559 assert!(!traces.get("port").unwrap().was_overridden());
560 }
561
562 #[test]
563 fn test_traced_config_deref() {
564 #[derive(Debug)]
565 struct TestConfig {
566 value: i32,
567 }
568
569 let config = Config::new(TestConfig { value: 42 });
570 let traced = TracedConfig::new(config, BTreeMap::new());
571
572 assert_eq!(traced.value, 42);
574
575 let r: &TestConfig = traced.as_ref();
577 assert_eq!(r.value, 42);
578 }
579
580 #[test]
581 fn test_traced_config_into_inner() {
582 #[derive(Debug, PartialEq)]
583 struct TestConfig {
584 value: i32,
585 }
586
587 let config = Config::new(TestConfig { value: 42 });
588 let traced = TracedConfig::new(config, BTreeMap::new());
589
590 let inner = traced.into_inner();
591 assert_eq!(inner, TestConfig { value: 42 });
592 }
593
594 #[test]
595 fn test_traced_config_into_config() {
596 #[derive(Debug)]
597 struct TestConfig {
598 value: i32,
599 }
600
601 let config = Config::new(TestConfig { value: 42 });
602 let traced = TracedConfig::new(config, BTreeMap::new());
603
604 let config = traced.into_config();
605 assert_eq!(config.get().value, 42);
606 }
607
608 #[test]
609 fn test_traced_config_paths() {
610 #[derive(Debug)]
611 struct TestConfig;
612
613 let config = Config::new(TestConfig);
614
615 let mut traces = BTreeMap::new();
616 traces.insert(
617 "a".to_string(),
618 ValueTrace::new(vec![TracedValue::new(
619 Value::Integer(1),
620 SourceLocation::new("test"),
621 false,
622 )])
623 .unwrap(),
624 );
625 traces.insert(
626 "b".to_string(),
627 ValueTrace::new(vec![TracedValue::new(
628 Value::Integer(2),
629 SourceLocation::new("test"),
630 false,
631 )])
632 .unwrap(),
633 );
634
635 let traced = TracedConfig::new(config, traces);
636
637 let paths: Vec<&str> = traced.paths().collect();
638 assert_eq!(paths.len(), 2);
639 assert!(paths.contains(&"a"));
640 assert!(paths.contains(&"b"));
641 }
642}