Merge pull request #2787 from ehuss/deny-unknown-fields
Deny all unknown config fields
This commit is contained in:
commit
79e9ae48a1
11 changed files with 287 additions and 227 deletions
|
|
@ -13,7 +13,6 @@
|
||||||
//! use std::path::PathBuf;
|
//! use std::path::PathBuf;
|
||||||
//! use std::str::FromStr;
|
//! use std::str::FromStr;
|
||||||
//! use mdbook_core::config::Config;
|
//! use mdbook_core::config::Config;
|
||||||
//! use toml::Value;
|
|
||||||
//!
|
//!
|
||||||
//! # fn run() -> Result<()> {
|
//! # fn run() -> Result<()> {
|
||||||
//! let src = r#"
|
//! let src = r#"
|
||||||
|
|
@ -21,9 +20,6 @@
|
||||||
//! title = "My Book"
|
//! title = "My Book"
|
||||||
//! authors = ["Michael-F-Bryan"]
|
//! authors = ["Michael-F-Bryan"]
|
||||||
//!
|
//!
|
||||||
//! [build]
|
|
||||||
//! src = "out"
|
|
||||||
//!
|
|
||||||
//! [preprocessor.my-preprocessor]
|
//! [preprocessor.my-preprocessor]
|
||||||
//! bar = 123
|
//! bar = 123
|
||||||
//! "#;
|
//! "#;
|
||||||
|
|
@ -36,7 +32,7 @@
|
||||||
//! assert_eq!(bar, Some(123));
|
//! assert_eq!(bar, Some(123));
|
||||||
//!
|
//!
|
||||||
//! // Set the `output.html.theme` directory
|
//! // Set the `output.html.theme` directory
|
||||||
//! assert!(cfg.get::<Value>("output.html")?.is_none());
|
//! assert!(cfg.get::<toml::Value>("output.html")?.is_none());
|
||||||
//! cfg.set("output.html.theme", "./themes");
|
//! cfg.set("output.html.theme", "./themes");
|
||||||
//!
|
//!
|
||||||
//! // then load it again, automatically deserializing to a `PathBuf`.
|
//! // then load it again, automatically deserializing to a `PathBuf`.
|
||||||
|
|
@ -49,9 +45,9 @@
|
||||||
|
|
||||||
use crate::utils::TomlExt;
|
use crate::utils::TomlExt;
|
||||||
use crate::utils::log_backtrace;
|
use crate::utils::log_backtrace;
|
||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Error, Result, bail};
|
||||||
use log::{debug, trace, warn};
|
use log::{debug, trace};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
@ -63,16 +59,34 @@ use toml::value::Table;
|
||||||
|
|
||||||
/// The overall configuration object for MDBook, essentially an in-memory
|
/// The overall configuration object for MDBook, essentially an in-memory
|
||||||
/// representation of `book.toml`.
|
/// representation of `book.toml`.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Metadata about the book.
|
/// Metadata about the book.
|
||||||
pub book: BookConfig,
|
pub book: BookConfig,
|
||||||
/// Information about the build environment.
|
/// Information about the build environment.
|
||||||
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub build: BuildConfig,
|
pub build: BuildConfig,
|
||||||
/// Information about Rust language support.
|
/// Information about Rust language support.
|
||||||
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub rust: RustConfig,
|
pub rust: RustConfig,
|
||||||
rest: Value,
|
/// The renderer configurations.
|
||||||
|
#[serde(skip_serializing_if = "toml_is_empty")]
|
||||||
|
output: Value,
|
||||||
|
/// The preprocessor configurations.
|
||||||
|
#[serde(skip_serializing_if = "toml_is_empty")]
|
||||||
|
preprocessor: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for serde serialization.
|
||||||
|
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
|
||||||
|
t == &T::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for serde serialization.
|
||||||
|
fn toml_is_empty(table: &Value) -> bool {
|
||||||
|
table.as_table().unwrap().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Config {
|
impl FromStr for Config {
|
||||||
|
|
@ -84,6 +98,18 @@ impl FromStr for Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Config {
|
||||||
|
Config {
|
||||||
|
book: BookConfig::default(),
|
||||||
|
build: BuildConfig::default(),
|
||||||
|
rust: RustConfig::default(),
|
||||||
|
output: Value::Table(Table::default()),
|
||||||
|
preprocessor: Value::Table(Table::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Load the configuration file from disk.
|
/// Load the configuration file from disk.
|
||||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||||
|
|
@ -161,24 +187,48 @@ impl Config {
|
||||||
/// dotted indices to access nested items (e.g. `output.html.playground`
|
/// dotted indices to access nested items (e.g. `output.html.playground`
|
||||||
/// will fetch the "playground" out of the html output table).
|
/// will fetch the "playground" out of the html output table).
|
||||||
///
|
///
|
||||||
/// This does not have access to the [`Config::book`], [`Config::build`],
|
/// This can only access the `output` and `preprocessor` tables.
|
||||||
/// or [`Config::rust`] fields.
|
|
||||||
///
|
///
|
||||||
/// Returns `Ok(None)` if the field is not set.
|
/// Returns `Ok(None)` if the field is not set.
|
||||||
///
|
///
|
||||||
/// Returns `Err` if it fails to deserialize.
|
/// Returns `Err` if it fails to deserialize.
|
||||||
pub fn get<'de, T: Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
|
pub fn get<'de, T: Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
|
||||||
self.rest
|
let (key, table) = if let Some(key) = name.strip_prefix("output.") {
|
||||||
.read(name)
|
(key, &self.output)
|
||||||
|
} else if let Some(key) = name.strip_prefix("preprocessor.") {
|
||||||
|
(key, &self.preprocessor)
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"unable to get `{name}`, only `output` and `preprocessor` table entries are allowed"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
table
|
||||||
|
.read(key)
|
||||||
.map(|value| {
|
.map(|value| {
|
||||||
value
|
value
|
||||||
.clone()
|
.clone()
|
||||||
.try_into()
|
.try_into()
|
||||||
.with_context(|| "Couldn't deserialize the value")
|
.with_context(|| "Failed to deserialize `{name}`")
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the configuration for all preprocessors.
|
||||||
|
pub fn preprocessors<'de, T: Deserialize<'de>>(&self) -> Result<HashMap<String, T>> {
|
||||||
|
self.preprocessor
|
||||||
|
.clone()
|
||||||
|
.try_into()
|
||||||
|
.with_context(|| "Failed to read preprocessors")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the configuration for all renderers.
|
||||||
|
pub fn outputs<'de, T: Deserialize<'de>>(&self) -> Result<HashMap<String, T>> {
|
||||||
|
self.output
|
||||||
|
.clone()
|
||||||
|
.try_into()
|
||||||
|
.with_context(|| "Failed to read renderers")
|
||||||
|
}
|
||||||
|
|
||||||
/// Convenience method for getting the html renderer's configuration.
|
/// Convenience method for getting the html renderer's configuration.
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
|
|
@ -187,10 +237,7 @@ impl Config {
|
||||||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||||
match self
|
match self.get("output.html") {
|
||||||
.get("output.html")
|
|
||||||
.with_context(|| "Parsing configuration [output.html]")
|
|
||||||
{
|
|
||||||
Ok(Some(config)) => Some(config),
|
Ok(Some(config)) => Some(config),
|
||||||
Ok(None) => None,
|
Ok(None) => None,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -220,110 +267,27 @@ impl Config {
|
||||||
self.build.update_value(key, value);
|
self.build.update_value(key, value);
|
||||||
} else if let Some(key) = index.strip_prefix("rust.") {
|
} else if let Some(key) = index.strip_prefix("rust.") {
|
||||||
self.rust.update_value(key, value);
|
self.rust.update_value(key, value);
|
||||||
|
} else if let Some(key) = index.strip_prefix("output.") {
|
||||||
|
self.output.update_value(key, value);
|
||||||
|
} else if let Some(key) = index.strip_prefix("preprocessor.") {
|
||||||
|
self.preprocessor.update_value(key, value);
|
||||||
} else {
|
} else {
|
||||||
self.rest.insert(index, value);
|
bail!("invalid key `{index}`");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Config {
|
|
||||||
Config {
|
|
||||||
book: BookConfig::default(),
|
|
||||||
build: BuildConfig::default(),
|
|
||||||
rust: RustConfig::default(),
|
|
||||||
rest: Value::Table(Table::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> serde::Deserialize<'de> for Config {
|
|
||||||
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
|
|
||||||
let raw = Value::deserialize(de)?;
|
|
||||||
|
|
||||||
warn_on_invalid_fields(&raw);
|
|
||||||
|
|
||||||
use serde::de::Error;
|
|
||||||
let mut table = match raw {
|
|
||||||
Value::Table(t) => t,
|
|
||||||
_ => {
|
|
||||||
return Err(D::Error::custom(
|
|
||||||
"A config file should always be a toml table",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let book: BookConfig = table
|
|
||||||
.remove("book")
|
|
||||||
.map(|book| book.try_into().map_err(D::Error::custom))
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let build: BuildConfig = table
|
|
||||||
.remove("build")
|
|
||||||
.map(|build| build.try_into().map_err(D::Error::custom))
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let rust: RustConfig = table
|
|
||||||
.remove("rust")
|
|
||||||
.map(|rust| rust.try_into().map_err(D::Error::custom))
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
Ok(Config {
|
|
||||||
book,
|
|
||||||
build,
|
|
||||||
rust,
|
|
||||||
rest: Value::Table(table),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Config {
|
|
||||||
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
|
|
||||||
// TODO: This should probably be removed and use a derive instead.
|
|
||||||
let mut table = self.rest.clone();
|
|
||||||
|
|
||||||
let book_config = Value::try_from(&self.book).expect("should always be serializable");
|
|
||||||
table.insert("book", book_config);
|
|
||||||
|
|
||||||
if self.build != BuildConfig::default() {
|
|
||||||
let build_config = Value::try_from(&self.build).expect("should always be serializable");
|
|
||||||
table.insert("build", build_config);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.rust != RustConfig::default() {
|
|
||||||
let rust_config = Value::try_from(&self.rust).expect("should always be serializable");
|
|
||||||
table.insert("rust", rust_config);
|
|
||||||
}
|
|
||||||
|
|
||||||
table.serialize(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_env(key: &str) -> Option<String> {
|
fn parse_env(key: &str) -> Option<String> {
|
||||||
key.strip_prefix("MDBOOK_")
|
key.strip_prefix("MDBOOK_")
|
||||||
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
|
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn warn_on_invalid_fields(table: &Value) {
|
|
||||||
let valid_items = ["book", "build", "rust", "output", "preprocessor"];
|
|
||||||
|
|
||||||
let table = table.as_table().expect("root must be a table");
|
|
||||||
for item in table.keys() {
|
|
||||||
if !valid_items.contains(&item.as_str()) {
|
|
||||||
warn!("Invalid field {:?} in book.toml", &item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration options which are specific to the book and required for
|
/// Configuration options which are specific to the book and required for
|
||||||
/// loading it from disk.
|
/// loading it from disk.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct BookConfig {
|
pub struct BookConfig {
|
||||||
/// The book's title.
|
/// The book's title.
|
||||||
|
|
@ -393,7 +357,7 @@ impl TextDirection {
|
||||||
|
|
||||||
/// Configuration for the build procedure.
|
/// Configuration for the build procedure.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct BuildConfig {
|
pub struct BuildConfig {
|
||||||
/// Where to put built artefacts relative to the book's root directory.
|
/// Where to put built artefacts relative to the book's root directory.
|
||||||
|
|
@ -421,7 +385,7 @@ impl Default for BuildConfig {
|
||||||
|
|
||||||
/// Configuration for the Rust compiler(e.g., for playground)
|
/// Configuration for the Rust compiler(e.g., for playground)
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct RustConfig {
|
pub struct RustConfig {
|
||||||
/// Rust edition used in playground
|
/// Rust edition used in playground
|
||||||
|
|
@ -448,7 +412,7 @@ pub enum RustEdition {
|
||||||
|
|
||||||
/// Configuration for the HTML renderer.
|
/// Configuration for the HTML renderer.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct HtmlConfig {
|
pub struct HtmlConfig {
|
||||||
/// The theme directory, if specified.
|
/// The theme directory, if specified.
|
||||||
|
|
@ -568,7 +532,7 @@ impl HtmlConfig {
|
||||||
|
|
||||||
/// Configuration for how to render the print icon, print.html, and print.css.
|
/// Configuration for how to render the print icon, print.html, and print.css.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Print {
|
pub struct Print {
|
||||||
/// Whether print support is enabled.
|
/// Whether print support is enabled.
|
||||||
|
|
@ -588,7 +552,7 @@ impl Default for Print {
|
||||||
|
|
||||||
/// Configuration for how to fold chapters of sidebar.
|
/// Configuration for how to fold chapters of sidebar.
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Fold {
|
pub struct Fold {
|
||||||
/// When off, all folds are open. Default: `false`.
|
/// When off, all folds are open. Default: `false`.
|
||||||
|
|
@ -601,7 +565,7 @@ pub struct Fold {
|
||||||
|
|
||||||
/// Configuration for tweaking how the HTML renderer handles the playground.
|
/// Configuration for tweaking how the HTML renderer handles the playground.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Playground {
|
pub struct Playground {
|
||||||
/// Should playground snippets be editable? Default: `false`.
|
/// Should playground snippets be editable? Default: `false`.
|
||||||
|
|
@ -631,7 +595,7 @@ impl Default for Playground {
|
||||||
|
|
||||||
/// Configuration for tweaking how the HTML renderer handles code blocks.
|
/// Configuration for tweaking how the HTML renderer handles code blocks.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Code {
|
pub struct Code {
|
||||||
/// A prefix string to hide lines per language (one or more chars).
|
/// A prefix string to hide lines per language (one or more chars).
|
||||||
|
|
@ -640,7 +604,7 @@ pub struct Code {
|
||||||
|
|
||||||
/// Configuration of the search functionality of the HTML renderer.
|
/// Configuration of the search functionality of the HTML renderer.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Search {
|
pub struct Search {
|
||||||
/// Enable the search feature. Default: `true`.
|
/// Enable the search feature. Default: `true`.
|
||||||
|
|
@ -698,7 +662,7 @@ impl Default for Search {
|
||||||
|
|
||||||
/// Search options for chapters (or paths).
|
/// Search options for chapters (or paths).
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct SearchChapterSettings {
|
pub struct SearchChapterSettings {
|
||||||
/// Whether or not indexing is enabled, default `true`.
|
/// Whether or not indexing is enabled, default `true`.
|
||||||
|
|
@ -732,7 +696,6 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::utils::fs::get_404_output_file;
|
use crate::utils::fs::get_404_output_file;
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
const COMPLEX_CONFIG: &str = r#"
|
const COMPLEX_CONFIG: &str = r#"
|
||||||
[book]
|
[book]
|
||||||
|
|
@ -757,7 +720,6 @@ mod tests {
|
||||||
|
|
||||||
[output.html.playground]
|
[output.html.playground]
|
||||||
editable = true
|
editable = true
|
||||||
editor = "ace"
|
|
||||||
|
|
||||||
[output.html.redirect]
|
[output.html.redirect]
|
||||||
"index.html" = "overview.html"
|
"index.html" = "overview.html"
|
||||||
|
|
@ -943,19 +905,6 @@ mod tests {
|
||||||
assert_eq!(got_baz, baz_should_be);
|
assert_eq!(got_baz, baz_should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn set_a_config_item() {
|
|
||||||
let mut cfg = Config::default();
|
|
||||||
let key = "foo.bar.baz";
|
|
||||||
let value = "Something Interesting";
|
|
||||||
|
|
||||||
assert!(cfg.get::<i32>(key).unwrap().is_none());
|
|
||||||
cfg.set(key, value).unwrap();
|
|
||||||
|
|
||||||
let got: String = cfg.get(key).unwrap().unwrap();
|
|
||||||
assert_eq!(got, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_special_tables() {
|
fn set_special_tables() {
|
||||||
let mut cfg = Config::default();
|
let mut cfg = Config::default();
|
||||||
|
|
@ -970,6 +919,21 @@ mod tests {
|
||||||
assert_eq!(cfg.rust.edition, None);
|
assert_eq!(cfg.rust.edition, None);
|
||||||
cfg.set("rust.edition", "2024").unwrap();
|
cfg.set("rust.edition", "2024").unwrap();
|
||||||
assert_eq!(cfg.rust.edition, Some(RustEdition::E2024));
|
assert_eq!(cfg.rust.edition, Some(RustEdition::E2024));
|
||||||
|
|
||||||
|
cfg.set("output.foo.value", "123").unwrap();
|
||||||
|
let got: String = cfg.get("output.foo.value").unwrap().unwrap();
|
||||||
|
assert_eq!(got, "123");
|
||||||
|
|
||||||
|
cfg.set("preprocessor.bar.value", "456").unwrap();
|
||||||
|
let got: String = cfg.get("preprocessor.bar.value").unwrap().unwrap();
|
||||||
|
assert_eq!(got, "456");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_invalid_keys() {
|
||||||
|
let mut cfg = Config::default();
|
||||||
|
let err = cfg.set("foo", "test").unwrap_err();
|
||||||
|
assert!(err.to_string().contains("invalid key `foo`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -989,67 +953,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_env_var(key: &str) -> String {
|
|
||||||
format!(
|
|
||||||
"MDBOOK_{}",
|
|
||||||
key.to_uppercase().replace('.', "__").replace('-', "_")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn update_config_using_env_var() {
|
|
||||||
let mut cfg = Config::default();
|
|
||||||
let key = "foo.bar";
|
|
||||||
let value = "baz";
|
|
||||||
|
|
||||||
assert!(cfg.get::<String>(key).unwrap().is_none());
|
|
||||||
|
|
||||||
let encoded_key = encode_env_var(key);
|
|
||||||
// TODO: This is unsafe, and should be rewritten to use a process.
|
|
||||||
unsafe { env::set_var(encoded_key, value) };
|
|
||||||
|
|
||||||
cfg.update_from_env();
|
|
||||||
|
|
||||||
assert_eq!(cfg.get::<String>(key).unwrap().unwrap(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn update_config_using_env_var_and_complex_value() {
|
|
||||||
let mut cfg = Config::default();
|
|
||||||
let key = "foo-bar.baz";
|
|
||||||
let value = json!({"array": [1, 2, 3], "number": 13.37});
|
|
||||||
let value_str = serde_json::to_string(&value).unwrap();
|
|
||||||
|
|
||||||
assert!(cfg.get::<serde_json::Value>(key).unwrap().is_none());
|
|
||||||
|
|
||||||
let encoded_key = encode_env_var(key);
|
|
||||||
// TODO: This is unsafe, and should be rewritten to use a process.
|
|
||||||
unsafe { env::set_var(encoded_key, value_str) };
|
|
||||||
|
|
||||||
cfg.update_from_env();
|
|
||||||
|
|
||||||
assert_eq!(cfg.get::<serde_json::Value>(key).unwrap().unwrap(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn update_book_title_via_env() {
|
|
||||||
let mut cfg = Config::default();
|
|
||||||
let should_be = "Something else".to_string();
|
|
||||||
|
|
||||||
assert_ne!(cfg.book.title, Some(should_be.clone()));
|
|
||||||
|
|
||||||
// TODO: This is unsafe, and should be rewritten to use a process.
|
|
||||||
unsafe { env::set_var("MDBOOK_BOOK__TITLE", &should_be) };
|
|
||||||
cfg.update_from_env();
|
|
||||||
|
|
||||||
assert_eq!(cfg.book.title, Some(should_be));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn file_404_default() {
|
fn file_404_default() {
|
||||||
let src = r#"
|
let src = r#"
|
||||||
[output.html]
|
[output.html]
|
||||||
destination = "my-book"
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let got = Config::from_str(src).unwrap();
|
let got = Config::from_str(src).unwrap();
|
||||||
|
|
@ -1063,7 +970,6 @@ mod tests {
|
||||||
let src = r#"
|
let src = r#"
|
||||||
[output.html]
|
[output.html]
|
||||||
input-404= "missing.md"
|
input-404= "missing.md"
|
||||||
output-404= "missing.html"
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let got = Config::from_str(src).unwrap();
|
let got = Config::from_str(src).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||||
use mdbook_renderer::{RenderContext, Renderer};
|
use mdbook_renderer::{RenderContext, Renderer};
|
||||||
use mdbook_summary::Summary;
|
use mdbook_summary::Summary;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::io::{IsTerminal, Write};
|
use std::io::{IsTerminal, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -423,22 +422,17 @@ struct OutputConfig {
|
||||||
fn determine_renderers(config: &Config) -> Result<Vec<Box<dyn Renderer>>> {
|
fn determine_renderers(config: &Config) -> Result<Vec<Box<dyn Renderer>>> {
|
||||||
let mut renderers = Vec::new();
|
let mut renderers = Vec::new();
|
||||||
|
|
||||||
match config.get::<HashMap<String, OutputConfig>>("output") {
|
let outputs = config.outputs::<OutputConfig>()?;
|
||||||
Ok(Some(output_table)) => {
|
renderers.extend(outputs.into_iter().map(|(key, table)| {
|
||||||
renderers.extend(output_table.into_iter().map(|(key, table)| {
|
if key == "html" {
|
||||||
if key == "html" {
|
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
} else if key == "markdown" {
|
||||||
} else if key == "markdown" {
|
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
} else {
|
||||||
} else {
|
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
|
||||||
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
|
Box::new(CmdRenderer::new(key, command))
|
||||||
Box::new(CmdRenderer::new(key, command))
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
}));
|
||||||
Err(e) => bail!("failed to get output table config: {e}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we couldn't find anything, add the HTML renderer as a default
|
// if we couldn't find anything, add the HTML renderer as a default
|
||||||
if renderers.is_empty() {
|
if renderers.is_empty() {
|
||||||
|
|
@ -477,12 +471,7 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let preprocessor_table = match config.get::<HashMap<String, PreprocessorConfig>>("preprocessor")
|
let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
|
||||||
{
|
|
||||||
Ok(Some(preprocessor_table)) => preprocessor_table,
|
|
||||||
Ok(None) => HashMap::new(),
|
|
||||||
Err(e) => bail!("failed to get preprocessor table config: {e}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (name, table) in preprocessor_table.iter() {
|
for (name, table) in preprocessor_table.iter() {
|
||||||
preprocessor_names.insert(name.to_string());
|
preprocessor_names.insert(name.to_string());
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ fn config_defaults_to_html_renderer_if_empty() {
|
||||||
let cfg = Config::default();
|
let cfg = Config::default();
|
||||||
|
|
||||||
// make sure we haven't got anything in the `output` table
|
// make sure we haven't got anything in the `output` table
|
||||||
assert!(cfg.get::<Value>("output").unwrap().is_none());
|
assert!(cfg.outputs::<toml::Value>().unwrap().is_empty());
|
||||||
|
|
||||||
let got = determine_renderers(&cfg).unwrap();
|
let got = determine_renderers(&cfg).unwrap();
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
|
||||||
let cfg = Config::default();
|
let cfg = Config::default();
|
||||||
|
|
||||||
// make sure we haven't got anything in the `preprocessor` table
|
// make sure we haven't got anything in the `preprocessor` table
|
||||||
assert!(cfg.get::<Value>("preprocessor").unwrap().is_none());
|
assert!(cfg.preprocessors::<toml::Value>().unwrap().is_empty());
|
||||||
|
|
||||||
let got = determine_preprocessors(&cfg);
|
let got = determine_preprocessors(&cfg);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use super::command_prelude::*;
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
use super::watch;
|
use super::watch;
|
||||||
use crate::{get_book_dir, open};
|
use crate::{get_book_dir, open};
|
||||||
use anyhow::{Result, bail};
|
use anyhow::Result;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
|
|
@ -76,10 +76,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
|
.ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
|
||||||
let build_dir = book.build_dir_for("html");
|
let build_dir = book.build_dir_for("html");
|
||||||
let input_404 = match book.config.get::<String>("output.html.input-404") {
|
let html_config = book.config.html_config();
|
||||||
Ok(v) => v,
|
let input_404 = html_config.and_then(|c| c.input_404);
|
||||||
Err(e) => bail!("expected string for output.html.input-404: {e}"),
|
|
||||||
};
|
|
||||||
let file_404 = get_404_output_file(&input_404);
|
let file_404 = get_404_output_file(&input_404);
|
||||||
|
|
||||||
// A channel used to broadcast to any websockets to reload when a file changes.
|
// A channel used to broadcast to any websockets to reload when a file changes.
|
||||||
|
|
|
||||||
159
tests/testsuite/config.rs
Normal file
159
tests/testsuite/config.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
//! Tests for book configuration loading.
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
// Test that config can load from environment variable.
|
||||||
|
#[test]
|
||||||
|
fn config_from_env() {
|
||||||
|
BookTest::from_dir("config/empty")
|
||||||
|
.run("build", |cmd| {
|
||||||
|
cmd.env("MDBOOK_BOOK__TITLE", "Custom env title");
|
||||||
|
})
|
||||||
|
.check_file_contains(
|
||||||
|
"book/index.html",
|
||||||
|
"<title>Chapter 1 - Custom env title</title>",
|
||||||
|
);
|
||||||
|
|
||||||
|
// json for some subtable
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test environment config with JSON.
|
||||||
|
#[test]
|
||||||
|
fn config_json_from_env() {
|
||||||
|
// build table
|
||||||
|
BookTest::from_dir("config/empty")
|
||||||
|
.run("build", |cmd| {
|
||||||
|
cmd.env(
|
||||||
|
"MDBOOK_BOOK",
|
||||||
|
r#"{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}"#,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.check_file_contains(
|
||||||
|
"book/index.html",
|
||||||
|
"<title>Chapter 1 - My Awesome Book</title>",
|
||||||
|
);
|
||||||
|
|
||||||
|
// book table
|
||||||
|
BookTest::from_dir("config/empty")
|
||||||
|
.run("build", |cmd| {
|
||||||
|
cmd.env("MDBOOK_BUILD", r#"{"build-dir": "alt"}"#);
|
||||||
|
})
|
||||||
|
.check_file_contains("alt/index.html", "<title>Chapter 1</title>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that a preprocessor receives config set in the environment.
|
||||||
|
#[test]
|
||||||
|
fn preprocessor_cfg_from_env() {
|
||||||
|
let mut test = BookTest::from_dir("config/empty");
|
||||||
|
test.rust_program(
|
||||||
|
"cat-to-file",
|
||||||
|
r#"
|
||||||
|
fn main() {
|
||||||
|
use std::io::Read;
|
||||||
|
let mut s = String::new();
|
||||||
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
||||||
|
std::fs::write("out.txt", s).unwrap();
|
||||||
|
println!("{{\"sections\": []}}");
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.run("build", |cmd| {
|
||||||
|
cmd.env(
|
||||||
|
"MDBOOK_PREPROCESSOR__CAT_TO_FILE",
|
||||||
|
r#"{"command":"./cat-to-file", "array": [1,2,3], "number": 123}"#,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let out = read_to_string(test.dir.join("out.txt"));
|
||||||
|
let (ctx, _book) = mdbook_preprocessor::parse_input(out.as_bytes()).unwrap();
|
||||||
|
let cfg: serde_json::Value = ctx.config.get("preprocessor.cat-to-file").unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cfg,
|
||||||
|
serde_json::json!({
|
||||||
|
"command": "./cat-to-file",
|
||||||
|
"array": [1,2,3],
|
||||||
|
"number": 123,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that a renderer receives config set in the environment.
|
||||||
|
#[test]
|
||||||
|
fn output_cfg_from_env() {
|
||||||
|
let mut test = BookTest::from_dir("config/empty");
|
||||||
|
test.rust_program(
|
||||||
|
"cat-to-file",
|
||||||
|
r#"
|
||||||
|
fn main() {
|
||||||
|
use std::io::Read;
|
||||||
|
let mut s = String::new();
|
||||||
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
||||||
|
std::fs::write("out.txt", s).unwrap();
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.run("build", |cmd| {
|
||||||
|
cmd.env(
|
||||||
|
"MDBOOK_OUTPUT__CAT_TO_FILE",
|
||||||
|
r#"{"command":"./cat-to-file", "array": [1,2,3], "number": 123}"#,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let out = read_to_string(test.dir.join("book/out.txt"));
|
||||||
|
let ctx = mdbook_renderer::RenderContext::from_json(out.as_bytes()).unwrap();
|
||||||
|
let cfg: serde_json::Value = ctx.config.get("output.cat-to-file").unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cfg,
|
||||||
|
serde_json::json!({
|
||||||
|
"command": "./cat-to-file",
|
||||||
|
"array": [1,2,3],
|
||||||
|
"number": 123,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An invalid key at the top level.
|
||||||
|
#[test]
|
||||||
|
fn bad_config_top_level() {
|
||||||
|
BookTest::init(|_| {})
|
||||||
|
.change_file("book.toml", "foo = 123")
|
||||||
|
.run("build", |cmd| {
|
||||||
|
cmd.expect_failure()
|
||||||
|
.expect_stdout(str![[""]])
|
||||||
|
.expect_stderr(str![[r#"
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Invalid configuration file
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: TOML parse error at line 1, column 1
|
||||||
|
|
|
||||||
|
1 | foo = 123
|
||||||
|
| ^^^
|
||||||
|
unknown field `foo`, expected one of `book`, `build`, `rust`, `output`, `preprocessor`
|
||||||
|
|
||||||
|
|
||||||
|
"#]]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// An invalid key in the main book table.
|
||||||
|
#[test]
|
||||||
|
fn bad_config_in_book_table() {
|
||||||
|
BookTest::init(|_| {})
|
||||||
|
.change_file(
|
||||||
|
"book.toml",
|
||||||
|
"[book]\n\
|
||||||
|
title = \"bad-config\"\n\
|
||||||
|
foo = 123"
|
||||||
|
)
|
||||||
|
.run("build", |cmd| {
|
||||||
|
cmd.expect_failure()
|
||||||
|
.expect_stdout(str![[""]])
|
||||||
|
.expect_stderr(str![[r#"
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Invalid configuration file
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: TOML parse error at line 3, column 1
|
||||||
|
|
|
||||||
|
3 | foo = 123
|
||||||
|
| ^^^
|
||||||
|
unknown field `foo`, expected one of `title`, `authors`, `description`, `src`, `language`, `text-direction`
|
||||||
|
|
||||||
|
|
||||||
|
"#]]);
|
||||||
|
});
|
||||||
|
}
|
||||||
0
tests/testsuite/config/empty/book.toml
Normal file
0
tests/testsuite/config/empty/book.toml
Normal file
3
tests/testsuite/config/empty/src/SUMMARY.md
Normal file
3
tests/testsuite/config/empty/src/SUMMARY.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
- [Chapter 1](./chapter_1.md)
|
||||||
1
tests/testsuite/config/empty/src/chapter_1.md
Normal file
1
tests/testsuite/config/empty/src/chapter_1.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Chapter 1
|
||||||
|
|
@ -28,8 +28,8 @@ All done, no errors...
|
||||||
str![[r#"
|
str![[r#"
|
||||||
[book]
|
[book]
|
||||||
authors = []
|
authors = []
|
||||||
language = "en"
|
|
||||||
src = "src"
|
src = "src"
|
||||||
|
language = "en"
|
||||||
|
|
||||||
"#]],
|
"#]],
|
||||||
)
|
)
|
||||||
|
|
@ -94,8 +94,8 @@ All done, no errors...
|
||||||
str![[r#"
|
str![[r#"
|
||||||
[book]
|
[book]
|
||||||
authors = []
|
authors = []
|
||||||
language = "en"
|
|
||||||
src = "src"
|
src = "src"
|
||||||
|
language = "en"
|
||||||
|
|
||||||
"#]],
|
"#]],
|
||||||
);
|
);
|
||||||
|
|
@ -126,10 +126,10 @@ All done, no errors...
|
||||||
"book.toml",
|
"book.toml",
|
||||||
str![[r#"
|
str![[r#"
|
||||||
[book]
|
[book]
|
||||||
authors = []
|
|
||||||
language = "en"
|
|
||||||
src = "src"
|
|
||||||
title = "Example title"
|
title = "Example title"
|
||||||
|
authors = []
|
||||||
|
src = "src"
|
||||||
|
language = "en"
|
||||||
|
|
||||||
"#]],
|
"#]],
|
||||||
);
|
);
|
||||||
|
|
@ -179,14 +179,14 @@ fn init_with_custom_book_and_src_locations() {
|
||||||
str![[r#"
|
str![[r#"
|
||||||
[book]
|
[book]
|
||||||
authors = []
|
authors = []
|
||||||
language = "en"
|
|
||||||
src = "in"
|
src = "in"
|
||||||
|
language = "en"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
build-dir = "out"
|
build-dir = "out"
|
||||||
create-missing = true
|
create-missing = true
|
||||||
extra-watch-dirs = []
|
|
||||||
use-default-preprocessors = true
|
use-default-preprocessors = true
|
||||||
|
extra-watch-dirs = []
|
||||||
|
|
||||||
"#]],
|
"#]],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
mod book_test;
|
mod book_test;
|
||||||
mod build;
|
mod build;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod config;
|
||||||
mod includes;
|
mod includes;
|
||||||
mod index;
|
mod index;
|
||||||
mod init;
|
mod init;
|
||||||
|
|
|
||||||
|
|
@ -186,8 +186,11 @@ fn backends_receive_render_context_via_stdin() {
|
||||||
"config": {
|
"config": {
|
||||||
"book": {
|
"book": {
|
||||||
"authors": [],
|
"authors": [],
|
||||||
|
"description": null,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"src": "src"
|
"src": "src",
|
||||||
|
"text-direction": null,
|
||||||
|
"title": null
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"cat-to-file": {
|
"cat-to-file": {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue