trail_config/
lib.rs

1use std::{collections::HashMap, error::Error, fs};
2use serde_yaml_bw::{Value, from_str};
3use strfmt::strfmt;
4
5#[derive(Debug, Clone)]
6pub struct Config {
7    content: Value,
8    filename: String,
9    separator: String,
10    environment: Option<String>
11}
12
13impl Default for Config {
14    fn default() -> Self {
15        Self::new("config.yaml", "/", None).unwrap()
16    }
17}
18
19impl Config {
20    pub fn new(filename: &str, sep: &str, env: Option<&str>) -> Result<Config, Box<dyn Error>> {
21        let (file, env) = Self::get_file(filename, env);
22
23        match Self::load(&file) {
24            Ok(yaml) => Ok(Config {
25                content: yaml,
26                filename: file,
27                separator: sep.to_string(),
28                environment: env
29            }),
30            Err(e) => Err(e)
31        }
32    }
33
34    pub fn environment(&self) -> Option<&str> {
35        match &self.environment {
36            Some(v) => Some(v),
37            None => None
38        }
39    }
40
41    pub fn get_filename(&self) -> &str {
42        &self.filename
43    }
44
45    pub fn get(&self, path: &str) -> Option<Value> {
46        Self::get_leaf(&self.content, path, &self.separator)
47    }
48
49    pub fn str(&self, path: &str) -> String {
50        let content = Self::get_leaf(&self.content, path, &self.separator);
51
52        match content {
53            Some(v) => Self::to_string(&v),
54            None => String::new()
55        }
56    }
57
58    pub fn list(&self, path: &str) -> Vec<String> {
59        let content = Self::get_leaf(&self.content, path, &self.separator);
60        
61        match content {
62            Some(v) => Self::to_list(&v),
63            None => vec![]
64        }
65    }
66
67    pub fn fmt(&self, format: &str, path: &str) -> String {
68        let mut content = &self.content.clone();
69        let mut parts = path.split(&self.separator).collect::<Vec<&str>>();
70        let last = parts.pop();
71    
72        for item in parts.iter() {
73            match content.get(item) {
74                Some(v) => { content = v; },
75                None => return String::new()
76            }
77        }
78
79        match last {
80            Some(v) => {
81                let attributes = v.split('+').collect::<Vec<&str>>();
82                let mut fmt = format.to_string();
83                let mut vars = HashMap::new();
84
85                for item in attributes.iter() {
86                    match content.get(item) {
87                        Some(v) => {
88                            fmt = fmt.replacen("{}", &format!("{{{}}}", item), 1);
89                            vars.insert(item.to_string(), Self::to_string(v));
90                        },
91                        None => return String::new()
92                    }
93                }
94
95                return match strfmt(&fmt, &vars) {
96                    Ok(r) => r,
97                    Err(_) => String::new()
98                };
99            },
100            None => String::new()
101        }
102    }
103
104    pub fn load_yaml(yaml: &str, sep: &str) -> Result<Config, Box<dyn Error>> {
105        let parsed = from_str(&yaml)?;
106
107        Ok(Config {
108            content: parsed,
109            filename: String::new(),
110            separator: sep.to_string(),
111            environment: None
112        })
113    }
114
115    fn get_leaf(mut content: &Value, path: &str, separator: &str) -> Option<Value> {
116        let parts = path.split(separator).collect::<Vec<&str>>();
117    
118        for item in parts.iter() {
119            match content.get(item) {
120                Some(v) => { content = v; },
121                None => return None
122            }
123        }
124
125        return Some(content.clone());
126    }
127
128    fn get_file(filename: &str, env: Option<&str>) -> (String, Option<String>) {
129        match env {
130            Some(v) => {
131                let mut vars = HashMap::new();
132                vars.insert(String::from("env"), v);
133                (strfmt(filename, &vars).unwrap(), Some(v.to_string()))
134            },
135            None => (String::from(filename), None)
136        }
137    }
138
139    fn load(filename: &str) -> Result<Value, Box<dyn Error>> {
140        let yaml = fs::read_to_string(filename)?;
141        let parsed = from_str(&yaml)?;
142        
143        Ok(parsed)
144    }
145    
146    fn to_string(value: &Value) -> String {
147        match value {
148            Value::String(v, _) => v.to_string(),
149            Value::Number(v, _) => v.to_string(),
150            Value::Bool(v, _) => v.to_string(),
151            _ => String::new()
152        }
153    }
154
155    fn to_list(value: &Value) -> Vec<String> {
156        match value {
157            Value::Sequence(v) => v.iter().map(Self::to_string).collect::<Vec<String>>(),
158            _ => vec![]
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::{from_str, Config, Value};
166    use serde_yaml_bw::Number;
167
168    const YAML: &str = "
169db:
170    redis:
171        server: 127.0.0.1
172        port: 6379
173        key_expiry: 3600
174    sql:
175        driver: SQL Server
176        server: 127.0.0.1
177        database: my_db
178        username: user
179        password: Pa$$w0rd!
180sources:
181    - one
182    - two
183    - three
184";
185
186    #[test]
187    fn fmt_test()  {
188        let parsed: Config = Config::load_yaml(YAML, "/").unwrap();
189        let formatted = parsed.fmt("{}:{}", "db/sql/database+username");
190
191        assert_eq!(formatted, String::from("my_db:user"));
192    }
193
194    #[test]
195    fn get_leaf_test()  {
196        let parsed: Value = from_str(YAML).unwrap();
197        let value1 = Config::get_leaf(&parsed, "db/redis/port", "/");
198        let value2 = Config::get_leaf(&parsed, "db/redis/username", "/");
199        
200        assert_eq!(value1, Some(Value::Number(Number::from(6379), None)));
201        assert_eq!(value2, None);
202    }
203
204    #[test]
205    fn get_file_test() {
206        let (file, env) = Config::get_file("config_{env}.yaml", Some("dev"));
207
208        assert_eq!(env,  Some(String::from("dev")));
209        assert_eq!(file, "config_dev.yaml");
210    }
211
212    #[test]
213    fn to_string_test() {
214        let parsed: Value = from_str(YAML).unwrap();
215        let value = Config::get_leaf(&parsed, "db/redis/port", "/").unwrap();
216        let str_value = Config::to_string(&value);
217
218        assert_eq!(str_value, "6379");
219    }
220
221    #[test]
222    fn to_list_test()  {
223        let parsed: Value = from_str(YAML).unwrap();
224        let value = Config::get_leaf(&parsed, "sources", "/").unwrap();
225        let list = Config::to_list(&value);
226
227        let mut vec = Vec::new();        
228        vec.push(String::from("one"));
229        vec.push(String::from("two"));
230        vec.push(String::from("three"));
231
232        assert_eq!(list, vec);
233    }
234}