better_config_loader/
json.rs

1use better_config_core::{merge_with_env_uppercase, AbstractConfig, Error, misc};
2use std::collections::{HashMap, HashSet};
3use std::fs;
4
5/// Indicates that structure can be initialized from JSON file.
6pub trait JsonConfig<T = HashMap<String, String>>: AbstractConfig<T> {
7    /// Load specified JSON file and initialize the structure.
8    ///
9    /// # Arguments
10    /// * `target` - Path to the JSON file.
11    ///
12    /// # Errors
13    /// * `Error::LoadFileError` - If the specified JSON file cannot be loaded or parsed.
14    fn load(target: Option<String>) -> Result<T, Error>
15    where
16        T: Default,
17        HashMap<String, String>: Into<T>,
18        Self: Sized,
19    {
20        Self::load_with_override(target, &HashSet::new())
21    }
22
23    /// Load specified JSON file with explicit control over which keys should not be overridden.
24    ///
25    /// # Arguments
26    /// * `target` - Path to the JSON file.
27    /// * `excluded_keys` - Keys that should not be overridden by environment variables.
28    ///
29    /// # Errors
30    /// * `Error::LoadFileError` - If the specified JSON file cannot be loaded or parsed.
31    fn load_with_override(target: Option<String>, excluded_keys: &HashSet<String>) -> Result<T, Error>
32    where
33        T: Default,
34        HashMap<String, String>: Into<T>,
35        Self: Sized,
36    {
37        let target = target.or(Some("config.json".to_string()));
38
39        let mut json_map = HashMap::new();
40
41        if let Some(target) = target {
42            let file_paths = misc::validate_and_split_paths(&target)?;
43
44            for file_path in file_paths {
45                // Check file accessibility before reading
46                misc::check_file_accessibility(&file_path)?;
47
48                let content = fs::read_to_string(&file_path)
49                    .map_err(|e| Error::IoError {
50                        operation: format!("read file '{}'", file_path),
51                        source: Some(Box::new(e)),
52                    })?;
53
54                let value: serde_json::Value = serde_json::from_str(&content)
55                    .map_err(|e| Error::parse_json_error(&file_path, e))?;
56
57                flatten_json_value(&value, None, &mut json_map)
58                    .map_err(|e| Error::value_conversion_error("json", "string", &format!("{}", e)))?;
59            }
60        }
61
62        // Apply environment variable override with excluded keys
63        let json_map = merge_with_env_uppercase(json_map, None, excluded_keys);
64
65        Ok(json_map.into())
66    }
67}
68
69fn flatten_json_value(value: &serde_json::Value, parent_key: Option<String>, map: &mut HashMap<String, String>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
70    match value {
71        serde_json::Value::Object(obj) => {
72            for (key, val) in obj {
73                let new_key = match &parent_key {
74                    Some(parent) => format!("{}.{}", parent, key),
75                    None => key.to_string(),
76                };
77                flatten_json_value(val, Some(new_key), map)?;
78            }
79        }
80        serde_json::Value::Array(arr) => {
81            for (i, val) in arr.iter().enumerate() {
82                let new_key = match &parent_key {
83                    Some(parent) => format!("{}[{}]", parent, i),
84                    None => i.to_string(),
85                };
86                flatten_json_value(val, Some(new_key), map)?;
87            }
88        }
89        serde_json::Value::String(s) => {
90            if let Some(key) = parent_key {
91                map.insert(key, s.to_string());
92            }
93        }
94        serde_json::Value::Number(n) => {
95            if let Some(key) = parent_key {
96                let num_str = if n.is_i64() {
97                    n.as_i64()
98                        .map(|v| v.to_string())
99                        .unwrap_or_else(|| n.to_string())
100                } else if n.is_f64() {
101                    n.as_f64()
102                        .map(|v| v.to_string())
103                        .unwrap_or_else(|| n.to_string())
104                } else {
105                    n.to_string()
106                };
107                map.insert(key, num_str);
108            }
109        }
110        serde_json::Value::Bool(b) => {
111            if let Some(key) = parent_key {
112                map.insert(key, b.to_string());
113            }
114        }
115        serde_json::Value::Null => {}
116    }
117
118    Ok(())
119}