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