diff --git a/src/config.rs b/src/config.rs index 848e8873..e591f94e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,26 +1,19 @@ -use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::fs::File; use std::io::Read; use toml::{self, Value}; -use serde::Deserialize; +use toml::value::Table; +use serde::{Deserialize, Deserializer}; use errors::*; /// The overall configuration object for MDBook. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct Config { /// Metadata about the book. pub book: BookConfig, - /// Arbitrary information which renderers can use during the rendering - /// stage. - pub output: BTreeMap, - /// Information for use by preprocessors. - pub preprocess: BTreeMap, - /// Information for use by postprocessors. - pub postprocess: BTreeMap, + rest: Table, } impl Config { @@ -39,6 +32,22 @@ impl Config { Config::from_str(&buffer) } + /// Fetch an arbitrary item from the `Config` as a `toml::Value`. + /// + /// You can use dotted indices to access nested items (e.g. + /// `output.html.playpen` will fetch the "playpen" out of the html output + /// table). + pub fn get(&self, key: &str) -> Option<&Value> { + let pieces: Vec<_> = key.split(".").collect(); + recursive_get(&pieces, &self.rest) + } + + /// Fetch a value from the `Config` so you can mutate it. + pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> { + let pieces: Vec<_> = key.split(".").collect(); + recursive_get_mut(&pieces, &mut self.rest) + } + /// Convenience method for getting the html renderer's configuration. /// /// # Note @@ -46,44 +55,74 @@ impl Config { /// This is for compatibility only. It will be removed completely once the /// rendering and plugin system is established. pub fn html_config(&self) -> Option { - self.try_get_output("html").ok() + self.get_deserialized("output.html").ok() } - /// Try to get an output and deserialize it as a `T`. - pub fn try_get_output<'de, T: Deserialize<'de>, S: AsRef>(&self, name: S) -> Result { - get_deserialized(name, &self.output) - } + /// Convenience function to fetch a value from the config and deserialize it + /// into some arbitrary type. + pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef>(&self, name: S) -> Result { + let name = name.as_ref(); - /// Try to get the configuration for a preprocessor, deserializing it as a - /// `T`. - pub fn try_get_preprocessor<'de, T: Deserialize<'de>, S: AsRef>(&self, - name: S) - -> Result { - get_deserialized(name, &self.preprocess) - } - - /// Try to get the configuration for a postprocessor, deserializing it as a - /// `T`. - pub fn try_get_postprocessor<'de, T: Deserialize<'de>, S: AsRef>(&self, - name: S) - -> Result { - get_deserialized(name, &self.postprocess) + if let Some(value) = self.get(name) { + value.clone() + .try_into() + .chain_err(|| "Couldn't deserialize the value") + } else { + bail!("Key not found, {:?}", name) + } } } -/// Convenience function to load a value from some table then deserialize it. -fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef>(name: S, - table: &BTreeMap) - -> Result { - let name = name.as_ref(); +fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> { + if key.is_empty() { + return None; + } else if key.len() == 1 { + return table.get(key[0]); + } - match table.get(name) { - Some(output) => { - output.clone() - .try_into() - .chain_err(|| "Couldn't deserialize the value") + let first = key[0]; + let rest = &key[1..]; + + if let Some(&Value::Table(ref nested)) = table.get(first) { + recursive_get(rest, nested) + } else { + None + } +} + +fn recursive_get_mut<'a>(key: &[&str], table: &'a mut Table) -> Option<&'a mut Value> { + // TODO: Figure out how to abstract over mutability to reduce copy-pasta + if key.is_empty() { + return None; + } else if key.len() == 1 { + return table.get_mut(key[0]); + } + + let first = key[0]; + let rest = &key[1..]; + + if let Some(&mut Value::Table(ref mut nested)) = table.get_mut(first) { + recursive_get_mut(rest, nested) + } else { + None + } +} + +impl<'de> Deserialize<'de> for Config { + fn deserialize>(de: D) -> ::std::result::Result { + let raw = Value::deserialize(de)?; + if let Value::Table(mut table) = raw { + let book: BookConfig = table.remove("book") + .and_then(|value| value.try_into().ok()) + .unwrap_or_default(); + Ok(Config { + book: book, + rest: table, + }) + } else { + use serde::de::Error; + Err(D::Error::custom("A config file should always be a toml table")) } - None => bail!("Key Not Found, {}", name), } } @@ -99,9 +138,9 @@ pub struct BookConfig { pub authors: Vec, /// An optional description for the book. pub description: Option, - /// Location of the book source, relative to the book's root directory. + /// Location of the book source relative to the book's root directory. pub src: PathBuf, - /// Where to put built artefacts, relative to the book's root directory. + /// Where to put built artefacts relative to the book's root directory. pub build_dir: PathBuf, /// Does this book support more than one language? pub multilingual: bool, @@ -132,6 +171,7 @@ pub struct HtmlConfig { pub playpen: Playpen, } +/// Configuration for tweaking how the the HTML renderer handles the playpen. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Playpen { pub editor: PathBuf, @@ -143,9 +183,7 @@ pub struct Playpen { mod tests { use super::*; - #[test] - fn load_a_complex_config_file() { - let src = r#" + const COMPLEX_CONFIG: &'static str = r#" [book] title = "Some Book" authors = ["Michael-F-Bryan "] @@ -165,6 +203,10 @@ mod tests { editor = "ace" "#; + #[test] + fn load_a_complex_config_file() { + let src = COMPLEX_CONFIG; + let book_should_be = BookConfig { title: Some(String::from("Some Book")), authors: vec![String::from("Michael-F-Bryan ")], @@ -216,8 +258,26 @@ mod tests { }; let cfg = Config::from_str(src).unwrap(); - let got: RandomOutput = cfg.try_get_output("random").unwrap(); + let got: RandomOutput = cfg.get_deserialized("output.random").unwrap(); assert_eq!(got, should_be); + + let baz: Vec = cfg.get_deserialized("output.random.baz").unwrap(); + let baz_should_be = vec![true, true, false]; + + assert_eq!(baz, baz_should_be); } + +#[test] +fn mutate_some_stuff() { + // really this is just a sanity check to make sure the borrow checker + // is happy... + let src = COMPLEX_CONFIG; + let mut config = Config::from_str(src).unwrap(); + let key = "output.html.playpen.editable"; + + assert_eq!(config.get(key).unwrap(), &Value::Boolean(true)); + *config.get_mut(key).unwrap() = Value::Boolean(false); + assert_eq!(config.get(key).unwrap(), &Value::Boolean(false)); +} }