• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dariusbakunas / cogrs / 13486377277

23 Feb 2025 07:54PM UTC coverage: 36.597% (-0.4%) from 36.957%
13486377277

push

github

dariusbakunas
refactor: extract init code from adhoc cli to cli trait

0 of 29 new or added lines in 2 files covered. (0.0%)

95 existing lines in 4 files now uncovered.

714 of 1951 relevant lines covered (36.6%)

1.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.89
/cogrs-core/src/config/manager.rs
1
use anyhow::{bail, Result};
2
use indexmap::IndexMap;
3
use log::warn;
4
use minijinja::Environment;
5
use once_cell::sync::Lazy;
6
use serde::de::DeserializeOwned;
7
use serde_yaml::Value;
8
use std::path::PathBuf;
9
use tokio::sync::Mutex;
10

11
pub struct ConfigManager {
12
    base_defs: IndexMap<String, Value>,
13
}
14

15
#[derive(Debug, PartialEq)]
16
pub enum ConfigOrigin {
17
    Env,
18
    Default,
19
}
20

21
impl ConfigManager {
22
    fn new() -> Self {
4✔
23
        ConfigManager {
24
            base_defs: IndexMap::new(),
4✔
25
        }
26
    }
27

28
    pub fn instance() -> &'static Mutex<ConfigManager> {
×
29
        &CONFIG_LOADER
×
30
    }
31

32
    pub fn init(&mut self) -> Result<()> {
×
33
        self.base_defs = self.read_config_yaml_file()?;
×
34
        let env = Environment::new();
×
35
        let template_context = self.load_template_context();
×
36
        let rendered_config = self.render_with_jinja(&env, &template_context)?;
×
37
        self.base_defs = rendered_config;
×
38
        Ok(())
×
39
    }
40

41
    fn render_with_jinja(
1✔
42
        &self,
43
        env: &Environment,
44
        context_data: &std::collections::HashMap<String, String>,
45
    ) -> Result<IndexMap<String, Value>> {
46
        let mut rendered_map = IndexMap::new();
1✔
47

48
        for (key, value) in self.base_defs.iter() {
4✔
49
            let rendered_value = self.recursively_render_value(&value, env, context_data)?;
2✔
50
            rendered_map.insert(key.clone(), rendered_value);
2✔
51
        }
52

53
        Ok(rendered_map)
1✔
54
    }
55

56
    fn recursively_render_value(
1✔
57
        &self,
58
        value: &Value,
59
        env: &Environment,
60
        context_data: &std::collections::HashMap<String, String>,
61
    ) -> Result<Value> {
62
        match value {
1✔
63
            Value::String(s) => {
1✔
64
                if s.contains("{{") {
2✔
65
                    // Create an inline template and render it
66
                    let template = env.template_from_str(s).map_err(|err| {
2✔
UNCOV
67
                        anyhow::anyhow!("Failed to parse Jinja template: {}", err)
×
68
                    })?;
69

70
                    let rendered = template.render(context_data).map_err(|err| {
2✔
UNCOV
71
                        anyhow::anyhow!("Failed to render Jinja template: {}", err)
×
72
                    })?;
73

74
                    Ok(Value::String(rendered))
1✔
75
                } else {
76
                    // If string doesn't contain Jinja syntax, leave it as-is
77
                    Ok(Value::String(s.clone()))
1✔
78
                }
79
            }
80
            Value::Mapping(map) => {
1✔
81
                // Convert the Mapping into a newly rendered Mapping
82
                let mut rendered_map = serde_yaml::Mapping::new();
1✔
83
                for (key, val) in map {
4✔
84
                    let rendered_key = self.recursively_render_value(key, env, context_data)?;
1✔
85
                    let rendered_val = self.recursively_render_value(val, env, context_data)?;
2✔
86
                    rendered_map.insert(rendered_key, rendered_val);
2✔
87
                }
88
                Ok(Value::Mapping(rendered_map))
1✔
89
            }
90
            Value::Sequence(seq) => {
1✔
91
                // Recursively process sequences
92
                let rendered_seq = seq
3✔
93
                    .iter()
94
                    .map(|val| self.recursively_render_value(val, env, context_data))
3✔
95
                    .collect::<Result<Vec<Value>>>()?;
96
                Ok(Value::Sequence(rendered_seq))
1✔
97
            }
98
            _ => {
99
                // Leave other types unmodified
100
                Ok(value.clone())
1✔
101
            }
102
        }
103
    }
