Went back and simplified Config to be a smart wrapper around toml::Table
This commit is contained in:
parent
3aa6436679
commit
c25c5d72c8
154
src/config.rs
154
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<String, Value>,
|
||||
/// Information for use by preprocessors.
|
||||
pub preprocess: BTreeMap<String, Value>,
|
||||
/// Information for use by postprocessors.
|
||||
pub postprocess: BTreeMap<String, Value>,
|
||||
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<HtmlConfig> {
|
||||
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<str>>(&self, name: S) -> Result<T> {
|
||||
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<str>>(&self, name: S) -> Result<T> {
|
||||
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<str>>(&self,
|
||||
name: S)
|
||||
-> Result<T> {
|
||||
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<str>>(&self,
|
||||
name: S)
|
||||
-> Result<T> {
|
||||
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<str>>(name: S,
|
||||
table: &BTreeMap<String, Value>)
|
||||
-> Result<T> {
|
||||
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<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>,
|
||||
/// An optional description for the book.
|
||||
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,
|
||||
/// 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 <michaelfbryan@gmail.com>"]
|
||||
|
@ -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 <michaelfbryan@gmail.com>")],
|
||||
|
@ -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<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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue