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::{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 storage-root resolution failure without
62 /// an API break, so it falls back to the current directory when the
63 /// platform configuration directory cannot be determined (for example, no
64 /// `HOME` or `%APPDATA%` in the environment). For production applications,
65 /// prefer [`for_app`](Self::for_app): it uses an explicit stable
66 /// application identity instead of deriving one from the executable file
67 /// name, and it reports resolution failure instead of silently falling
68 /// back.
69 pub fn new() -> Self {
70 let folder_path = default_config_dir()
71 .unwrap_or_else(|_| PathBuf::from("."))
72 .join(default_runtime_app_name());
73 Self::from_parts(folder_path, DEFAULT_FILE_NAME)
74 }
75
76 /// Creates a config manager for an explicit application name.
77 ///
78 /// This is the recommended desktop constructor for production apps because
79 /// the storage directory is stable even if the executable file name changes.
80 /// The `app_name` must be a single safe path component, not a path.
81 ///
82 /// Returns [`ConfigError::Platform`] if the platform configuration
83 /// directory cannot be resolved (for example, no `HOME` or `%APPDATA%` in
84 /// the environment). Applications that hit this in practice — typically
85 /// services or containers without a user environment — should supply a
86 /// path explicitly with [`with_root_dir`](Self::with_root_dir) instead.
87 pub fn for_app(app_name: &str) -> Result<Self> {
88 let app_name = validate_path_component(app_name)?;
89 let folder_path = default_config_dir()?.join(app_name);
90 Ok(Self::from_parts(folder_path, DEFAULT_FILE_NAME))
91 }
92
93 fn from_parts<P>(folder_path: P, file_name: &str) -> Self
94 where
95 P: Into<PathBuf>,
96 {
97 Self {
98 folder_path: folder_path.into(),
99 file_name: file_name.to_string(),
100 json_format: JsonFormat::Pretty,
101 save_mode: SaveMode::Atomic,
102 _marker: PhantomData,
103 }
104 }
105
106 /// Stores the settings file in the current working directory.
107 ///
108 /// If the current working directory cannot be determined, falls back to
109 /// `"."`. Unlike [`for_app`](Self::for_app), this fallback is not
110 /// surprising here: the caller explicitly asked for working-directory
111 /// storage, and `"."` already means "the working directory" to the
112 /// filesystem.
113 pub fn at_current_dir(mut self) -> Self {
114 self.folder_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
115 self
116 }
117
118 /// Stores the settings file in a caller-provided directory.
119 ///
120 /// This is the primary compatibility seam for sandboxed hosts, including
121 /// Pure UWP. The host application may resolve its application data directory
122 /// and pass it here.
123 pub fn with_root_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
124 self.folder_path = path.into();
125 self
126 }
127
128 /// Stores the settings file in a caller-provided directory.
129 ///
130 /// This method is kept for compatibility with v2.0.x. Prefer
131 /// [`with_root_dir`](Self::with_root_dir) in new code.
132 pub fn at_custom_dir<P: Into<PathBuf>>(self, path: P) -> Self {
133 self.with_root_dir(path)
134 }
135
136 /// Stores the settings file under `ApplicationData.Current.LocalFolder`.
137 ///
138 /// This method is available only on Windows when the optional `uwp` feature
139 /// is enabled.
140 #[cfg(all(windows, feature = "uwp"))]
141 pub fn at_uwp_local_folder(mut self) -> Result<Self> {
142 self.folder_path = uwp_local_folder_dir()?;
143 Ok(self)
144 }
145
146 /// Changes the settings file name without validation.
147 ///
148 /// This method is retained for v2.x compatibility. New code should prefer
149 /// [`try_with_filename`](Self::try_with_filename), which rejects path-like
150 /// names such as `../settings.json`.
151 pub fn with_filename(mut self, name: &str) -> Self {
152 self.file_name = name.to_string();
153 self
154 }
155
156 /// Changes the settings file name after validating it as a plain file name.
157 ///
158 /// The accepted value must be a single file name, not an absolute path and
159 /// not a relative path containing directory traversal.
160 pub fn try_with_filename(mut self, name: &str) -> Result<Self> {
161 self.file_name = validate_plain_file_name(name)?.to_string();
162 Ok(self)
163 }
164
165 /// Stores JSON in compact form instead of pretty-printed form.
166 pub fn disable_pretty_json(mut self) -> Self {
167 self.json_format = JsonFormat::Compact;
168 self
169 }
170
171 /// Selects the save strategy.
172 ///
173 /// The default is [`SaveMode::Atomic`]. Use [`SaveMode::Direct`] only when
174 /// an application intentionally wants v2.2-style direct overwrite behavior
175 /// or needs to work around unusual filesystem semantics.
176 pub fn with_save_mode(mut self, mode: SaveMode) -> Self {
177 self.save_mode = mode;
178 self
179 }
180
181 /// Selects direct overwrite saves.
182 ///
183 /// This is a convenience alias for `with_save_mode(SaveMode::Direct)`.
184 pub fn with_direct_save(self) -> Self {
185 self.with_save_mode(SaveMode::Direct)
186 }
187
188 /// Returns the configured save strategy.
189 pub fn save_mode(&self) -> SaveMode {
190 self.save_mode
191 }
192
193 /// Returns the settings folder path.
194 pub fn folder_path(&self) -> &Path {
195 &self.folder_path
196 }
197
198 /// Returns the settings file name.
199 pub fn file_name(&self) -> &str {
200 &self.file_name
201 }
202
203 /// Returns the settings file path.
204 pub fn path(&self) -> PathBuf {
205 self.folder_path.join(&self.file_name)
206 }
207
208 /// Saves the complete configuration, replacing the existing file content.
209 pub fn save(&self, config: &T) -> Result<()> {
210 let content = serialize(config, self.json_format)?;
211 save_to_path(&self.path(), &content, self.save_mode)
212 }
213
214 /// Loads a configuration file that is expected to already exist.
215 pub fn load(&self) -> Result<T> {
216 let content = fs::read_to_string(self.path())?;
217 deserialize(&content)
218 }
219}
220
221impl<T> Default for ConfigManager<T>
222where
223 T: Serialize + DeserializeOwned,
224{
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl<T> ConfigManager<T>
231where
232 T: Serialize + DeserializeOwned + Default,
233{
234 /// Loads the configuration, or creates and saves `T::default()` on first run.
235 pub fn load_or_default(&self) -> Result<T> {
236 let path = self.path();
237
238 match fs::read_to_string(&path) {
239 Ok(content) => deserialize(&content),
240
241 Err(e) if e.kind() == io::ErrorKind::NotFound => {
242 let default_config = T::default();
243 self.save(&default_config)?;
244 Ok(default_config)
245 }
246
247 Err(e) => Err(ConfigError::Io(e)),
248 }
249 }
250
251 /// Applies a read-modify-write update and saves the result.
252 pub fn update<F>(&self, f: F) -> Result<T>
253 where
254 F: FnOnce(&mut T),
255 {
256 let mut cfg = self.load_or_default()?;
257 f(&mut cfg);
258 self.save(&cfg)?;
259 Ok(cfg)
260 }
261}