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}