104

105
    fn load_template_context(&self) -> std::collections::HashMap<String, String> {
1✔
106
        let mut context = std::collections::HashMap::new();
1✔
107

108
        for key in self.base_defs.keys() {
4✔
109
            if let Ok(Some((value, _))) = self.get_config_value::<String>(key) {
2✔
110
                context.insert(key.clone(), value);
2✔
111
            } else if let Ok(Some((value, _))) = self.get_config_value::<usize>(key) {
4✔
112
                // Handle numeric types (converted to strings for Jinja)
113
                context.insert(key.clone(), value.to_string());
2✔
114
            } else if let Ok(Some((value, _))) = self.get_config_value::<bool>(key) {
4✔
115
                context.insert(key.clone(), value.to_string());
4✔
116
            }
117
        }
118

119
        context
2✔
120
    }
121

UNCOV
122
    fn read_config_yaml_file(&self) -> Result<IndexMap<String, Value>> {
×
123
        let yaml_content = include_str!("base.yaml");
×
124
        let value: Value = serde_yaml::from_str(&yaml_content)?;
×
125

UNCOV
126
        let config_map = value
×
127
            .as_mapping()
128
            .cloned()
UNCOV
129
            .ok_or_else(|| anyhow::anyhow!("YAML root is not a mapping"))?
×
130
            .into_iter()
UNCOV
131
            .map(|(key, value)| {
×
132
                let key_str = key
×
133
                    .as_str()
UNCOV
134
                    .ok_or_else(|| anyhow::anyhow!("YAML key is not a string"))?
×
135
                    .to_string();
UNCOV
136
                Ok((key_str, value))
×
137
            })
138
            .collect::<Result<IndexMap<String, Value>>>()?;
139

UNCOV
140
        Ok(config_map)
×
141
    }
142

143
    pub fn get_config_value<T: DeserializeOwned>(
6✔
144
        &self,
145
        key: &str,
146
    ) -> Result<Option<(T, ConfigOrigin)>> {
147
        // Get the value from the base definitions
148
        let value = match self.base_defs.get(key) {
7✔
149
            Some(value) => value,
6✔
UNCOV
150
            None => {
×
151
                warn!("Config key {} not found", key);
2✔
152
                return Ok(None);
1✔
153
            }
154
        };
155

156
        // Ensure the value is a mapping
157
        let mapping = match value {
6✔
158
            Value::Mapping(map) => map,
6✔
UNCOV
159
            _ => {
×
160
                warn!("Config key {} is not a mapping", key);
×
161
                return Ok(None);
×
162
            }
163
        };
164

165
        // Check for "env" overrides
166
        if let Some(Value::Sequence(env_list)) = mapping.get("env") {
11✔
167
            for item in env_list {
2✔
168
                if let Value::Mapping(item_map) = item {
2✔
169
                    if let Some(Value::String(env_key)) = item_map.get("name") {
2✔
170
                        if let Ok(env_value) = std::env::var(env_key) {
2✔
171
                            return parse_config_value::<T>(
1✔
172
                                Value::String(env_value),
1✔
173
                                ConfigOrigin::Env,
1✔
UNCOV
174
                                key,
×
175
                                mapping
2✔
UNCOV
176
                                    .get("type")
×
177
                                    .filter(|v| v.is_string())
×
178
                                    .and_then(|v| v.as_str().map(|s| s.to_string())),
×
179
                            );
180
                        }
181
                    }
182
                }
183
            }
184
        }
185

186
        // Fall back to the "default" value
187
        if let Some(default_value) = mapping.get("default") {
9✔
188
            return parse_config_value::<T>(
3✔
189
                default_value.clone(),
4✔
190
                ConfigOrigin::Default,
4✔
UNCOV
191
                key,
×
192
                mapping
7✔
UNCOV
193
                    .get("type")
×
194
                    .filter(|v| v.is_string())
×
195
                    .and_then(|v| v.as_str().map(|s| s.to_string())),
×
196
            );
197
        }
198

199
        // If no valid value is found
200
        warn!("Config key {} has no valid value", key);
14✔
201
        Ok(None)
7✔
202
    }
203
}
204

