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;
11use crate::core::dir::{default_config_dir, default_runtime_app_name};
12#[cfg(all(windows, feature = "uwp"))]
13use crate::core::dir::uwp_local_folder_dir;
14use crate::core::json::{JsonFormat, deserialize, serialize};
15use crate::core::validation::{validate_path_component, validate_plain_file_name};
16
17pub mod constant;
18mod dir;
19pub mod error;
20mod json;
21pub mod validation;
22
23#[cfg(test)]
24mod tests;
25
26/// Manages one typed JSON settings file.
27///
28/// `ConfigManager<T>` stores and loads a complete configuration value of type
29/// `T`. The type must implement Serde `Serialize` and `DeserializeOwned`.
30///
31/// The manager is intentionally small. It owns only:
32///
33/// * the directory containing the settings file,
34/// * the settings file name, and
35/// * the JSON output format.
36#[derive(Debug, Clone)]
37pub struct ConfigManager<T> {
38    folder_path: PathBuf,
39    file_name: String,
40    json_format: JsonFormat,
41    _marker: PhantomData<T>,
42}
43
44impl<T> ConfigManager<T>
45where
46    T: Serialize + DeserializeOwned,
47{
48    /// Creates a config manager using the OS-standard config directory and the
49    /// current executable name.
50    ///
51    /// On Windows desktop apps this resolves under `%APPDATA%`. Pure UWP apps
52    /// should use [`with_root_dir`](Self::with_root_dir) or the optional
53    /// `uwp` feature instead.
54    ///
55    /// For production applications, prefer [`for_app`](Self::for_app) because
56    /// it uses an explicit stable application identity instead of deriving one
57    /// from the executable file name.
58    pub fn new() -> Self {
59        Self::from_parts(default_config_dir().join(default_runtime_app_name()), DEFAULT_FILE_NAME)
60    }
61
62    /// Creates a config manager for an explicit application name.
63    ///
64    /// This is the recommended desktop constructor for production apps because
65    /// the storage directory is stable even if the executable file name changes.
66    /// The `app_name` must be a single safe path component, not a path.
67    pub fn for_app(app_name: &str) -> Result<Self> {
68        let app_name = validate_path_component(app_name)?;
69        Ok(Self::from_parts(
70            default_config_dir().join(app_name),
71            DEFAULT_FILE_NAME,
72        ))
73    }
74
75    fn from_parts<P>(folder_path: P, file_name: &str) -> Self
76    where
77        P: Into<PathBuf>,
78    {
79        Self {
80            folder_path: folder_path.into(),
81            file_name: file_name.to_string(),
82            json_format: JsonFormat::Pretty,
83            _marker: PhantomData,
84        }
85    }
86
87    /// Stores the settings file in the current working directory.
88    pub fn at_current_dir(mut self) -> Self {
89        self.folder_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
90        self
91    }
92
93    /// Stores the settings file in a caller-provided directory.
94    ///
95    /// This is the primary compatibility seam for sandboxed hosts, including
96    /// Pure UWP. The host application may resolve its application data directory
97    /// and pass it here.
98    pub fn with_root_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
99        self.folder_path = path.into();
100        self
101    }
102
103    /// Stores the settings file in a caller-provided directory.
104    ///
105    /// This method is kept for compatibility with v2.0.x. Prefer
106    /// [`with_root_dir`](Self::with_root_dir) in new code.
107    pub fn at_custom_dir<P: Into<PathBuf>>(self, path: P) -> Self {
108        self.with_root_dir(path)
109    }
110
111    /// Stores the settings file under `ApplicationData.Current.LocalFolder`.
112    ///
113    /// This method is available only on Windows when the optional `uwp` feature
114    /// is enabled.
115    #[cfg(all(windows, feature = "uwp"))]
116    pub fn at_uwp_local_folder(mut self) -> Result<Self> {
117        self.folder_path = uwp_local_folder_dir()?;
118        Ok(self)
119    }
120
121    /// Changes the settings file name without validation.
122    ///
123    /// This method is retained for v2.x compatibility. New code should prefer
124    /// [`try_with_filename`](Self::try_with_filename), which rejects path-like
125    /// names such as `../settings.json`.
126    pub fn with_filename(mut self, name: &str) -> Self {
127        self.file_name = name.to_string();
128        self
129    }
130
131    /// Changes the settings file name after validating it as a plain file name.
132    ///
133    /// The accepted value must be a single file name, not an absolute path and
134    /// not a relative path containing directory traversal.
135    pub fn try_with_filename(mut self, name: &str) -> Result<Self> {
136        self.file_name = validate_plain_file_name(name)?.to_string();
137        Ok(self)
138    }
139
140    /// Stores JSON in compact form instead of pretty-printed form.
141    pub fn disable_pretty_json(mut self) -> Self {
142        self.json_format = JsonFormat::Compact;
143        self
144    }
145
146    /// Returns the settings folder path.
147    pub fn folder_path(&self) -> &Path {
148        &self.folder_path
149    }
150
151    /// Returns the settings file name.
152    pub fn file_name(&self) -> &str {
153        &self.file_name
154    }
155
156    /// Returns the settings file path.
157    pub fn path(&self) -> PathBuf {
158        self.folder_path.join(&self.file_name)
159    }
160
161    /// Saves the complete configuration, replacing the existing file content.
162    pub fn save(&self, config: &T) -> Result<()> {
163        fs::create_dir_all(&self.folder_path)?;
164        fs::write(self.path(), serialize(config, self.json_format)?)?;
165        Ok(())
166    }
167
168    /// Loads a configuration file that is expected to already exist.
169    pub fn load(&self) -> Result<T> {
170        let content = fs::read_to_string(self.path())?;
171        deserialize(&content)
172    }
173}
174
175impl<T> Default for ConfigManager<T>
176where
177    T: Serialize + DeserializeOwned,
178{
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184impl<T> ConfigManager<T>
185where
186    T: Serialize + DeserializeOwned + Default,
187{
188    /// Loads the configuration, or creates and saves `T::default()` on first run.
189    pub fn load_or_default(&self) -> Result<T> {
190        let path = self.path();
191
192        match fs::read_to_string(&path) {
193            Ok(content) => deserialize(&content),
194
195            Err(e) if e.kind() == io::ErrorKind::NotFound => {
196                let default_config = T::default();
197                self.save(&default_config)?;
198                Ok(default_config)
199            }
200
201            Err(e) => Err(ConfigError::Io(e)),
202        }
203    }
204
205    /// Applies a read-modify-write update and saves the result.
206    pub fn update<F>(&self, f: F) -> Result<T>
207    where
208        F: FnOnce(&mut T),
209    {
210        let mut cfg = self.load_or_default()?;
211        f(&mut cfg);
212        self.save(&cfg)?;
213        Ok(cfg)
214    }
215}