toml_config/
lib.rs

1#![cfg_attr(feature = "serde-serialization", feature(custom_derive, plugin))]
2#![cfg_attr(feature = "serde-serialization", plugin(serde_macros))]
3
4//! # Configuring your dependency
5//! `toml-config` can be configured to use `rustc_serialize` or `serde`
6//!
7//! ### Using `toml-config` with `rustc_serialize`
8//! By default `toml-config` uses `rustc_serialize`, so just add the dependency in Cargo.toml normally:
9//!
10//! ```toml
11//! [dependencies]
12//! toml-config = "0.2"
13//! ```
14//! ### Using `toml-config` with `serde`
15//! To use toml-config with `serde`, opt out from the default features and enable the `serde-serialization`
16//! feature:
17//!
18//! ```toml
19//! [dependencies.toml-config]
20//! version = "0.2"
21//! default-features = false
22//! features = ["serde-serialization"]
23//! ```
24
25#[cfg(feature = "rustc-serialize")] extern crate rustc_serialize;
26#[cfg(feature = "serde-serialization")] extern crate serde;
27
28#[macro_use] extern crate log;
29
30extern crate toml;
31
32#[cfg(feature = "rustc-serialize")] use rustc_serialize::{Encodable, Decodable};
33#[cfg(feature = "serde-serialization")] use serde::{Serialize, Deserialize};
34
35
36use std::io::Read;
37use std::fs::File;
38use std::path::Path;
39
40/// Implements helper functions for loading TOML files into a structure
41///
42/// # Examples
43/// To load a file into a Config struct, use `ConfigFactory`.
44///
45/// Either `rustc_serialize` or `serde` can be used for serialization.
46///
47/// ## Example using rustc_serialize
48/// ```no_run
49/// # #[cfg(feature = "rustc-serialize")]
50/// extern crate rustc_serialize;
51/// extern crate toml_config;
52///
53/// # #[cfg(feature = "rustc-serialize")]
54/// # fn main() {
55/// use rustc_serialize::{Encodable, Decodable};
56/// use std::path::Path;
57/// use toml_config::ConfigFactory;
58///
59/// #[derive(RustcEncodable, RustcDecodable)]
60/// struct Config  {
61///     nested: NestedConfig
62/// }
63///
64/// // Defaults will be used for missing/invalid configurations in the TOML config file
65/// impl Default for Config {
66///     fn default() -> Config {
67///         Config {
68///             nested: NestedConfig::default()
69///         }
70///     }
71/// }
72///
73/// #[derive(RustcEncodable, RustcDecodable)]
74/// struct NestedConfig  {
75///     value: String,
76///     values: Vec<u16>
77/// }
78///
79/// impl Default for NestedConfig {
80///     fn default() -> NestedConfig {
81///         NestedConfig {
82///             value: "default".to_owned(),
83///             values: vec![0, 0, 0]
84///         }
85///     }
86/// }
87///
88/// /* config.toml:
89///  * [nested]
90///  * value = "test"
91///  * values = [1, 2, 3]
92///  */
93///
94/// let config: Config = ConfigFactory::load(Path::new("config.toml"));
95/// assert_eq!(config.nested.value, "test");
96/// assert_eq!(config.nested.values, vec![1, 2, 3]);
97/// # }
98/// # #[cfg(feature = "serde-serialization")] fn main() { }
99/// ```
100/// ## Example using serde
101/// ```no_run
102/// # #![cfg_attr(feature = "serde-serialization", feature(custom_derive, plugin))]
103/// # #![cfg_attr(feature = "serde-serialization", plugin(serde_macros))]
104/// # #[cfg(feature = "serde-serialization")]
105/// extern crate serde;
106/// extern crate toml_config;
107///
108/// # #[cfg(feature = "serde-serialization")]
109/// # fn main() {
110/// use serde::{Serialize, Deserialize};
111/// use std::path::Path;
112/// use toml_config::ConfigFactory;
113///
114/// #[derive(Serialize, Deserialize)]
115/// struct Config  {
116///     nested: NestedConfig
117/// }
118///
119/// // Defaults will be used for missing/invalid configurations in the TOML config file
120/// impl Default for Config {
121///     fn default() -> Config {
122///         Config {
123///             nested: NestedConfig::default()
124///         }
125///     }
126/// }
127///
128/// #[derive(Serialize, Deserialize)]
129/// struct NestedConfig  {
130///     value: String,
131///     values: Vec<u16>
132/// }
133///
134/// impl Default for NestedConfig {
135///     fn default() -> NestedConfig {
136///         NestedConfig {
137///             value: "default".to_owned(),
138///             values: vec![0, 0, 0]
139///         }
140///     }
141/// }
142///
143/// /* config.toml:
144///  * [nested]
145///  * value = "test"
146///  * values = [1, 2, 3]
147///  */
148///
149/// let config: Config = ConfigFactory::load(Path::new("config.toml"));
150/// assert_eq!(config.nested.value, "test");
151/// assert_eq!(config.nested.values, vec![1, 2, 3]);
152/// # }
153/// # #[cfg(feature = "rustc-serialize")] fn main() { }
154/// ```
155pub struct ConfigFactory;
156
157impl ConfigFactory {
158    /// Loads a TOML file and decodes it into a target structure, using default values
159    /// for missing or invalid file configurations
160    #[cfg(feature = "rustc-serialize")]
161    pub fn load<T>(path: &Path) -> T where T: Encodable + Decodable + Default {
162        match ConfigFactory::parse_toml_file(path) {
163            Some(toml_table) => ConfigFactory::decode(toml_table),
164            None => T::default()
165        }
166    }
167
168    /// Loads a TOML file and decodes it into a target structure, using default values
169    /// for missing or invalid file configurations
170    #[cfg(feature = "serde-serialization")]
171    pub fn load<T>(path: &Path) -> T where T: Serialize + Deserialize + Default {
172        match ConfigFactory::parse_toml_file(path) {
173            Some(toml_table) => ConfigFactory::decode(toml_table),
174            None => T::default()
175        }
176    }
177
178    /// Decodes a TOML table into a target structure, using default values
179    /// for missing or invalid configurations
180    #[cfg(feature = "rustc-serialize")]
181    pub fn decode<T>(toml_table: toml::Table) -> T where T: Encodable + Decodable + Default {
182        let default_table = toml::encode(&T::default()).as_table().unwrap().clone();
183        let table_with_overrides = ConfigFactory::apply_overrides(default_table, toml_table);
184        toml::decode(toml::Value::Table(table_with_overrides)).unwrap()
185    }
186
187    /// Decodes a TOML table into a target structure, using default values
188    /// for missing or invalid configurations
189    #[cfg(feature = "serde-serialization")]
190    pub fn decode<T>(toml_table: toml::Table) -> T where T: Serialize + Deserialize + Default {
191        let default_table = toml::encode(&T::default()).as_table().unwrap().clone();
192        let table_with_overrides = ConfigFactory::apply_overrides(default_table, toml_table);
193        toml::decode(toml::Value::Table(table_with_overrides)).unwrap()
194    }
195
196    fn parse_toml_file(path: &Path) -> Option<toml::Table> {
197        let mut toml_config = String::new();
198
199        let mut file = match File::open(path) {
200            Ok(file) => file,
201            Err(_)  => {
202                warn!("Config file not found: {}, using defaults..", path.display());
203                return None;
204            }
205        };
206
207        file.read_to_string(&mut toml_config)
208            .unwrap_or_else(|err| panic!("Unable to read config file: {}", err));
209
210        let mut parser = toml::Parser::new(&toml_config);
211        let toml_table = parser.parse();
212
213        if toml_table.is_none() {
214            for err in &parser.errors {
215                let (line, col) = parser.to_linecol(err.lo);
216                error!("Parsing of {} failed [{}:{}] - {}", path.display(), line + 1, col + 1, err.desc);
217            }
218            error!("Unable to parse config file: {}, using defaults..", path.display());
219            return None;
220        }
221
222        toml_table
223    }
224
225    fn apply_overrides(defaults: toml::Table, overrides: toml::Table) -> toml::Table {
226        let mut merged = defaults.clone();
227        for (k, v) in overrides.into_iter() {
228            match defaults.get(&k) {
229                Some(default_value) =>
230                    if v.type_str() == default_value.type_str() {
231                        match v {
232                            toml::Value::Table(overrides_table) => {
233                                let defaults_table = default_value.as_table().unwrap().clone();
234                                let merged_table = ConfigFactory::apply_overrides(defaults_table, overrides_table);
235                                merged.insert(k, toml::Value::Table(merged_table))
236                            },
237                            any => merged.insert(k, any)
238                        };
239                    } else {
240                        warn!("Wrong type for config: {}. Expected: {}, found: {}, using default value..",
241                            k, default_value.type_str(), v.type_str());
242                    },
243                None => {
244                    merged.insert(k, v);
245                }
246            }
247        }
248        merged
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::ConfigFactory;
255
256    extern crate tempfile;
257    use std::io::Write;
258
259    macro_rules! tempfile {
260        ($content:expr) => {
261            {
262                let mut f = tempfile::NamedTempFile::new().unwrap();
263                f.write_all($content.as_bytes()).unwrap();
264                f.flush().unwrap();
265                f
266            }
267        }
268    }
269
270    #[cfg_attr(feature = "rustc-serialize", derive(RustcEncodable, RustcDecodable))]
271    #[cfg_attr(feature = "serde-serialization", derive(Serialize, Deserialize))]
272    pub struct Config  {
273        pub nested: NestedConfig,
274        pub optional: Option<String>
275    }
276
277    impl Default for Config {
278        fn default() -> Config {
279            Config {
280                nested: NestedConfig::default(),
281                optional: None
282            }
283        }
284    }
285
286    #[cfg_attr(feature = "rustc-serialize", derive(RustcEncodable, RustcDecodable))]
287    #[cfg_attr(feature = "serde-serialization", derive(Serialize, Deserialize))]
288    pub struct NestedConfig  {
289        pub value: String,
290        pub values: Vec<u16>
291    }
292
293    impl Default for NestedConfig {
294        fn default() -> NestedConfig {
295            NestedConfig {
296                value: "default".to_owned(),
297                values: vec![0, 0, 0]
298            }
299        }
300    }
301
302    #[test]
303    fn it_should_parse_and_decode_a_valid_toml_file() {
304        let toml_file = tempfile!(r#"
305            optional = "override"
306            [nested]
307            value = "test"
308            values = [1, 2, 3]
309        "#);
310        let config: Config = ConfigFactory::load(toml_file.path());
311        assert_eq!(config.nested.value, "test");
312        assert_eq!(config.nested.values, vec![1, 2, 3]);
313        assert_eq!(config.optional, Some("override".to_owned()));
314    }
315
316    #[test]
317    fn it_should_use_the_default_for_missing_config() {
318        let toml_file = tempfile!("");
319        let config: Config = ConfigFactory::load(toml_file.path());
320        assert_eq!(config.nested.value, "default");
321        assert_eq!(config.nested.values, vec![0, 0, 0]);
322    }
323
324    #[test]
325    fn it_should_use_the_default_values_for_missing_config_values() {
326        let toml_file = tempfile!(r#"
327            [nested]
328            value = "test"
329        "#);
330        let config: Config = ConfigFactory::load(toml_file.path());
331        assert_eq!(config.nested.value, "test");
332        assert_eq!(config.nested.values, vec![0, 0, 0]);
333    }
334
335    #[test]
336    fn it_should_return_the_default_config_value_for_misconfigured_properties() {
337        let toml_file = tempfile!(r#"
338            [nested]
339            value = "test"
340            values = "wrong-type"
341        "#);
342        let config: Config = ConfigFactory::load(toml_file.path());
343        assert_eq!(config.nested.value, "test");
344        assert_eq!(config.nested.values, vec![0, 0, 0]);
345    }
346
347    #[test]
348    fn it_should_ignore_unexpected_config() {
349        let toml_file = tempfile!(r#"
350            [nested]
351            value = "test"
352            values = [1, 2, 3]
353            unexpected = "ignore-me"
354        "#);
355        let config: Config = ConfigFactory::load(toml_file.path());
356        assert_eq!(config.nested.value, "test");
357        assert_eq!(config.nested.values, vec![1, 2, 3]);
358    }
359
360    #[test]
361    fn it_should_return_the_default_config_when_parsing_fails() {
362        let toml_file = tempfile!(r#"
363            [nested]
364            value = "test
365            values = [1, 2, 3]
366        "#);
367        let config: Config = ConfigFactory::load(toml_file.path());
368        assert_eq!(config.nested.value, "default");
369        assert_eq!(config.nested.values, vec![0, 0, 0]);
370    }
371}