better_config_loader/
json.rs

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