Skip to main content

agp_config/
provider.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
21// Define a trait for reading YAML content from different sources
22pub 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    /// New ConfigResolver
33    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    /// Register a new provider
41    pub fn register(&mut self, name: String, provider: Box<dyn ConfigProvider>) {
42        self.providers.insert(name, provider);
43    }
44
45    /// Resolve given the string
46    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    /// Resolve the values in the given YAML content
53    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        // set an env variable to test
112        unsafe {
113            // ignore clippy warning for this line
114            #[allow(clippy::disallowed_methods)]
115            std::env::set_var("HOME", "/home/user");
116        }
117
118        // Declare a resolver
119        let resolver = ConfigResolver::new();
120
121        // Test resolving an env value
122        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        // Test resolving a file value
127
128        // open file
129        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        // Test resolving a value with an unknown provider
137        let mut value = Value::String("${unknown:HOME}".to_string());
138        assert!(resolver.resolve(&mut value).is_err());
139
140        // Test resolving a value with an unknown variable
141        let mut value = Value::String("${env:UNKNOWN}".to_string());
142        assert!(resolver.resolve(&mut value).is_err());
143
144        // Test resolving a value with an invalid format
145        let mut value = Value::String("${env:HOME}${env:HOME}".to_string());
146        assert!(resolver.resolve(&mut value).is_err());
147
148        // Test resolving a sequence
149        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        // Test resolving a mapping
159        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}