Skip to main content

app_json_settings/
core.rs

1use serde::{Serialize, de::DeserializeOwned};
2
3use std::fs;
4use std::io;
5use std::marker::PhantomData;
6use std::path::{Path, PathBuf};
7
8use crate::ConfigError;
9use crate::Result;
10use crate::core::constant::DEFAULT_FILE_NAME;
11#[cfg(all(windows, feature = "uwp"))]
12use crate::core::dir::uwp_local_folder_dir;
13use crate::core::dir::{app_name_from, default_config_dir, default_runtime_app_name};
14use crate::core::json::{JsonFormat, deserialize, serialize};
15use crate::core::save::save_to_path;
16use crate::core::validation::{validate_path_component, validate_plain_file_name};
17
18pub mod constant;
19mod dir;
20pub mod error;
21mod json;
22mod save;
23pub mod validation;
24
25pub use save::SaveMode;
26
27#[cfg(test)]
28mod tests;
29
30/// Manages one typed JSON settings file.
31///
32/// `ConfigManager<T>` stores and loads a complete configuration value of type
33/// `T`. The type must implement Serde `Serialize` and `DeserializeOwned`.
34///
35/// The manager is intentionally small. It owns only:
36///
37/// * the directory containing the settings file,
38/// * the settings file name,
39/// * the JSON output format, and
40/// * the save strategy.
41#[derive(Debug, Clone)]
42pub struct ConfigManager<T> {
43    folder_path: PathBuf,
44    file_name: String,
45    json_format: JsonFormat,
46    save_mode: SaveMode,
47    _marker: PhantomData<T>,
48}
49
50impl<T> ConfigManager<T>
51where
52    T: Serialize + DeserializeOwned,
53{
54    /// Creates a config manager using the OS-standard config directory and the
55    /// current executable name.
56    ///
57    /// On Windows desktop apps this resolves under `%APPDATA%`. Pure UWP apps
58    /// should use [`with_root_dir`](Self::with_root_dir) or the optional
59    /// `uwp` feature instead.
60    ///
61    /// This constructor cannot report failure without an API break, so it
62    /// falls back silently in two places instead of erroring:
63    ///
64    /// * if the platform configuration directory cannot be determined (for
65    ///   example, no `HOME` or `%APPDATA%` in the environment), it falls back
66    ///   to the current directory (`.`);
67    /// * if the current executable's name cannot be determined, or is not a
68    ///   safe path component, it falls back to the literal name `"app"`.
69    ///
70    /// **The second fallback is a fixed constant.** Any two executables that
71    /// both hit it resolve to the same settings file and can silently read
72    /// and overwrite each other's settings. If that is not acceptable, use
73    /// [`try_new`](Self::try_new) to fail instead of falling back, or
74    /// [`for_app`](Self::for_app) to supply an explicit identity so nothing
75    /// is derived in the first place.
76    pub fn new() -> Self {
77        let folder_path = default_config_dir()
78            .unwrap_or_else(|_| PathBuf::from("."))
79            .join(default_runtime_app_name());
80        Self::from_parts(folder_path, DEFAULT_FILE_NAME)
81    }
82
83    /// Creates a config manager using the OS-standard config directory and
84    /// the current executable name, failing rather than silently
85    /// substituting either.
86    ///
87    /// Fail-closed counterpart to [`new`](Self::new): returns
88    /// [`ConfigError::Platform`] if the platform configuration directory
89    /// cannot be resolved, or if the current executable's name cannot be
90    /// determined or is not a safe path component -- the two cases `new()`
91    /// papers over with `.` and `"app"` respectively. Both failures report
92    /// through the same variant with distinguishable messages, since adding
93    /// a dedicated variant would itself be a breaking change: `ConfigError`
94    /// is not `#[non_exhaustive]`, so a new variant breaks any exhaustive
95    /// `match` on it.
96    ///
97    /// Prefer this over `new()` when you genuinely want the executable's
98    /// derived name but sharing a settings file with another executable
99    /// that hits the same fallback is not acceptable. If you have a stable
100    /// application identity to supply instead, prefer
101    /// [`for_app`](Self::for_app), which needs no derivation at all.
102    pub fn try_new() -> Result<Self> {
103        let folder_path = default_config_dir()?.join(app_name_from(std::env::current_exe().ok())?);
104        Ok(Self::from_parts(folder_path, DEFAULT_FILE_NAME))
105    }
106
107    /// Creates a config manager for an explicit application name.
108    ///
109    /// This is the recommended desktop constructor for production apps because
110    /// the storage directory is stable even if the executable file name changes.
111    /// The `app_name` must be a single safe path component, not a path.
112    ///
113    /// Returns [`ConfigError::Platform`] if the platform configuration
114    /// directory cannot be resolved (for example, no `HOME` or `%APPDATA%` in
115    /// the environment). Applications that hit this in practice — typically
116    /// services or containers without a user environment — should supply a
117    /// path explicitly with [`with_root_dir`](Self::with_root_dir) instead.
118    pub fn for_app(app_name: &str) -> Result<Self> {
119        let app_name = validate_path_component(app_name)?;
120        let folder_path = default_config_dir()?.join(app_name);
121        Ok(Self::from_parts(folder_path, DEFAULT_FILE_NAME))
122    }
123
124    fn from_parts<P>(folder_path: P, file_name: &str) -> Self
125    where
126        P: Into<PathBuf>,
127    {
128        Self {
129            folder_path: folder_path.into(),
130            file_name: file_name.to_string(),
131            json_format: JsonFormat::Pretty,
132            save_mode: SaveMode::Atomic,
133            _marker: PhantomData,
134        }
135    }
136
137    /// Stores the settings file in the current working directory.
138    ///
139    /// If the current working directory cannot be determined, falls back to
140    /// `"."`. Unlike [`for_app`](Self::for_app), this fallback is not
141    /// surprising here: the caller explicitly asked for working-directory
142    /// storage, and `"."` already means "the working directory" to the
143    /// filesystem.
144    pub fn at_current_dir(mut self) -> Self {
145        self.folder_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
146        self
147    }
148
149    /// Stores the settings file in a caller-provided directory.
150    ///
151    /// This is the primary compatibility seam for sandboxed hosts, including
152    /// Pure UWP. The host application may resolve its application data directory
153    /// and pass it here.
154    pub fn with_root_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
155        self.folder_path = path.into();
156        self
157    }
158
159    /// Stores the settings file in a caller-provided directory.
160    ///
161    /// This method is kept for compatibility with v2.0.x. Prefer
162    /// [`with_root_dir`](Self::with_root_dir) in new code.
163    pub fn at_custom_dir<P: Into<PathBuf>>(self, path: P) -> Self {
164        self.with_root_dir(path)
165    }
166
167    /// Stores the settings file under `ApplicationData.Current.LocalFolder`.
168    ///
169    /// This method is available only on Windows when the optional `uwp` feature
170    /// is enabled.
171    #[cfg(all(windows, feature = "uwp"))]
172    pub fn at_uwp_local_folder(mut self) -> Result<Self> {
173        self.folder_path = uwp_local_folder_dir()?;
174        Ok(self)
175    }
176
177    /// Changes the settings file name without validation.
178    ///
179    /// This method is retained for v2.x compatibility. New code should prefer
180    /// [`try_with_filename`](Self::try_with_filename), which rejects path-like
181    /// names such as `../settings.json`.
182    pub fn with_filename(mut self, name: &str) -> Self {
183        self.file_name = name.to_string();
184        self
185    }
186
187    /// Changes the settings file name after validating it as a plain file name.
188    ///
189    /// The accepted value must be a single file name, not an absolute path and
190    /// not a relative path containing directory traversal.
191    pub fn try_with_filename(mut self, name: &str) -> Result<Self> {
192        self.file_name = validate_plain_file_name(name)?.to_string();
193        Ok(self)
194    }
195
196    /// Stores JSON in compact form instead of pretty-printed form.
197    pub fn disable_pretty_json(mut self) -> Self {
198        self.json_format = JsonFormat::Compact;
199        self
200    }
201
202    /// Selects the save strategy.
203    ///
204    /// The default is [`SaveMode::Atomic`]. Use [`SaveMode::Direct`] only when
205    /// an application intentionally wants v2.2-style direct overwrite behavior
206    /// or needs to work around unusual filesystem semantics.
207    pub fn with_save_mode(mut self, mode: SaveMode) -> Self {
208        self.save_mode = mode;
209        self
210    }
211
212    /// Selects direct overwrite saves.
213    ///
214    /// This is a convenience alias for `with_save_mode(SaveMode::Direct)`.
215    pub fn with_direct_save(self) -> Self {
216        self.with_save_mode(SaveMode::Direct)
217    }
218
219    /// Returns the configured save strategy.
220    pub fn save_mode(&self) -> SaveMode {
221        self.save_mode
222    }
223
224    /// Returns the settings folder path.
225    pub fn folder_path(&self) -> &Path {
226        &self.folder_path
227    }
228
229    /// Returns the settings file name.
230    pub fn file_name(&self) -> &str {
231        &self.file_name
232    }
233
234    /// Returns the settings file path.
235    pub fn path(&self) -> PathBuf {
236        self.folder_path.join(&self.file_name)
237    }
238
239    /// Saves the complete configuration, replacing the existing file content.
240    pub fn save(&self, config: &T) -> Result<()> {
241        let content = serialize(config, self.json_format)?;
242        save_to_path(&self.path(), &content, self.save_mode)
243    }
244
245    /// Loads a configuration file that is expected to already exist.
246    pub fn load(&self) -> Result<T> {
247        let content = fs::read_to_string(self.path())?;
248        deserialize(&content)
249    }
250}
251
252impl<T> Default for ConfigManager<T>
253where
254    T: Serialize + DeserializeOwned,
255{
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl<T> ConfigManager<T>
262where
263    T: Serialize + DeserializeOwned + Default,
264{
265    /// Loads the configuration, or creates and saves `T::default()` on first run.
266    pub fn load_or_default(&self) -> Result<T> {
267        let path = self.path();
268
269        match fs::read_to_string(&path) {
270            Ok(content) => deserialize(&content),
271
272            Err(e) if e.kind() == io::ErrorKind::NotFound => {
273                let default_config = T::default();
274                self.save(&default_config)?;
275                Ok(default_config)
276            }
277
278            Err(e) => Err(ConfigError::Io(e)),
279        }
280    }
281
282    /// Applies a read-modify-write update and saves the result.
283    pub fn update<F>(&self, f: F) -> Result<T>
284    where
285        F: FnOnce(&mut T),
286    {
287        let mut cfg = self.load_or_default()?;
288        f(&mut cfg);
289        self.save(&cfg)?;
290        Ok(cfg)
291    }
292}