Went back and simplified Config to be a smart wrapper around toml::Table

This commit is contained in:
Michael Bryan 2017-11-12 02:03:28 +08:00
parent 3aa6436679
commit c25c5d72c8
No known key found for this signature in database
GPG Key ID: E9C602B0D9A998DC
1 changed files with 107 additions and 47 deletions

View File

@ -1,26 +1,19 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use toml::{self, Value}; use toml::{self, Value};
use serde::Deserialize; use toml::value::Table;
use serde::{Deserialize, Deserializer};
use errors::*; use errors::*;
/// The overall configuration object for MDBook. /// The overall configuration object for MDBook.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct Config { pub struct Config {
/// Metadata about the book. /// Metadata about the book.
pub book: BookConfig, pub book: BookConfig,
/// Arbitrary information which renderers can use during the rendering rest: Table,
/// stage.
pub output: BTreeMap<String, Value>,
/// Information for use by preprocessors.
pub preprocess: BTreeMap<String, Value>,
/// Information for use by postprocessors.
pub postprocess: BTreeMap<String, Value>,
} }
impl Config { impl Config {
@ -39,6 +32,22 @@ impl Config {
Config::from_str(&buffer) 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. /// Convenience method for getting the html renderer's configuration.
/// ///
/// # Note /// # Note
@ -46,44 +55,74 @@ impl Config {
/// This is for compatibility only. It will be removed completely once the /// This is for compatibility only. It will be removed completely once the
/// rendering and plugin system is established. /// rendering and plugin system is established.
pub fn html_config(&self) -> Option<HtmlConfig> { pub fn html_config(&self) -> Option<HtmlConfig> {
self.try_get_output("html").ok() self.get_deserialized("output.html").ok()
} }
/// Try to get an output and deserialize it as a `T`. /// Convenience function to fetch a value from the config and deserialize it
pub fn try_get_output<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> { /// into some arbitrary type.
get_deserialized(name, &self.output) pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
} let name = name.as_ref();
/// Try to get the configuration for a preprocessor, deserializing it as a if let Some(value) = self.get(name) {
/// `T`. value.clone()
pub fn try_get_preprocessor<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, .try_into()
name: S) .chain_err(|| "Couldn't deserialize the value")
-> Result<T> { } else {
get_deserialized(name, &self.preprocess) bail!("Key not found, {:?}", name)
} }
/// Try to get the configuration for a postprocessor, deserializing it as a
/// `T`.
pub fn try_get_postprocessor<'de, T: Deserialize<'de>, S: AsRef<str>>(&self,
name: S)
-> Result<T> {
get_deserialized(name, &self.postprocess)
} }
} }
/// Convenience function to load a value from some table then deserialize it. fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> {
fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(name: S, if key.is_empty() {
table: &BTreeMap<String, Value>) return None;
-> Result<T> { } else if key.len() == 1 {
let name = name.as_ref(); return table.get(key[0]);
}
match table.get(name) { let first = key[0];
Some(output) => { let rest = &key[1..];
output.clone()
.try_into() if let Some(&Value::Table(ref nested)) = table.get(first) {
.chain_err(|| "Couldn't deserialize the value") 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<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
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<String>, pub authors: Vec<String>,
/// An optional description for the book. /// An optional description for the book.
pub description: Option<String>, pub description: Option<String>,
/// 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, 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, pub build_dir: PathBuf,
/// Does this book support more than one language? /// Does this book support more than one language?
pub multilingual: bool, pub multilingual: bool,
@ -132,6 +171,7 @@ pub struct HtmlConfig {
pub playpen: Playpen, pub playpen: Playpen,
} }
/// Configuration for tweaking how the the HTML renderer handles the playpen.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Playpen { pub struct Playpen {
pub editor: PathBuf, pub editor: PathBuf,
@ -143,9 +183,7 @@ pub struct Playpen {
mod tests { mod tests {
use super::*; use super::*;
#[test] const COMPLEX_CONFIG: &'static str = r#"
fn load_a_complex_config_file() {
let src = r#"
[book] [book]
title = "Some Book" title = "Some Book"
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"] authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
@ -165,6 +203,10 @@ mod tests {
editor = "ace" editor = "ace"
"#; "#;
#[test]
fn load_a_complex_config_file() {
let src = COMPLEX_CONFIG;
let book_should_be = BookConfig { let book_should_be = BookConfig {
title: Some(String::from("Some Book")), title: Some(String::from("Some Book")),
authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")], authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
@ -216,8 +258,26 @@ mod tests {
}; };
let cfg = Config::from_str(src).unwrap(); 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); assert_eq!(got, should_be);
let baz: Vec<bool> = 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));
}
} }