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::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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue