1#![cfg_attr(feature = "serde-serialization", feature(custom_derive, plugin))]
2#![cfg_attr(feature = "serde-serialization", plugin(serde_macros))]
3
4#[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
40pub struct ConfigFactory;
156
157impl ConfigFactory {
158 #[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 #[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 #[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 #[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}