Skip to main content

app_json_settings/
core.rs

1use serde::{de::DeserializeOwned, Serialize};
2
3use std::fs;
4use std::io;
5use std::marker::PhantomData;
6use std::path::{Path, PathBuf};
7
8use crate::core::constant::DEFAULT_FILE_NAME;
9use crate::core::dir::default_config_dir;
10#[cfg(all(windows, feature = "uwp"))]
11use crate::core::dir::uwp_local_folder_dir;
12use crate::core::json::JsonFormat;
13use crate::ConfigError;
14use crate::Result;
15
16pub mod constant;
17mod dir;
18pub mod error;
19mod json;
20
21#[cfg(test)]
22mod tests;
23
24pub struct ConfigManager<T> {
25    folder_path: PathBuf,
26    file_name: String,
27    json_format: JsonFormat,
28    _marker: PhantomData<T>,
29}
30
31impl<T> ConfigManager<T>
32where
33    T: Serialize + DeserializeOwned,
34{
35    /// Creates a config manager using the OS standard config directory and current executable name.
36    ///
37    /// On Windows desktop apps this resolves under `%APPDATA%`.
38    /// Pure UWP apps should use `with_root_dir` or the optional `uwp` feature instead.
39    pub fn new() -> Self {
40        let app_name = std::env::current_exe()
41            .ok()
42            .and_then(|path| path.file_stem().map(|name| name.to_string_lossy().to_string()))
43            .unwrap_or_else(|| "app".to_string());
44
45        let folder_path = default_config_dir().join(&app_name);
46
47        Self {
48            folder_path,
49            file_name: DEFAULT_FILE_NAME.to_string(),
50            json_format: JsonFormat::Pretty,
51            _marker: PhantomData,
52        }
53    }
54
55    /// Stores the settings file in the current working directory.
56    pub fn at_current_dir(mut self) -> Self {
57        self.folder_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
58        self
59    }
60
61    /// Stores the settings file in a caller-provided directory.
62    ///
63    /// This is the primary compatibility seam for sandboxed hosts, including Pure UWP.
64    /// The host application may resolve its application data directory and pass it here.
65    pub fn with_root_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
66        self.folder_path = path.into();
67        self
68    }
69
70    /// Stores the settings file in a caller-provided directory.
71    ///
72    /// This method is kept for compatibility with v2.0.x. Prefer `with_root_dir` in new code.
73    pub fn at_custom_dir<P: Into<PathBuf>>(self, path: P) -> Self {
74        self.with_root_dir(path)
75    }
76
77    /// Stores the settings file under `ApplicationData.Current.LocalFolder`.
78    ///
79    /// This method is available only on Windows when the optional `uwp` feature is enabled.
80    #[cfg(all(windows, feature = "uwp"))]
81    pub fn at_uwp_local_folder(mut self) -> Result<Self> {
82        self.folder_path = uwp_local_folder_dir()?;
83        Ok(self)
84    }
85
86    /// Changes the settings file name.
87    pub fn with_filename(mut self, name: &str) -> Self {
88        self.file_name = name.to_string();
89        self
90    }
91
92    /// Stores JSON in compact form instead of pretty-printed form.
93    pub fn disable_pretty_json(mut self) -> Self {
94        self.json_format = JsonFormat::Compact;
95        self
96    }
97
98    /// Returns the settings folder path.
99    pub fn folder_path(&self) -> &Path {
100        &self.folder_path
101    }
102
103    /// Returns the settings file path.
104    pub fn path(&self) -> PathBuf {
105        self.folder_path.join(&self.file_name)
106    }
107
108    /// Saves the complete configuration, replacing the existing file content.
109    pub fn save(&self, config: &T) -> Result<()> {
110        if !self.folder_path.exists() {
111            fs::create_dir_all(&self.folder_path)?;
112        }
113
114        let content = match self.json_format {
115            JsonFormat::Compact => serde_json::to_string(config)?,
116            JsonFormat::Pretty => serde_json::to_string_pretty(config)?,
117        };
118
119        fs::write(self.path(), content)?;
120        Ok(())
121    }
122
123    /// Loads a configuration file that is expected to already exist.
124    pub fn load(&self) -> Result<T> {
125        let content = fs::read_to_string(self.path())?;
126        Ok(serde_json::from_str(&content)?)
127    }
128}
129
130impl<T> Default for ConfigManager<T>
131where
132    T: Serialize + DeserializeOwned,
133{
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl<T> ConfigManager<T>
140where
141    T: Serialize + DeserializeOwned + Default,
142{
143    /// Loads the configuration, or creates and saves `T::default()` on first run.
144    pub fn load_or_default(&self) -> Result<T> {
145        let path = self.path();
146
147        match fs::read_to_string(&path) {
148            Ok(content) => Ok(serde_json::from_str(&content)?),
149
150            Err(e) if e.kind() == io::ErrorKind::NotFound => {
151                let default_config = T::default();
152                self.save(&default_config)?;
153                Ok(default_config)
154            }
155
156            Err(e) => Err(ConfigError::Io(e)),
157        }
158    }
159
160    /// Applies a read-modify-write update and saves the result.
161    pub fn update<F>(&self, f: F) -> Result<T>
162    where
163        F: FnOnce(&mut T),
164    {
165        let mut cfg = self.load_or_default()?;
166        f(&mut cfg);
167        self.save(&cfg)?;
168        Ok(cfg)
169    }
170}