app_json_settings/
lib.rs

1//! App JSON Settings
2
3use serde::{Deserialize, Serialize};
4use serde_json::{from_str, Value};
5use std::fs::{create_dir_all, read_to_string, remove_dir, remove_file, File};
6use std::io::{Read, Write};
7use std::path::PathBuf;
8
9#[cfg(test)]
10mod tests;
11
12/// default file name
13const DEFAULT_FILENAME: &str = "settings.json";
14
15/// core
16#[derive(Serialize, Deserialize)]
17pub struct JsonSettings {
18    filepath: PathBuf,
19}
20
21/// i/o
22#[derive(Serialize, Deserialize)]
23pub struct KeyValue {
24    key: Option<String>,
25    pub value: Option<Value>,
26    file_exists: bool,
27    key_exists: bool,
28}
29
30impl JsonSettings {
31    /// create instance
32    pub fn new(filepath: &PathBuf) -> JsonSettings {
33        JsonSettings {
34            filepath: filepath.to_owned(),
35        }
36    }
37
38    /// create instance to manage file in executable dir
39    pub fn exe_dir() -> JsonSettings {
40        let filepath = exe_dir_filepath(DEFAULT_FILENAME);
41        JsonSettings::new(&filepath)
42    }
43
44    /// create instance to manage file in executable dir
45    pub fn exe_dir_with_filename(filename: &str) -> JsonSettings {
46        let filepath = exe_dir_filepath(filename);
47        JsonSettings::new(&filepath)
48    }
49
50    /// create instance to manage file in user config dir
51    pub fn config_dir() -> JsonSettings {
52        let filepath = config_dir_filepath(DEFAULT_FILENAME);
53        JsonSettings::new(&filepath)
54    }
55
56    /// create instance to manage file in user config dir
57    pub fn config_dir_with_filename(filename: &str) -> JsonSettings {
58        let filepath = config_dir_filepath(filename);
59        JsonSettings::new(&filepath)
60    }
61
62    /// get value from key if exists
63    pub fn read_by_key(&self, key: &str) -> Result<KeyValue, Box<dyn std::error::Error>> {
64        let filepath = &self.filepath;
65
66        if !filepath.exists() {
67            return Ok(KeyValue {
68                key: None,
69                value: None,
70                file_exists: false,
71                key_exists: false,
72            });
73        }
74
75        let json = json_load(&filepath)?;
76        if let Some(value) = json.get(key) {
77            Ok(KeyValue {
78                key: Some(key.to_owned()),
79                value: Some(value.to_owned()),
80                file_exists: true,
81                key_exists: true,
82            })
83        } else {
84            return Ok(KeyValue {
85                key: Some(key.to_owned()),
86                value: None,
87                file_exists: true,
88                key_exists: false,
89            });
90        }
91    }
92
93    /// append or update value bound to key
94    pub fn write_by_key(&self, key: &str, value: &Value) -> Result<(), std::io::Error> {
95        let filepath = &self.filepath;
96
97        let mut current_settings = if filepath.exists() {
98            let file_text = read_to_string(&filepath)?;
99            serde_json::from_str(&file_text).unwrap_or_default()
100        } else {
101            Value::Object(serde_json::Map::new())
102        };
103
104        let map = current_settings.as_object_mut().unwrap();
105        map.insert(key.to_owned(), value.to_owned());
106
107        let updated_settings = serde_json::to_string_pretty(&current_settings)?;
108
109        let mut file = File::create(&filepath)?;
110        file.write_all(updated_settings.as_bytes())?;
111
112        Ok(())
113    }
114
115    /// remove settings file
116    pub fn remove(&self, remove_dir_if_empty: bool) {
117        remove_file(&self.filepath).expect("Failed to remove settings file");
118        if !remove_dir_if_empty {
119            match remove_dir(self.filepath.parent().unwrap()) {
120                Ok(_) => (),
121                Err(_) => (), // dir is not empty
122            }
123        }
124    }
125}
126
127/// user config dir
128pub fn config_dir() -> PathBuf {
129    let current_exe = std::env::current_exe().unwrap();
130    let filename = current_exe.file_name().unwrap().to_str().unwrap();
131    config_root_dir().join(filename)
132}
133
134/// settings file path in executable dir
135fn exe_dir_filepath(filename: &str) -> PathBuf {
136    let exec_filepath = std::env::current_exe().expect("Failed to get exec path");
137    let dirpath = exec_filepath
138        .parent()
139        .expect("Failed to get exec parent dir path");
140    dirpath.join(filename)
141}
142
143/// settings file path in config dir
144fn config_dir_filepath(filename: &str) -> PathBuf {
145    let dirpath = config_dir();
146    if !dirpath.exists() {
147        create_dir_all(&dirpath).expect("Failed to create app dir in user config");
148    }
149    dirpath.join(filename)
150}
151
152/// read settings file and get json key-value pairs
153fn json_load(filepath: &PathBuf) -> Result<Value, Box<dyn std::error::Error>> {
154    let mut file =
155        File::open(&filepath).map_err(|e| format!("Failed to open settings file: {}", e))?;
156    let mut contents = String::new();
157    file.read_to_string(&mut contents)
158        .map_err(|e| format!("Failed to read settings file: {}", e))?;
159    let json: Value =
160        from_str(&contents).map_err(|e| format!("Failed to deserialize settings: {}", e))?;
161    Ok(json)
162}
163
164#[cfg(target_os = "linux")]
165fn config_root_dir() -> PathBuf {
166    std::env::var("XDG_CONFIG_HOME")
167        .map(PathBuf::from)
168        .unwrap_or_else(|_| {
169            let mut home_dir = std::env::var("HOME").expect("HOME not set");
170            home_dir.push_str("/.config");
171            PathBuf::from(home_dir)
172        })
173}
174
175#[cfg(target_os = "windows")]
176fn config_root_dir() -> PathBuf {
177    std::env::var("APPDATA")
178        .map(PathBuf::from)
179        .expect("APPDATA not set")
180}
181
182#[cfg(target_os = "macos")]
183fn config_root_dir() -> PathBuf {
184    let mut home_dir = std::env::var("HOME").expect("HOME not set");
185    home_dir.push_str("/Library/Application Support");
186    PathBuf::from(home_dir)
187}
188
189#[cfg(target_os = "android")]
190fn config_root_dir() -> PathBuf {
191    let internal_storage =
192        std::env::var("ANDROID_INTERNAL_STORAGE").expect("ANDROID_INTERNAL_STORAGE not set");
193    PathBuf::from(internal_storage).join("config")
194}
195
196#[cfg(target_os = "ios")]
197fn config_root_dir() -> PathBuf {
198    let home_dir = std::env::var("HOME").expect("HOME not set");
199    PathBuf::from(home_dir).join("Documents").join("config")
200}