home_config/
lib.rs

1//! Usage
2//! ```no_run
3//! use home_config::HomeConfig;
4//!
5//! let config = HomeConfig::with_config_dir("app", "config");
6//! // Linux: /home/name/.config/app/config
7//! // macOS: /Users/name/.config/app/config
8//! // Windows: C:\Users\name\.config\app\config
9//!
10//! // Write
11//! config.save("123456789").unwrap();
12//!
13//! // Read
14//! let data = config.read_to_string().unwrap();
15//! // 123456789
16//! ```
17//!
18//! ### Serde format support
19//!
20//! * [JSON](https://www.json.org/)
21//! * [YAML](https://yaml.org/)
22//! * [TOML](https://toml.io/)
23//! * [HCL](https://github.com/hashicorp/hcl)
24//!
25//! ```toml
26//! home-config = { version = "*", features = ["json", "yaml", "toml", "hcl"] }
27//! ```
28//!
29//! A `JSON` example:
30//!
31//! ```no_run
32//! use home_config::HomeConfig;
33//! use serde::{Deserialize, Serialize};
34//!
35//! #[derive(Serialize, Deserialize, Default)]
36//! struct People {
37//!     name: String,
38//!     age: u32,
39//! }
40//!
41//! let config = HomeConfig::with_file("test.json");
42//! // Linux: /home/name/test.json
43//! // macOS: /Users/name/test.json
44//! // Windows: C:\Users\name\test.json
45//!
46//! // Parse
47//! let people = config.json::<People>().unwrap();
48//! // people.name == "XiaoMing";
49//! // people.age == 18;
50//!
51//! // Save to file
52//! config.save_json(&people).unwrap();
53//! ```
54
55#[cfg(any(feature = "json", feature = "yaml", feature = "toml", feature = "hcl"))]
56use serde::{de::DeserializeOwned, Serialize};
57use std::fs::{self, File};
58#[cfg(any(feature = "json", feature = "yaml", feature = "toml", feature = "hcl"))]
59use std::io::Error as IoError;
60use std::io::{ErrorKind, Read, Result as IoResult};
61use std::path::{Path, PathBuf};
62
63fn home_dir() -> PathBuf {
64    dirs::home_dir().expect("Get home dir")
65}
66
67/// Serde `json` error
68#[derive(Debug)]
69#[cfg(feature = "json")]
70pub enum JsonError {
71    Io(IoError),
72    Serde(serde_json::Error),
73}
74
75/// Serde `yaml` error
76#[derive(Debug)]
77#[cfg(feature = "yaml")]
78pub enum YamlError {
79    Io(IoError),
80    Serde(serde_yaml::Error),
81}
82
83/// Serde `toml` parse error
84#[derive(Debug)]
85#[cfg(feature = "toml")]
86pub enum TomlParseError {
87    Io(IoError),
88    Serde(toml::de::Error),
89}
90
91/// Serde `toml` save error
92#[derive(Debug)]
93#[cfg(feature = "toml")]
94pub enum TomlSaveError {
95    Io(IoError),
96    Serde(toml::ser::Error),
97}
98
99/// Serde `hcl` error
100#[derive(Debug)]
101#[cfg(feature = "hcl")]
102pub enum HclError {
103    Io(IoError),
104    Serde(hcl::Error),
105}
106
107/// Use the configuration file in the current user directory
108#[derive(Debug, Clone)]
109pub struct HomeConfig {
110    path: PathBuf,
111}
112
113impl HomeConfig {
114    /// Parse or create configuration file
115    ///
116    /// eg. `/home/name/.config/app/config`
117    pub fn with_config_dir<P: AsRef<Path>>(app_name: &'static str, file_name: P) -> Self {
118        Self {
119            path: home_dir().join(".config").join(app_name).join(file_name),
120        }
121    }
122
123    /// Parse or create configuration file
124    ///
125    /// eg. `/home/name/test.json`
126    pub fn with_file<P: AsRef<Path>>(p: P) -> Self {
127        Self {
128            path: home_dir().join(p),
129        }
130    }
131
132    /// Get the configuration file path
133    pub fn path(&self) -> &PathBuf {
134        &self.path
135    }
136
137    /// Read the entire contents of a file into a string
138    pub fn read_to_string(&self) -> IoResult<String> {
139        fs::read_to_string(&self.path)
140    }
141
142    /// Read the entire contents of a file into a `Vec<u8>`
143    pub fn read_to_vec(&self) -> IoResult<Vec<u8>> {
144        let mut f = File::open(&self.path)?;
145        let mut buf = Vec::new();
146        f.read_to_end(&mut buf)?;
147        Ok(buf)
148    }
149
150    /// Parse the config file from `json` content
151    #[cfg(feature = "json")]
152    pub fn json<T>(&self) -> Result<T, JsonError>
153    where
154        T: DeserializeOwned,
155    {
156        let f = File::open(&self.path).map_err(JsonError::Io)?;
157        serde_json::from_reader(f).map_err(JsonError::Serde)
158    }
159
160    /// Parse the config file from `yaml` content
161    #[cfg(feature = "yaml")]
162    pub fn yaml<T>(&self) -> Result<T, YamlError>
163    where
164        T: DeserializeOwned,
165    {
166        let f = File::open(&self.path).map_err(YamlError::Io)?;
167        serde_yaml::from_reader(f).map_err(YamlError::Serde)
168    }
169
170    /// Parse the config file from `toml` content
171    #[cfg(feature = "toml")]
172    pub fn toml<T>(&self) -> Result<T, TomlParseError>
173    where
174        T: DeserializeOwned,
175    {
176        let bytes = self.read_to_vec().map_err(TomlParseError::Io)?;
177        toml::from_slice(&bytes).map_err(TomlParseError::Serde)
178    }
179
180    /// Parse the config file from `hcl` content
181    #[cfg(feature = "hcl")]
182    pub fn hcl<T>(&self) -> Result<T, HclError>
183    where
184        T: DeserializeOwned,
185    {
186        let f = File::open(&self.path).map_err(HclError::Io)?;
187        hcl::from_reader(f).map_err(HclError::Serde)
188    }
189
190    fn create_parent_dir(&self) -> IoResult<()> {
191        if !self.path.exists() {
192            if let Some(parent) = self.path.parent() {
193                fs::create_dir_all(parent)?;
194            }
195        }
196        Ok(())
197    }
198
199    /// Save content to local file
200    pub fn save<T: AsRef<[u8]>>(&self, data: T) -> IoResult<()> {
201        self.create_parent_dir()?;
202        fs::write(&self.path, data.as_ref())
203    }
204
205    /// Save struct to local file (`json` format)
206    #[cfg(feature = "json")]
207    pub fn save_json<T>(&self, data: T) -> Result<(), JsonError>
208    where
209        T: Serialize,
210    {
211        let bytes = serde_json::to_vec_pretty(&data).map_err(JsonError::Serde)?;
212        self.create_parent_dir().map_err(JsonError::Io)?;
213        fs::write(&self.path, &bytes).map_err(JsonError::Io)?;
214        Ok(())
215    }
216
217    /// Save struct to local file (`yaml` format)
218    #[cfg(feature = "yaml")]
219    pub fn save_yaml<T>(&self, data: T) -> Result<(), YamlError>
220    where
221        T: Serialize,
222    {
223        let bytes = serde_yaml::to_string(&data).map_err(YamlError::Serde)?;
224        self.create_parent_dir().map_err(YamlError::Io)?;
225        fs::write(&self.path, &bytes).map_err(YamlError::Io)?;
226        Ok(())
227    }
228
229    /// Save struct to local file (`toml` format)
230    #[cfg(feature = "toml")]
231    pub fn save_toml<T>(&self, data: T) -> Result<(), TomlSaveError>
232    where
233        T: Serialize,
234    {
235        let bytes = toml::to_string_pretty(&data).map_err(TomlSaveError::Serde)?;
236        self.create_parent_dir().map_err(TomlSaveError::Io)?;
237        fs::write(&self.path, &bytes).map_err(TomlSaveError::Io)?;
238        Ok(())
239    }
240
241    /// Save struct to local file (`hcl` format)
242    #[cfg(feature = "hcl")]
243    pub fn save_hcl<T>(&self, data: T) -> Result<(), HclError>
244    where
245        T: Serialize,
246    {
247        let bytes = hcl::to_vec(&data).map_err(HclError::Serde)?;
248        self.create_parent_dir().map_err(HclError::Io)?;
249        fs::write(&self.path, &bytes).map_err(HclError::Io)?;
250        Ok(())
251    }
252
253    /// Delete the config file
254    pub fn delete(&self) -> IoResult<()> {
255        match fs::remove_file(&self.path) {
256            Ok(()) => Ok(()),
257            Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
258            Err(err) => Err(err),
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use crate::*;
266
267    #[test]
268    fn test_content() {
269        let config = HomeConfig::with_config_dir("test", "file");
270        // Save
271        config.save("123").unwrap();
272        // Read
273        assert_eq!(config.read_to_string().unwrap(), "123");
274    }
275
276    #[test]
277    fn test_delete() {
278        let config = HomeConfig::with_config_dir("test", "delete");
279
280        assert!(!config.path().exists());
281
282        config.save("0").unwrap();
283        assert!(config.path().exists());
284
285        config.delete().unwrap();
286        assert!(!config.path().exists());
287    }
288
289    #[cfg(any(feature = "json", feature = "yaml", feature = "toml", feature = "hcl"))]
290    use serde::{Deserialize, Serialize};
291
292    #[cfg(any(feature = "json", feature = "yaml", feature = "toml", feature = "hcl"))]
293    #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
294    struct People {
295        name: String,
296        age: u32,
297    }
298
299    #[test]
300    #[cfg(feature = "json")]
301    fn test_json() {
302        let config = HomeConfig::with_config_dir("test", "config.json");
303        let data = People {
304            name: "123".to_string(),
305            age: 18,
306        };
307        config.save_json(&data).unwrap();
308        assert_eq!(config.json::<People>().unwrap(), data);
309    }
310
311    #[test]
312    #[cfg(feature = "yaml")]
313    fn test_yaml() {
314        let config = HomeConfig::with_config_dir("test", "config.yaml");
315        let data = People {
316            name: "123".to_string(),
317            age: 18,
318        };
319        config.save_yaml(&data).unwrap();
320        assert_eq!(config.yaml::<People>().unwrap(), data);
321    }
322
323    #[test]
324    #[cfg(feature = "toml")]
325    fn test_toml() {
326        let config = HomeConfig::with_config_dir("test", "config.toml");
327        let data = People {
328            name: "123".to_string(),
329            age: 18,
330        };
331        config.save_toml(&data).unwrap();
332        assert_eq!(config.toml::<People>().unwrap(), data);
333    }
334
335    #[test]
336    #[cfg(feature = "hcl")]
337    fn test_hcl() {
338        let config = HomeConfig::with_config_dir("test", "config.hcl");
339        let data = People {
340            name: "123".to_string(),
341            age: 18,
342        };
343        config.save_hcl(&data).unwrap();
344        assert_eq!(config.hcl::<People>().unwrap(), data);
345    }
346}