config_file/
lib.rs

1#![deny(missing_docs)]
2#![warn(rust_2018_idioms)]
3#![doc(html_root_url = "https://docs.rs/config-file/0.2.3/")]
4
5//! # Read and parse configuration file automatically
6//!
7//! config-file reads your configuration files and parse them automatically using their extension.
8//!
9//! # Features
10//!
11//! - toml is enabled by default
12//! - json is optional
13//! - xml is optional
14//! - yaml is optional
15//!
16//! # Examples
17//!
18//! ```rust,no_run
19//! use config_file::FromConfigFile;
20//! use serde::Deserialize;
21//!
22//! #[derive(Deserialize)]
23//! struct Config {
24//!     host: String,
25//! }
26//!
27//! let config = Config::from_config_file("/etc/myconfig.toml").unwrap();
28//! ```
29
30use serde::de::DeserializeOwned;
31use std::{ffi::OsStr, fs::File, path::Path};
32use thiserror::Error;
33#[cfg(feature = "toml")]
34use toml_crate as toml;
35
36/// Trait for loading a struct from a configuration file.
37/// This trait is automatically implemented when serde::Deserialize is.
38pub trait FromConfigFile {
39    /// Load ourselves from the configuration file located at @path
40    fn from_config_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigFileError>
41    where
42        Self: Sized;
43}
44
45impl<C: DeserializeOwned> FromConfigFile for C {
46    fn from_config_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigFileError>
47    where
48        Self: Sized,
49    {
50        let path = path.as_ref();
51        let extension = path
52            .extension()
53            .and_then(OsStr::to_str)
54            .map(|extension| extension.to_lowercase());
55        match extension.as_deref() {
56            #[cfg(feature = "json")]
57            Some("json") => {
58                serde_json::from_reader(open_file(path)?).map_err(ConfigFileError::Json)
59            }
60            #[cfg(feature = "toml")]
61            Some("toml") => toml::from_str(
62                std::fs::read_to_string(path)
63                    .map_err(ConfigFileError::FileAccess)?
64                    .as_str(),
65            )
66            .map_err(ConfigFileError::Toml),
67            #[cfg(feature = "xml")]
68            Some("xml") => {
69                serde_xml_rs::from_reader(open_file(path)?).map_err(ConfigFileError::Xml)
70            }
71            #[cfg(feature = "yaml")]
72            Some("yaml") | Some("yml") => {
73                serde_yaml::from_reader(open_file(path)?).map_err(ConfigFileError::Yaml)
74            }
75            _ => Err(ConfigFileError::UnsupportedFormat),
76        }
77    }
78}
79
80#[allow(unused)]
81fn open_file(path: &Path) -> Result<File, ConfigFileError> {
82    File::open(path).map_err(ConfigFileError::FileAccess)
83}
84
85/// This type represents all possible errors that can occur when loading data from a configuration file.
86#[derive(Error, Debug)]
87pub enum ConfigFileError {
88    #[error("couldn't read config file")]
89    /// There was an error while reading the configuration file
90    FileAccess(#[from] std::io::Error),
91    #[cfg(feature = "json")]
92    #[error("couldn't parse JSON file")]
93    /// There was an error while parsing the JSON data
94    Json(#[from] serde_json::Error),
95    #[cfg(feature = "toml")]
96    #[error("couldn't parse TOML file")]
97    /// There was an error while parsing the TOML data
98    Toml(#[from] toml::de::Error),
99    #[cfg(feature = "xml")]
100    #[error("couldn't parse XML file")]
101    /// There was an error while parsing the XML data
102    Xml(#[from] serde_xml_rs::Error),
103    #[cfg(feature = "yaml")]
104    #[error("couldn't parse YAML file")]
105    /// There was an error while parsing the YAML data
106    Yaml(#[from] serde_yaml::Error),
107    #[error("don't know how to parse file")]
108    /// We don't know how to parse this format according to the file extension
109    UnsupportedFormat,
110}
111
112#[cfg(test)]
113mod test {
114    use super::*;
115
116    use serde::Deserialize;
117
118    #[derive(Debug, Deserialize, PartialEq)]
119    struct TestConfig {
120        host: String,
121        port: u64,
122        tags: Vec<String>,
123        inner: TestConfigInner,
124    }
125
126    #[derive(Debug, Deserialize, PartialEq)]
127    struct TestConfigInner {
128        answer: u8,
129    }
130
131    impl TestConfig {
132        #[allow(unused)]
133        fn example() -> Self {
134            Self {
135                host: "example.com".to_string(),
136                port: 443,
137                tags: vec!["example".to_string(), "test".to_string()],
138                inner: TestConfigInner { answer: 42 },
139            }
140        }
141    }
142
143    #[test]
144    fn test_unknown() {
145        let config = TestConfig::from_config_file("/tmp/foobar");
146        assert!(matches!(config, Err(ConfigFileError::UnsupportedFormat)));
147    }
148
149    #[test]
150    #[cfg(feature = "toml")]
151    fn test_file_not_found() {
152        let config = TestConfig::from_config_file("/tmp/foobar.toml");
153        assert!(matches!(config, Err(ConfigFileError::FileAccess(_))));
154    }
155
156    #[test]
157    #[cfg(feature = "json")]
158    fn test_json() {
159        let config = TestConfig::from_config_file("testdata/config.json");
160        assert_eq!(config.unwrap(), TestConfig::example());
161    }
162
163    #[test]
164    #[cfg(feature = "toml")]
165    fn test_toml() {
166        let config = TestConfig::from_config_file("testdata/config.toml");
167        assert_eq!(config.unwrap(), TestConfig::example());
168    }
169
170    #[test]
171    #[cfg(feature = "xml")]
172    fn test_xml() {
173        let config = TestConfig::from_config_file("testdata/config.xml");
174        assert_eq!(config.unwrap(), TestConfig::example());
175    }
176
177    #[test]
178    #[cfg(feature = "yaml")]
179    fn test_yaml() {
180        let config = TestConfig::from_config_file("testdata/config.yml");
181        assert_eq!(config.unwrap(), TestConfig::example());
182    }
183}