205
// Utility function for deserialization and error handling
206
fn parse_config_value<T: DeserializeOwned>(
4✔
207
    value: Value,
208
    origin: ConfigOrigin,
209
    key: &str,
210
    value_type: Option<String>,
211
) -> Result<Option<(T, ConfigOrigin)>> {
212
    if value_type.as_deref() == Some("path") {
9✔
UNCOV
213
        if let Value::String(path_str) = &value {
×
214
            let expanded_paths: Result<Vec<String>> = path_str
×
215
                .split(':') // Split paths by colon
UNCOV
216
                .map(|path| {
×
217
                    if path.starts_with('~') {
×
218
                        // Expand '~' to the user's home directory
UNCOV
219
                        if let Some(home_dir) = dirs::home_dir() {
×
220
                            Ok(path.replacen('~', home_dir.to_str().unwrap_or_default(), 1))
×
221
                        } else {
UNCOV
222
                            bail!("Failed to expand '~/': Home directory could not be determined.");
×
223
                        }
224
                    } else {
UNCOV
225
                        Ok(path.to_string()) // Path does not start with '~', leave it as is
×
226
                    }
227
                })
228
                .collect();
229

230
            // Join expanded paths back into a colon-separated string
UNCOV
231
            let expanded_path_str = expanded_paths?.join(":");
×
232
            return Ok(Some((
×
233
                serde_yaml::from_value::<T>(Value::String(expanded_path_str))?, // Deserialize expanded PathBuf
×
234
                origin,
×
235
            )));
236
        }
237
    }
238

239
    match serde_yaml::from_value::<T>(value) {
9✔
240
        Ok(deserialized_value) => Ok(Some((deserialized_value, origin))),
5✔
241
        Err(err) => bail!(
12✔
242
            "Failed to cast config key '{}' value to the required type: {}",
UNCOV
243
            key,
×
244
            err
×
245
        ),
246
    }
247
}
248

UNCOV
249
static CONFIG_LOADER: Lazy<Mutex<ConfigManager>> = Lazy::new(|| Mutex::new(ConfigManager::new()));
×
250

