1use std::collections::HashMap;
5use std::str;
6
7use serde_yaml::Value;
8use thiserror::Error;
9
10mod env;
11mod file;
12
13#[derive(Error, Debug)]
14pub enum ProviderError {
15 #[error("not found")]
16 NotFound,
17 #[error("unknown error")]
18 Unknown,
19}
20
21pub trait ConfigProvider {
23 fn load(&self, var: &str) -> Result<String, ProviderError>;
24}
25
26#[derive(Default)]
27pub struct ConfigResolver {
28 providers: HashMap<String, Box<dyn ConfigProvider>>,
29}
30
31impl ConfigResolver {
32 pub fn new() -> Self {
34 let mut providers = HashMap::<String, Box<dyn ConfigProvider>>::new();
35 providers.insert("env".to_string(), Box::new(env::EnvConfigProvider));
36 providers.insert("file".to_string(), Box::new(file::FileConfigProvider));
37 ConfigResolver { providers }
38 }
39
40 pub fn register(&mut self, name: String, provider: Box<dyn ConfigProvider>) {
42 self.providers.insert(name, provider);
43 }
44
45 pub fn resolve_str(&self, value: &str) -> Result<String, ProviderError> {
47 let mut value = Value::String(value.to_string());
48 self.resolve(&mut value)?;
49 Ok(value.as_str().unwrap().to_string())
50 }
51
52 pub fn resolve(&self, value: &mut Value) -> Result<(), ProviderError> {
54 match value {
55 Value::String(s) => {
56 if let Some(provider_and_ref) =
57 s.strip_prefix("${").and_then(|s| s.strip_suffix('}'))
58 {
59 let mut parts = provider_and_ref.splitn(2, ':');
60 let provider = parts.next();
61 let var = parts.next();
62
63 match (provider, var) {
64 (Some(provider), Some(var)) => {
65 if let Some(provider) = self.providers.get(provider) {
66 *s = provider.load(var)?;
67 Ok(())
68 } else {
69 Err(ProviderError::NotFound)
70 }
71 }
72 _ => Err(ProviderError::NotFound),
73 }
74 } else {
75 Ok(())
76 }
77 }
78 Value::Sequence(seq) => {
79 for item in seq {
80 self.resolve(item)?;
81 }
82
83 Ok(())
84 }
85 Value::Mapping(map) => {
86 for (_, v) in map {
87 self.resolve(v)?;
88 }
89
90 Ok(())
91 }
92 _ => Ok(()),
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use serde_yaml::Value;
101 use tracing::debug;
102 use tracing_test::traced_test;
103
104 #[test]
105 #[traced_test]
106 fn test_resolve() {
107 let testdata_path: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata");
108
109 debug!("testdata_path: {}", testdata_path);
110
111 unsafe {
113 #[allow(clippy::disallowed_methods)]
115 std::env::set_var("HOME", "/home/user");
116 }
117
118 let resolver = ConfigResolver::new();
120
121 let mut value = Value::String("${env:HOME}".to_string());
123 assert!(resolver.resolve(&mut value).is_ok());
124 assert!(value.as_str() == Some("/home/user"));
125
126 let path = format!("{}/testfile", testdata_path);
130 let file_str = std::fs::read_to_string(&path).unwrap();
131
132 let mut value = Value::String(format!("${{file:{}}}", path).to_string());
133 assert!(resolver.resolve(&mut value).is_ok());
134 assert!(value.as_str() == Some(file_str.as_str()));
135
136 let mut value = Value::String("${unknown:HOME}".to_string());
138 assert!(resolver.resolve(&mut value).is_err());
139
140 let mut value = Value::String("${env:UNKNOWN}".to_string());
142 assert!(resolver.resolve(&mut value).is_err());
143
144 let mut value = Value::String("${env:HOME}${env:HOME}".to_string());
146 assert!(resolver.resolve(&mut value).is_err());
147
148 let mut value = Value::Sequence(vec![
150 Value::String("${env:HOME}".to_string()),
151 Value::String(format!("${{file:{}}}", path).to_string()),
152 ]);
153 assert!(resolver.resolve(&mut value).is_ok());
154 assert!(value.as_sequence().unwrap().iter().all(|v| v.is_string()));
155 assert!(value[0].as_str().unwrap() == "/home/user");
156 assert!(value[1].as_str().unwrap() == file_str);
157
158 let mut map = serde_yaml::Mapping::new();
160 map.insert(
161 Value::String("env".to_string()),
162 Value::String("${env:HOME}".to_string()),
163 );
164 map.insert(
165 Value::String("file".to_string()),
166 Value::String(format!("${{file:{}}}", path).to_string()),
167 );
168
169 let mut value = Value::Mapping(map);
170 assert!(resolver.resolve(&mut value).is_ok());
171
172 let map = value.as_mapping().unwrap();
173 assert!(map.iter().all(|(k, v)| k.is_string() && v.is_string()));
174 assert!(map[&Value::String("env".to_string())].as_str().unwrap() == "/home/user");
175 assert!(map[&Value::String("file".to_string())].as_str().unwrap() == file_str);
176 }
177}