1use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ConfigSource {
13 Default,
15 UserConfig,
17 ProjectConfig,
19 DotEnv,
21 Profile,
23 Environment,
25 CommandLine,
27}
28
29impl ConfigSource {
30 pub fn precedence(&self) -> u8 {
32 match self {
33 ConfigSource::Default => 0,
34 ConfigSource::UserConfig => 1,
35 ConfigSource::ProjectConfig => 2,
36 ConfigSource::DotEnv => 3,
37 ConfigSource::Profile => 4,
38 ConfigSource::Environment => 5,
39 ConfigSource::CommandLine => 6,
40 }
41 }
42
43 pub fn display_name(&self) -> &'static str {
45 match self {
46 ConfigSource::Default => "default",
47 ConfigSource::UserConfig => "user config",
48 ConfigSource::ProjectConfig => "project config",
49 ConfigSource::DotEnv => ".env file",
50 ConfigSource::Profile => "profile",
51 ConfigSource::Environment => "environment",
52 ConfigSource::CommandLine => "command line",
53 }
54 }
55}
56
57impl fmt::Display for ConfigSource {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "{}", self.display_name())
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum ConfigValueSource {
67 Default,
69 UserConfig(PathBuf),
71 ProjectConfig(PathBuf),
73 EnvVar(String),
75}
76
77impl ConfigValueSource {
78 pub fn label(&self) -> String {
80 match self {
81 ConfigValueSource::Default => "default".to_string(),
82 ConfigValueSource::UserConfig(path) => {
83 format!("user:{}", path.display())
84 }
85 ConfigValueSource::ProjectConfig(path) => {
86 format!("project:{}", path.display())
87 }
88 ConfigValueSource::EnvVar(name) => format!("env:{}", name),
89 }
90 }
91}
92
93impl fmt::Display for ConfigValueSource {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(f, "{}", self.label())
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct Sourced<T> {
102 pub value: T,
104 pub source: ConfigSource,
106 pub env_var: Option<String>,
108}
109
110impl<T> Sourced<T> {
111 pub fn new(value: T, source: ConfigSource) -> Self {
113 Self {
114 value,
115 source,
116 env_var: None,
117 }
118 }
119
120 pub fn from_env(value: T, var_name: impl Into<String>) -> Self {
122 Self {
123 value,
124 source: ConfigSource::Environment,
125 env_var: Some(var_name.into()),
126 }
127 }
128
129 pub fn default_value(value: T) -> Self {
131 Self::new(value, ConfigSource::Default)
132 }
133
134 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Sourced<U> {
136 Sourced {
137 value: f(self.value),
138 source: self.source,
139 env_var: self.env_var,
140 }
141 }
142
143 pub fn merge(self, other: Self) -> Self {
145 if other.source.precedence() >= self.source.precedence() {
146 other
147 } else {
148 self
149 }
150 }
151}
152
153impl<T: Default> Default for Sourced<T> {
154 fn default() -> Self {
155 Self::default_value(T::default())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_source_precedence() {
165 assert!(ConfigSource::Environment.precedence() > ConfigSource::UserConfig.precedence());
166 assert!(ConfigSource::CommandLine.precedence() > ConfigSource::Environment.precedence());
167 assert!(ConfigSource::Default.precedence() < ConfigSource::ProjectConfig.precedence());
168 }
169
170 #[test]
171 fn test_sourced_merge() {
172 let default = Sourced::new(10, ConfigSource::Default);
173 let env = Sourced::new(20, ConfigSource::Environment);
174
175 let merged = default.merge(env);
176 assert_eq!(merged.value, 20);
177 assert_eq!(merged.source, ConfigSource::Environment);
178 }
179
180 #[test]
181 fn test_sourced_map() {
182 let sourced = Sourced::from_env("42".to_string(), "MY_VAR");
183 let mapped = sourced.map(|s| s.parse::<i32>().unwrap());
184
185 assert_eq!(mapped.value, 42);
186 assert_eq!(mapped.source, ConfigSource::Environment);
187 assert_eq!(mapped.env_var.as_deref(), Some("MY_VAR"));
188 }
189
190 #[test]
195 fn test_config_source_precedence_order() {
196 let sources = [
197 ConfigSource::Default,
198 ConfigSource::UserConfig,
199 ConfigSource::ProjectConfig,
200 ConfigSource::DotEnv,
201 ConfigSource::Profile,
202 ConfigSource::Environment,
203 ConfigSource::CommandLine,
204 ];
205
206 for i in 0..sources.len() - 1 {
207 assert!(
208 sources[i].precedence() < sources[i + 1].precedence(),
209 "{:?} should have lower precedence than {:?}",
210 sources[i],
211 sources[i + 1]
212 );
213 }
214 }
215
216 #[test]
217 fn test_config_source_display_name_all_variants() {
218 assert_eq!(ConfigSource::Default.display_name(), "default");
219 assert_eq!(ConfigSource::UserConfig.display_name(), "user config");
220 assert_eq!(ConfigSource::ProjectConfig.display_name(), "project config");
221 assert_eq!(ConfigSource::DotEnv.display_name(), ".env file");
222 assert_eq!(ConfigSource::Profile.display_name(), "profile");
223 assert_eq!(ConfigSource::Environment.display_name(), "environment");
224 assert_eq!(ConfigSource::CommandLine.display_name(), "command line");
225 }
226
227 #[test]
228 fn test_config_source_display_trait() {
229 assert_eq!(format!("{}", ConfigSource::Default), "default");
230 assert_eq!(format!("{}", ConfigSource::CommandLine), "command line");
231 }
232
233 #[test]
234 fn test_config_source_serialization() {
235 let source = ConfigSource::Environment;
236 let json = serde_json::to_string(&source).unwrap();
237 assert_eq!(json, "\"environment\"");
238
239 let deserialized: ConfigSource = serde_json::from_str(&json).unwrap();
240 assert_eq!(deserialized, ConfigSource::Environment);
241 }
242
243 #[test]
244 fn test_config_source_all_variants_serialize_roundtrip() {
245 let sources = [
246 ConfigSource::Default,
247 ConfigSource::UserConfig,
248 ConfigSource::ProjectConfig,
249 ConfigSource::DotEnv,
250 ConfigSource::Profile,
251 ConfigSource::Environment,
252 ConfigSource::CommandLine,
253 ];
254
255 for source in sources {
256 let json = serde_json::to_string(&source).unwrap();
257 let restored: ConfigSource = serde_json::from_str(&json).unwrap();
258 assert_eq!(restored, source);
259 }
260 }
261
262 #[test]
263 fn test_config_source_copy_trait() {
264 let source = ConfigSource::Environment;
265 let copied = source;
266 assert_eq!(source, copied);
267 }
268
269 #[test]
274 fn test_config_value_source_default_label() {
275 let source = ConfigValueSource::Default;
276 assert_eq!(source.label(), "default");
277 }
278
279 #[test]
280 fn test_config_value_source_user_config_label() {
281 let source =
282 ConfigValueSource::UserConfig(PathBuf::from("/home/user/.config/rch/config.toml"));
283 assert!(source.label().starts_with("user:"));
284 assert!(source.label().contains("config.toml"));
285 }
286
287 #[test]
288 fn test_config_value_source_project_config_label() {
289 let source = ConfigValueSource::ProjectConfig(PathBuf::from("/project/.rch/config.toml"));
290 assert!(source.label().starts_with("project:"));
291 assert!(source.label().contains("config.toml"));
292 }
293
294 #[test]
295 fn test_config_value_source_env_var_label() {
296 let source = ConfigValueSource::EnvVar("RCH_WORKERS".to_string());
297 assert_eq!(source.label(), "env:RCH_WORKERS");
298 }
299
300 #[test]
301 fn test_config_value_source_display_trait() {
302 let source = ConfigValueSource::EnvVar("MY_VAR".to_string());
303 assert_eq!(format!("{}", source), "env:MY_VAR");
304 }
305
306 #[test]
307 fn test_config_value_source_serialization_default() {
308 let source = ConfigValueSource::Default;
309 let json = serde_json::to_string(&source).unwrap();
310 let restored: ConfigValueSource = serde_json::from_str(&json).unwrap();
311 assert_eq!(restored, ConfigValueSource::Default);
312 }
313
314 #[test]
315 fn test_config_value_source_serialization_env_var() {
316 let source = ConfigValueSource::EnvVar("TEST_VAR".to_string());
317 let json = serde_json::to_string(&source).unwrap();
318 let restored: ConfigValueSource = serde_json::from_str(&json).unwrap();
319 assert_eq!(restored, source);
320 }
321
322 #[test]
323 fn test_config_value_source_serialization_user_config() {
324 let source = ConfigValueSource::UserConfig(PathBuf::from("/test/path"));
325 let json = serde_json::to_string(&source).unwrap();
326 let restored: ConfigValueSource = serde_json::from_str(&json).unwrap();
327 assert_eq!(restored, source);
328 }
329
330 #[test]
331 fn test_config_value_source_equality() {
332 let source1 = ConfigValueSource::EnvVar("VAR1".to_string());
333 let source2 = ConfigValueSource::EnvVar("VAR1".to_string());
334 let source3 = ConfigValueSource::EnvVar("VAR2".to_string());
335
336 assert_eq!(source1, source2);
337 assert_ne!(source1, source3);
338 }
339
340 #[test]
345 fn test_sourced_new() {
346 let sourced = Sourced::new(42, ConfigSource::UserConfig);
347 assert_eq!(sourced.value, 42);
348 assert_eq!(sourced.source, ConfigSource::UserConfig);
349 assert!(sourced.env_var.is_none());
350 }
351
352 #[test]
353 fn test_sourced_from_env() {
354 let sourced = Sourced::from_env("value".to_string(), "MY_ENV_VAR");
355 assert_eq!(sourced.value, "value");
356 assert_eq!(sourced.source, ConfigSource::Environment);
357 assert_eq!(sourced.env_var.as_deref(), Some("MY_ENV_VAR"));
358 }
359
360 #[test]
361 fn test_sourced_default_value() {
362 let sourced = Sourced::default_value(100);
363 assert_eq!(sourced.value, 100);
364 assert_eq!(sourced.source, ConfigSource::Default);
365 assert!(sourced.env_var.is_none());
366 }
367
368 #[test]
369 fn test_sourced_default_trait() {
370 let sourced: Sourced<i32> = Sourced::default();
371 assert_eq!(sourced.value, 0);
372 assert_eq!(sourced.source, ConfigSource::Default);
373 }
374
375 #[test]
376 fn test_sourced_default_trait_string() {
377 let sourced: Sourced<String> = Sourced::default();
378 assert_eq!(sourced.value, "");
379 assert_eq!(sourced.source, ConfigSource::Default);
380 }
381
382 #[test]
383 fn test_sourced_merge_higher_precedence_wins() {
384 let lower = Sourced::new(10, ConfigSource::Default);
385 let higher = Sourced::new(20, ConfigSource::CommandLine);
386
387 let result = lower.merge(higher);
388 assert_eq!(result.value, 20);
389 assert_eq!(result.source, ConfigSource::CommandLine);
390 }
391
392 #[test]
393 fn test_sourced_merge_equal_precedence_takes_other() {
394 let first = Sourced::new(10, ConfigSource::Environment);
395 let second = Sourced::new(20, ConfigSource::Environment);
396
397 let result = first.merge(second);
398 assert_eq!(result.value, 20);
399 }
400
401 #[test]
402 fn test_sourced_merge_lower_precedence_keeps_self() {
403 let higher = Sourced::new(10, ConfigSource::CommandLine);
404 let lower = Sourced::new(20, ConfigSource::Default);
405
406 let result = higher.merge(lower);
407 assert_eq!(result.value, 10);
408 assert_eq!(result.source, ConfigSource::CommandLine);
409 }
410
411 #[test]
412 fn test_sourced_merge_preserves_env_var() {
413 let default = Sourced::new(10, ConfigSource::Default);
414 let env = Sourced::from_env(20, "MY_VAR");
415
416 let result = default.merge(env);
417 assert_eq!(result.env_var.as_deref(), Some("MY_VAR"));
418 }
419
420 #[test]
421 fn test_sourced_map_preserves_source() {
422 let sourced = Sourced::new(42, ConfigSource::ProjectConfig);
423 let mapped = sourced.map(|v| v.to_string());
424
425 assert_eq!(mapped.value, "42");
426 assert_eq!(mapped.source, ConfigSource::ProjectConfig);
427 }
428
429 #[test]
430 fn test_sourced_map_preserves_env_var() {
431 let sourced = Sourced::from_env(42, "NUMBER");
432 let mapped = sourced.map(|v| v * 2);
433
434 assert_eq!(mapped.value, 84);
435 assert_eq!(mapped.env_var.as_deref(), Some("NUMBER"));
436 }
437
438 #[test]
439 fn test_sourced_map_chain() {
440 let sourced = Sourced::new("hello".to_string(), ConfigSource::UserConfig);
441 let mapped = sourced.map(|s| s.len()).map(|n| n * 2);
442
443 assert_eq!(mapped.value, 10);
444 assert_eq!(mapped.source, ConfigSource::UserConfig);
445 }
446
447 #[test]
448 fn test_sourced_with_complex_type() {
449 #[derive(Debug, Clone, PartialEq)]
450 struct Config {
451 timeout: u64,
452 retries: u32,
453 }
454
455 let config = Config {
456 timeout: 30,
457 retries: 3,
458 };
459 let sourced = Sourced::new(config.clone(), ConfigSource::Environment);
460
461 assert_eq!(sourced.value.timeout, 30);
462 assert_eq!(sourced.value.retries, 3);
463
464 let mapped = sourced.map(|c| c.timeout);
465 assert_eq!(mapped.value, 30);
466 }
467}