251
#[cfg(test)]
252
mod tests {
253
    use super::*;
254
    use indexmap::IndexMap;
255
    fn setup_test_manager() -> ConfigManager {
256
        let mut manager = ConfigManager::new();
257

258
        // Populate base_defs with sample configuration
259
        let mut base_defs = IndexMap::new();
260
        base_defs.insert(
261
            "COGRS_HOME".to_string(),
262
            serde_yaml::Value::Mapping(
263
                serde_yaml::from_str(
264
                    r#"
265
                    env: [{name: COGRS_HOME}]
266
                    default: '/home/cogrs'
267
                "#,
268
                )
269
                .unwrap(),
270
            ),
271
        );
272

273
        base_defs.insert(
274
            "NUM_THREADS".to_string(),
275
            serde_yaml::Value::Mapping(serde_yaml::from_str("default: 4").unwrap()),
276
        );
277

278
        base_defs.insert(
279
            "ENABLE_DEBUG".to_string(),
280
            serde_yaml::Value::Mapping(serde_yaml::from_str("default: false").unwrap()),
281
        );
282

283
        base_defs.insert(
284
            "SERVER_CONFIG".to_string(),
285
            serde_yaml::Value::Mapping(
286
                serde_yaml::from_str("default: { HOST: '127.0.0.1', PORT: 8080 }").unwrap(),
287
            ),
288
        );
289

290
        base_defs.insert(
291
            "key_with_env".to_string(),
292
            serde_yaml::from_str(
293
                r#"
294
                env:
295
                  - name: ENV_VAR_TEST
296
                default: "default_value"
297
                "#,
298
            )
299
            .unwrap(),
300
        );
301
        base_defs.insert(
302
            "key_with_missing_env".to_string(),
303
            serde_yaml::from_str(
304
                r#"
305
                env:
306
                  - name: ENV_VAR_TEST_2
307
                default: "default_value"
308
                "#,
309
            )
310
            .unwrap(),
311
        );
312
        base_defs.insert(
313
            "key_without_env".to_string(),
314
            serde_yaml::from_str(
315
                r#"
316
                default: 42
317
                "#,
318
            )
319
            .unwrap(),
320
        );
321
        base_defs.insert(
322
            "invalid_key".to_string(),
323
            serde_yaml::from_str(
324
                r#"
325
                invalid_field: "invalid"
326
                "#,
327
            )
328
            .unwrap(),
329
        );
330
        manager.base_defs = base_defs;
331
        manager
332
    }
333

334
    #[tokio::test]
335
    async fn test_get_config_value_env_var_present() {
336
        // Set the environment variable for the test
337
        std::env::set_var("ENV_VAR_TEST", "env_value");
338

339
        let manager = setup_test_manager();
340
        let result: Option<(String, ConfigOrigin)> =
341
            manager.get_config_value("key_with_env").unwrap();
342

343
        assert!(result.is_some());
344
        let (value, origin) = result.unwrap();
345
        assert_eq!(value, "env_value");
346
        assert_eq!(origin, ConfigOrigin::Env);
347

348
        // Clean up the environment variable
349
        std::env::remove_var("ENV_VAR_TEST");
350
    }
351

352
    #[tokio::test]
353
    async fn test_get_config_value_default_value() {
354
        let manager = setup_test_manager();
355
        let result: Option<(String, ConfigOrigin)> =
356
            manager.get_config_value("key_with_missing_env").unwrap();
357

358
        assert!(result.is_some());
359
        let (value, origin) = result.unwrap();
360
        assert_eq!(value, "default_value");
361
        assert_eq!(origin, ConfigOrigin::Default);
362
    }
363

364
    #[tokio::test]
365
    async fn test_get_config_value_no_env_no_default() {
366
        let manager = setup_test_manager();
367
        let result: Option<(i32, ConfigOrigin)> = manager.get_config_value("invalid_key").unwrap();
368

369
        assert!(result.is_none());
370
    }
371

372
    #[tokio::test]
373
    async fn test_get_config_value_wrong_type() {
374
        let manager = setup_test_manager();
375
        let result: Result<Option<(String, ConfigOrigin)>> =
376
            manager.get_config_value("key_without_env");
377

378
        assert!(result.is_err());
379
        if let Err(err) = result {
380
            assert!(err.to_string().contains("Failed to cast"));
381
        }
382
    }
383

384
    #[tokio::test]
385
    async fn test_get_config_value_key_not_found() {
386
        let manager = setup_test_manager();
387
        let result: Option<(String, ConfigOrigin)> =
388
            manager.get_config_value("unknown_key").unwrap();
389

390
        assert!(result.is_none());
391
    }
392

393
    #[test]
394
    fn test_load_template_context_with_get_config_value() {
395
        let mut manager = setup_test_manager();
396

397
        let context = manager.load_template_context();
398

399
        // Validate the context contains expected values
400
        assert_eq!(context.get("COGRS_HOME"), Some(&"/home/cogrs".to_string()));
401
        assert_eq!(context.get("NUM_THREADS"), Some(&"4".to_string())); // Numbers are converted to strings
402
        assert_eq!(context.get("ENABLE_DEBUG"), Some(&"false".to_string())); // Numbers are converted to strings
403
        assert!(!context.contains_key("SERVER_CONFIG")); // Complex types, skipped
404
    }
405

406
    #[tokio::test]
407
    async fn test_render_jinja_mapping_templates() {
408
        std::env::set_var("COGRS_HOME", "/home/cogrs");
409

410
        let mut manager = setup_test_manager();
411

412
        manager.base_defs.insert(
413
            "key1".to_string(),
414
            serde_yaml::from_str(
415
                r#"
416
                default: '{{ COGRS_HOME ~ "/plugins/become" }}'
417
                "#,
418
            )
419
            .unwrap(),
420
        );
421

422
        manager.base_defs.insert(
423
            "key2".to_string(),
424
            serde_yaml::from_str(
425
                r#"
426
                default: 42
427
                "#,
428
            )
429
            .unwrap(),
430
        );
431

432
        let env = Environment::new();
433
        let template_context = manager.load_template_context();
434
        let rendered_config = manager.render_with_jinja(&env, &template_context).unwrap();
435
        manager.base_defs = rendered_config;
436

437
        let (key1, _) = manager.get_config_value::<String>("key1").unwrap().unwrap();
438
        let (key2, _) = manager.get_config_value::<usize>("key2").unwrap().unwrap();
439

440
        assert_eq!(key1, "/home/cogrs/plugins/become".to_owned());
441

442
        assert_eq!(key2, 42);
443
    }
444
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc