1#![deny(missing_docs)]
2#![warn(rust_2018_idioms)]
3#![doc(html_root_url = "https://docs.rs/config-file/0.2.3/")]
4
5use serde::de::DeserializeOwned;
31use std::{ffi::OsStr, fs::File, path::Path};
32use thiserror::Error;
33#[cfg(feature = "toml")]
34use toml_crate as toml;
35
36pub trait FromConfigFile {
39 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#[derive(Error, Debug)]
87pub enum ConfigFileError {
88 #[error("couldn't read config file")]
89 FileAccess(#[from] std::io::Error),
91 #[cfg(feature = "json")]
92 #[error("couldn't parse JSON file")]
93 Json(#[from] serde_json::Error),
95 #[cfg(feature = "toml")]
96 #[error("couldn't parse TOML file")]
97 Toml(#[from] toml::de::Error),
99 #[cfg(feature = "xml")]
100 #[error("couldn't parse XML file")]
101 Xml(#[from] serde_xml_rs::Error),
103 #[cfg(feature = "yaml")]
104 #[error("couldn't parse YAML file")]
105 Yaml(#[from] serde_yaml::Error),
107 #[error("don't know how to parse file")]
108 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}