confmap/
lib.rs

1//! # confmap
2//!
3//! A library for reading config file into a map in memory.
4//! This library is based on serde_json and once_cell.
5//! after the config file is read, you can easily get the config by using get_string, get_int64, get_bool...
6//! This library is created because I cannot find a library like this in rust. (the idea is the same to viper package in golang)
7//!
8//! example:
9//! put a json format file in your project folder like this:
10//!
11//!         config.json
12//!         {
13//!             "testGetString": "YesMan",
14//!             "testGetInt64": 43,
15//!             "testGetStringArray": [
16//!                 "+44 1234567",
17//!                 "+44 2345678"
18//!             ]
19//!         }
20//!
21//! add dependency in Cargo.toml:
22//!
23//!     [dependencies]
24//!
25//!     confmap = "1.0.0"
26//!
27//! in your project main.rs:
28//!
29//!     use confmap;
30//!
31//!     fn main() {
32//!
33//!         confmap::add_config_path(path_str);
34//!
35//!         confmap::set_config_name("config.json");
36//!
37//!         confmap::read_config();
38//!
39//!         assert_eq!(Some("YesMan".to_string()), confmap::get_string("testGetString"));
40//!
41//!         assert_eq!(Some(43), confmap::get_int64("testGetInt64"));
42//!
43//!         assert_eq!(Some(vec!["+44 1234567".to_string(), "+44 2345678".to_string()]), confmap::get_string_array("testGetStringArray"));
44//!
45//!     }
46
47use std::env;
48use std::error::Error;
49use std::fs;
50use std::path::Path;
51use std::sync::{Arc, Mutex};
52use once_cell::sync::Lazy;
53use serde_json::{Map, Value};
54
55struct ConfigSerde;
56
57static mut CONFIG_NAME: String = String::new();
58static mut CONFIG_PATH: String = String::new();
59static CONFIGS: Lazy<Arc<Mutex<Map<String, Value>>>> = Lazy::new(|| {
60    let m = Map::new();
61    Arc::new(Mutex::new(m))
62});
63
64impl ConfigSerde {
65    fn parse_value(value_ref: &Value) -> Value {
66        value_ref.clone()
67    }
68
69    fn read_config(config_path: &str) -> Result<Map<String, Value>, Box<dyn Error>> {
70        println!("reading file {}", config_path);
71        let config = fs::read_to_string(config_path)?;
72        let parsed: Map<String, Value> = serde_json::from_str(config.as_str())?;
73        let result = parsed
74            .into_iter()
75            .map(|(k, v)| (k, ConfigSerde::parse_value(&v)))
76            .collect();
77        Ok(result)
78    }
79}
80
81/// Set filename.
82/// put config file in the folder of the executable file
83/// # Example
84/// confmap::set_config_name("config.json");
85/// ```
86///
87pub fn set_config_name(config_name: &str) {
88    unsafe { CONFIG_NAME = config_name.to_string(); }
89}
90
91/// Add path of the file.
92/// this will allow you to put config file in other path
93/// # Example
94/// confmap::add_config_path("config.json");
95/// ```
96pub fn add_config_path(path: &str) {
97    unsafe {
98        #[cfg(target_family = "unix")]
99        if path.ends_with("/") {
100            CONFIG_PATH = path.to_string();
101        } else {
102            CONFIG_PATH = path.to_string() + "/";
103        }
104        #[cfg(target_family = "windows")]
105        if path.ends_with("\\") {
106            CONFIG_PATH = path.to_string();
107        } else {
108            CONFIG_PATH = path.to_string() + "\\";
109        }
110    }
111}
112
113/// this function read config file after file path and file name are given.
114/// you can use get_string, get_int64 ...etc, to get the value after config file is loaded by this function.
115/// # Example
116/// ```
117/// confmap::read_config();
118/// ```
119pub fn read_config() {
120    if !unsafe { CONFIG_NAME.is_empty() } {
121        let path_buf = env::current_exe().expect("Failed to get executable path");
122        let paths = fs::read_dir(path_buf.parent().unwrap()).unwrap();
123        let mut is_found:bool;
124        unsafe{
125            let file_path = CONFIG_PATH.to_owned() + &CONFIG_NAME;
126            let path = Path::new(&file_path);
127            is_found = path.exists() && path.is_file();
128        }
129        if !is_found {
130            for path in paths {
131                let path_str = path.unwrap().path();
132                let filename = path_str.file_name().unwrap().to_string_lossy();
133                unsafe {
134                    if filename == CONFIG_NAME.to_string() {
135                        #[cfg(target_family = "unix")]
136                        {
137                            CONFIG_PATH = path_str.clone().parent().unwrap().to_string_lossy().to_string() + "/";
138                        }
139                        #[cfg(target_family = "windows")]
140                        {
141                            CONFIG_PATH = path_str.clone().parent().unwrap().to_string_lossy().parse().unwrap() + "\\";
142                        }
143                        // CONFIG_NAME = filename.parse().unwrap();
144                        println!("file is found!!");
145                        is_found = true;
146                        break;
147                    } // else {
148                    //     println!("Got: {}, CONFIG_NAME: {:?}", filename, CONFIG_NAME.to_string());
149                    // }
150                }
151            }
152        }
153
154        if is_found {
155            init_lazy_configs(&mut CONFIGS.lock().unwrap());
156        } else {
157            println!("file is not found");
158        }
159    }
160}
161
162fn init_lazy_configs(input: &mut Map<String, Value>) {
163    let path = unsafe { CONFIG_PATH.to_string() + &CONFIG_NAME };
164    println!("init_lazy_configs path: {}", path);
165    match ConfigSerde::read_config(&path) {
166        Ok(configs) => {
167            for (k, v) in configs.iter() {
168                input.insert(k.clone(), v.clone()); // Assuming Value is Cloneable
169            }
170        }
171        Err(_e) => {
172            // not thing to do
173        }
174    }
175    println!("configs: {:?}", input);
176}
177
178/// this function will return Option<String> when you put a key argument.
179/// # Example
180/// ```
181/// confmap::get_string("testGetString");
182/// ```
183pub fn get_string(key: &str) -> Option<String> {
184    let configs = CONFIGS.lock().unwrap();
185    if let Some(value) = configs.get(key) {
186        value.as_str().map(|s| s.to_string())
187    } else {
188        None
189    }
190}
191
192/// this function will return Option<Vec<String>> when you put a key argument.
193/// # Example
194/// ```
195/// confmap::get_string_array("testGetStringArray");
196/// ```
197pub fn get_string_array(key: &str) -> Option<Vec<String>> {
198    let configs = CONFIGS.lock().unwrap();
199    if let Some(value) = configs.get(key) {
200        if let Value::Array(arr) = value {
201            let mut string_array = Vec::new();
202            for element in arr {
203                if let Value::String(s) = element {
204                    string_array.push(s.clone());
205                }
206            }
207            Some(string_array)
208        } else {
209            None
210        }
211    } else {
212        None
213    }
214}
215
216/// this function will return Option<i64> when you put a key argument.
217/// # Example
218/// ```
219/// confmap::get_int64("testGetInt64");
220/// ```
221pub fn get_int64(key: &str) -> Option<i64> {
222    let configs = CONFIGS.lock().unwrap();
223    if let Some(value) = configs.get(key) {
224        match value {
225            Value::Number(n) => n.as_i64(),
226            _ => None,
227        }
228    } else {
229        None
230    }
231}
232
233/// this function will return Option<Vec<i64>> when you put a key argument.
234/// # Example
235/// ```
236/// confmap::get_int64_array("testGetFloat64Array");
237/// ```
238pub fn get_int64_array(key: &str) -> Option<Vec<i64>> {
239    let configs = CONFIGS.lock().unwrap();
240    if let Some(value) = configs.get(key) {
241        if let Value::Array(arr) = value {
242            let mut int64_array = Vec::new();
243            for element in arr {
244                if let Value::Number(n) = element {
245                    if let Some(int_value) = n.as_i64() {
246                        int64_array.push(int_value);
247                    }
248                }
249            }
250            Some(int64_array)
251        } else {
252            None
253        }
254    } else {
255        None
256    }
257}
258
259/// this function will return Option<i32> when you put a key argument.
260/// # Example
261/// ```
262/// confmap::get_int32("testGetInt32");
263/// ```
264pub fn get_i32(key: &str) -> Option<i32> {
265    let configs = CONFIGS.lock().unwrap();
266    if let Some(value) = configs.get(key) {
267        match value {
268            Value::Number(n) => n.as_i64().map(|n| n as i32),
269            _ => None,
270        }
271    } else {
272        None
273    }
274}
275
276/// this function will return Option<i16> when you put a key argument.
277/// # Example
278/// ```
279/// confmap::get_int16("testGetInt16");
280/// ```
281pub fn get_i16(key: &str) -> Option<i16> {
282    let configs = CONFIGS.lock().unwrap();
283    if let Some(value) = configs.get(key) {
284        match value {
285            Value::Number(n) => n.as_i64().map(|n| n as i16),
286            _ => None,
287        }
288    } else {
289        None
290    }
291}
292
293/// this function will return Option<i8> when you put a key argument.
294/// # Example
295/// ```
296/// confmap::get_int8("testGetInt8");
297/// ```
298pub fn get_int8(key: &str) -> Option<i8> {
299    let configs = CONFIGS.lock().unwrap();
300    if let Some(value) = configs.get(key) {
301        match value {
302            Value::Number(n) => n.as_i64().map(|n| n as i8),
303            _ => None,
304        }
305    } else {
306        None
307    }
308}
309
310/// this function will return Option<f64> when you put a key argument.
311/// # Example
312/// ```
313/// confmap::get_float64("testGetFloat64");
314/// ```
315pub fn get_float64(key: &str) -> Option<f64> {
316    let configs = CONFIGS.lock().unwrap();
317    if let Some(value) = configs.get(key) {
318        match value {
319            Value::Number(n) => n.as_f64(),
320            _ => None,
321        }
322    } else {
323        None
324    }
325}
326
327/// this function will return Option<Vec<f64>> when you put a key argument.
328/// # Example
329/// ```
330/// confmap::get_float64_array("testGetFloat64Array");
331/// ```
332pub fn get_float64_array(key: &str) -> Option<Vec<f64>> {
333    let configs = CONFIGS.lock().unwrap();
334    if let Some(value) = configs.get(key) {
335        if let Value::Array(arr) = value {
336            let mut float64_array = Vec::new();
337            for element in arr {
338                if let Value::Number(n) = element {
339                    if let Some(int_value) = n.as_f64() {
340                        float64_array.push(int_value);
341                    }
342                }
343            }
344            Some(float64_array)
345        } else {
346            None
347        }
348    } else {
349        None
350    }
351}
352
353/// this function will return Option<f32> when you put a key argument.
354/// # Example
355/// ```
356/// confmap::get_float32("testGetFloat32");
357/// ```
358pub fn get_float32(key: &str) -> Option<f32> {
359    let configs = CONFIGS.lock().unwrap();
360    if let Some(value) = configs.get(key) {
361        match value {
362            Value::Number(n) => n.as_f64().map(|n| n as f32),
363            _ => None,
364        }
365    } else {
366        None
367    }
368}
369
370/// this function will return Option<bool> when you put a key argument.
371/// # Example
372/// ```
373/// confmap::get_bool("testGetBool");
374/// ```
375pub fn get_bool(key: &str) -> Option<bool> {
376    let configs = CONFIGS.lock().unwrap();
377    if let Some(value) = configs.get(key) {
378        value.as_bool()
379    } else {
380        None
381    }
382}
383
384/// this function will return Option<serde_json::Value> when you put a key argument.
385/// # Example
386/// ```
387/// confmap::get("testGet");
388/// ```
389pub fn get(key: &str) -> Option<Value> {
390    CONFIGS.lock().unwrap().get(key).cloned()
391}
392
393/// this function will return Option<Vec<serde_json::Value>> when you put a key argument.
394/// # Example
395/// ```
396/// confmap::get_array("testGetArray");
397/// ```
398pub fn get_array(key: &str) -> Option<Vec<Value>> {
399    let configs = CONFIGS.lock().unwrap();
400    if let Some(value) = configs.get(key) {
401        if let Value::Array(arr) = value {
402            let mut array = Vec::new();
403            for element in arr {
404                if let Value::Object(_) = element {
405                    array.push(element.clone());
406                }
407            }
408            Some(array)
409        } else {
410            None
411        }
412    } else {
413        None
414    }
415}
416
417/// this function will return Option<Map<String, Value>> when you put a key argument.
418/// # Example
419/// ```
420/// confmap::get_map("testGetMap");
421/// ```
422pub fn get_map(key: &str) -> Option<Map<String, Value>> {
423    let configs = CONFIGS.lock().unwrap();
424    if let Some(map) = configs.get(key) {
425        map.as_object().cloned()
426    } else {
427        None
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use std::env;
434    use std::io::Write;
435    use std::path::{PathBuf};
436    use super::*;
437
438    #[test]
439    fn it_works() {
440        let data = r#"
441        {
442            "testGetString": "YesMan",
443            "testGetInt64": 43,
444            "testGetStringArray": [
445                "+44 1234567",
446                "+44 2345678"
447            ]
448        }"#;
449        let path_buf = env::current_exe().expect("Failed to get executable path");
450        let path_str = path_buf
451            .ancestors()
452            .nth(4) // Adjust the number as needed to reach the desired parent directory
453            .expect("Invalid number of ancestors")
454            .to_str()
455            .expect("Failed to convert path to string");
456        let mut path = PathBuf::from(path_str);
457        path.push("config.json");
458        let mut file = std::fs::File::create(path.clone()).expect("create failed");
459        file.write_all(data.as_bytes()).expect("write failed");
460        add_config_path(path_str);
461        set_config_name("config.json");
462        read_config();
463        if path.as_path().exists() {
464            std::fs::remove_file(path.as_path()).expect("failed to delete test file");
465        }
466        assert_eq!(Some("YesMan".to_string()), get_string("testGetString"));
467        assert_eq!(Some(43), get_int64("testGetInt64"));
468        assert_eq!(Some(vec!["+44 1234567".to_string(), "+44 2345678".to_string()]), get_string_array("testGetStringArray"));
469    }
470}