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}