Deny all unknown config fields
This changes it so that it is an error if there is ever an unknown configuration field. This is intended to help avoid things like typos, or using an outdated version of mdbook. Although it is possible that new fields could potentially safely be ignored, setting up a warning system is a bit more of a hassle. I don't think mdbook needs to have the same kind of multi-version support as something like cargo does. However, if this ends up being too much of a pain point, we can try to add a warning system instead. There are a variety of changes here: - The top-level config namespace is now closed so that it only accepts the keys defined in `Config`. - All config tables now reject unknown fields. - Added `Config::outputs` and `Config::preprocessors` for convenience to access the entire `output` and `preprocessor` tables. - Moved the unit-tests that were setting environment variables to the testsuite where it launches a process instead. Closes https://github.com/rust-lang/mdBook/issues/1595
This commit is contained in:
parent
e284eb1c30
commit
d29072783f
11 changed files with 287 additions and 227 deletions
|
|
@ -13,7 +13,6 @@
|
|||
//! use std::path::PathBuf;
|
||||
//! use std::str::FromStr;
|
||||
//! use mdbook_core::config::Config;
|
||||
//! use toml::Value;
|
||||
//!
|
||||
//! # fn run() -> Result<()> {
|
||||
//! let src = r#"
|
||||
|
|
@ -21,9 +20,6 @@
|
|||
//! title = "My Book"
|
||||
//! authors = ["Michael-F-Bryan"]
|
||||
//!
|
||||
//! [build]
|
||||
//! src = "out"
|
||||
//!
|
||||
//! [preprocessor.my-preprocessor]
|
||||
//! bar = 123
|
||||
//! "#;
|
||||
|
|
@ -36,7 +32,7 @@
|
|||
//! assert_eq!(bar, Some(123));
|
||||
//!
|
||||
//! // 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");
|
||||
//!
|
||||
//! // then load it again, automatically deserializing to a `PathBuf`.
|
||||
|
|
@ -49,9 +45,9 @@
|
|||
|
||||
use crate::utils::TomlExt;
|
||||
use crate::utils::log_backtrace;
|
||||
use anyhow::{Context, Error, Result};
|
||||
use log::{debug, trace, warn};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use anyhow::{Context, Error, Result, bail};
|
||||
use log::{debug, trace};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
|
|
@ -63,16 +59,34 @@ use toml::value::Table;
|
|||
|
||||
/// The overall configuration object for MDBook, essentially an in-memory
|
||||
/// representation of `book.toml`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct Config {
|
||||
/// Metadata about the book.
|
||||
pub book: BookConfig,
|
||||
/// Information about the build environment.
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
pub build: BuildConfig,
|
||||
/// Information about Rust language support.
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
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 {
|
||||
|
|
@ -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 {
|
||||
/// Load the configuration file from disk.
|
||||
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`
|
||||
/// will fetch the "playground" out of the html output table).
|
||||
///
|
||||
/// This does not have access to the [`Config::book`], [`Config::build`],
|
||||
/// or [`Config::rust`] fields.
|
||||
/// This can only access the `output` and `preprocessor` tables.
|
||||
///
|
||||
/// Returns `Ok(None)` if the field is not set.
|
||||
///
|
||||
/// Returns `Err` if it fails to deserialize.
|
||||
pub fn get<'de, T: Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
|
||||
self.rest
|
||||
.read(name)
|
||||
let (key, table) = if let Some(key) = name.strip_prefix("output.") {
|
||||
(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| {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.with_context(|| "Couldn't deserialize the value")
|
||||
.with_context(|| "Failed to deserialize `{name}`")
|
||||
})
|
||||
.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.
|
||||
///
|
||||
/// # Note
|
||||
|
|
@ -187,10 +237,7 @@ impl Config {
|
|||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||
#[doc(hidden)]
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
match self
|
||||
.get("output.html")
|
||||
.with_context(|| "Parsing configuration [output.html]")
|
||||
{
|
||||
match self.get("output.html") {
|
||||
Ok(Some(config)) => Some(config),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
|
|
@ -220,110 +267,27 @@ impl Config {
|
|||
self.build.update_value(key, value);
|
||||
} else if let Some(key) = index.strip_prefix("rust.") {
|
||||
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 {
|
||||
self.rest.insert(index, value);
|
||||
bail!("invalid key `{index}`");
|
||||
}
|
||||
|
||||
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> {
|
||||
key.strip_prefix("MDBOOK_")
|
||||
.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
|
||||
/// loading it from disk.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct BookConfig {
|
||||
/// The book's title.
|
||||
|
|
@ -393,7 +357,7 @@ impl TextDirection {
|
|||
|
||||
/// Configuration for the build procedure.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct BuildConfig {
|
||||
/// 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)
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct RustConfig {
|
||||
/// Rust edition used in playground
|
||||
|
|
@ -448,7 +412,7 @@ pub enum RustEdition {
|
|||
|
||||
/// Configuration for the HTML renderer.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct HtmlConfig {
|
||||
/// The theme directory, if specified.
|
||||
|
|
@ -568,7 +532,7 @@ impl HtmlConfig {
|
|||
|
||||
/// Configuration for how to render the print icon, print.html, and print.css.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct Print {
|
||||
/// Whether print support is enabled.
|
||||
|
|
@ -588,7 +552,7 @@ impl Default for Print {
|
|||
|
||||
/// Configuration for how to fold chapters of sidebar.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct Fold {
|
||||
/// 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.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct Playground {
|
||||
/// 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.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct Code {
|
||||
/// 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.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct Search {
|
||||
/// Enable the search feature. Default: `true`.
|
||||
|
|
@ -698,7 +662,7 @@ impl Default for Search {
|
|||
|
||||
/// Search options for chapters (or paths).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub struct SearchChapterSettings {
|
||||
/// 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 {
|
||||
use super::*;
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use serde_json::json;
|
||||
|
||||
const COMPLEX_CONFIG: &str = r#"
|
||||
[book]
|
||||
|
|
@ -757,7 +720,6 @@ mod tests {
|
|||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
|
||||
[output.html.redirect]
|
||||
"index.html" = "overview.html"
|
||||
|
|
@ -943,19 +905,6 @@ mod tests {
|
|||
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]
|
||||
fn set_special_tables() {
|
||||
let mut cfg = Config::default();
|
||||
|
|
@ -970,6 +919,21 @@ mod tests {
|
|||
assert_eq!(cfg.rust.edition, None);
|
||||
cfg.set("rust.edition", "2024").unwrap();
|
||||
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]
|
||||
|
|
@ -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]
|
||||
fn file_404_default() {
|
||||
let src = r#"
|
||||
[output.html]
|
||||
destination = "my-book"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
|
|
@ -1063,7 +970,6 @@ mod tests {
|
|||
let src = r#"
|
||||
[output.html]
|
||||
input-404= "missing.md"
|
||||
output-404= "missing.html"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
|||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use mdbook_summary::Summary;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -423,22 +422,17 @@ struct OutputConfig {
|
|||
fn determine_renderers(config: &Config) -> Result<Vec<Box<dyn Renderer>>> {
|
||||
let mut renderers = Vec::new();
|
||||
|
||||
match config.get::<HashMap<String, OutputConfig>>("output") {
|
||||
Ok(Some(output_table)) => {
|
||||
renderers.extend(output_table.into_iter().map(|(key, table)| {
|
||||
if key == "html" {
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else if key == "markdown" {
|
||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||
} else {
|
||||
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
|
||||
Box::new(CmdRenderer::new(key, command))
|
||||
}
|
||||
}));
|
||||
let outputs = config.outputs::<OutputConfig>()?;
|
||||
renderers.extend(outputs.into_iter().map(|(key, table)| {
|
||||
if key == "html" {
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else if key == "markdown" {
|
||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||
} else {
|
||||
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
|
||||
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 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")
|
||||
{
|
||||
Ok(Some(preprocessor_table)) => preprocessor_table,
|
||||
Ok(None) => HashMap::new(),
|
||||
Err(e) => bail!("failed to get preprocessor table config: {e}"),
|
||||
};
|
||||
let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
|
||||
|
||||
for (name, table) in preprocessor_table.iter() {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ fn config_defaults_to_html_renderer_if_empty() {
|
|||
let cfg = Config::default();
|
||||
|
||||
// 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();
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
|
|||
let cfg = Config::default();
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::command_prelude::*;
|
|||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::Result;
|
||||
use axum::Router;
|
||||
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||
use axum::routing::get;
|
||||
|
|
@ -76,10 +76,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
|
||||
let build_dir = book.build_dir_for("html");
|
||||
let input_404 = match book.config.get::<String>("output.html.input-404") {
|
||||
Ok(v) => v,
|
||||
Err(e) => bail!("expected string for output.html.input-404: {e}"),
|
||||
};
|
||||
let html_config = book.config.html_config();
|
||||
let input_404 = html_config.and_then(|c| c.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.
|
||||
|
|
|
|||
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#"
|
||||
[book]
|
||||
authors = []
|
||||
language = "en"
|
||||
src = "src"
|
||||
language = "en"
|
||||
|
||||
"#]],
|
||||
)
|
||||
|
|
@ -94,8 +94,8 @@ All done, no errors...
|
|||
str![[r#"
|
||||
[book]
|
||||
authors = []
|
||||
language = "en"
|
||||
src = "src"
|
||||
language = "en"
|
||||
|
||||
"#]],
|
||||
);
|
||||
|
|
@ -126,10 +126,10 @@ All done, no errors...
|
|||
"book.toml",
|
||||
str![[r#"
|
||||
[book]
|
||||
authors = []
|
||||
language = "en"
|
||||
src = "src"
|
||||
title = "Example title"
|
||||
authors = []
|
||||
src = "src"
|
||||
language = "en"
|
||||
|
||||
"#]],
|
||||
);
|
||||
|
|
@ -179,14 +179,14 @@ fn init_with_custom_book_and_src_locations() {
|
|||
str![[r#"
|
||||
[book]
|
||||
authors = []
|
||||
language = "en"
|
||||
src = "in"
|
||||
language = "en"
|
||||
|
||||
[build]
|
||||
build-dir = "out"
|
||||
create-missing = true
|
||||
extra-watch-dirs = []
|
||||
use-default-preprocessors = true
|
||||
extra-watch-dirs = []
|
||||
|
||||
"#]],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
mod book_test;
|
||||
mod build;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod includes;
|
||||
mod index;
|
||||
mod init;
|
||||
|
|
|
|||
|
|
@ -186,8 +186,11 @@ fn backends_receive_render_context_via_stdin() {
|
|||
"config": {
|
||||
"book": {
|
||||
"authors": [],
|
||||
"description": null,
|
||||
"language": "en",
|
||||
"src": "src"
|
||||
"src": "src",
|
||||
"text-direction": null,
|
||||
"title": null
|
||||
},
|
||||
"output": {
|
||||
"cat-to-file": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue