From 529cfc34ec2aebb28aa2bf03d8ea16aefbf31772 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Fri, 25 Jul 2025 11:28:52 -0700 Subject: [PATCH] Remove toml as a public dependency This removes toml as a public dependency. This reduces the exposure of the public API, reduces exposure of internal implementation, and makes it easier to make semver-incompatible changes to toml. This is accomplished through a variety of changes: - `get` and `get_mut` are removed. - `get_deserialized_opt` is renamed to `get`. - Dropped the AsRef for `get_deserialized_opt` for ergonomics, since using an `&` for a String is not too much to ask, and the other generic arg needs to be specified in a fair number of situations. - Removed deprecated `get_deserialized`. - Dropped `TomlExt` from the public API. - Removed `get_renderer` and `get_preprocessor` since they were trivial wrappers over `get`. --- Cargo.lock | 1 + crates/mdbook-core/src/config.rs | 131 ++++--------- crates/mdbook-core/src/utils/mod.rs | 4 +- crates/mdbook-core/src/utils/toml_ext.rs | 12 +- crates/mdbook-driver/Cargo.toml | 1 + .../src/builtin_renderers/mod.rs | 6 +- crates/mdbook-driver/src/mdbook.rs | 182 ++++++++---------- crates/mdbook-driver/src/mdbook/tests.rs | 38 ++-- examples/nop-preprocessor.rs | 11 +- src/cmd/serve.rs | 11 +- 10 files changed, 160 insertions(+), 237 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10ec141e..9d692f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1315,6 +1315,7 @@ dependencies = [ "mdbook-renderer", "mdbook-summary", "regex", + "serde", "serde_json", "shlex", "tempfile", diff --git a/crates/mdbook-core/src/config.rs b/crates/mdbook-core/src/config.rs index 36e586a6..ef00b143 100644 --- a/crates/mdbook-core/src/config.rs +++ b/crates/mdbook-core/src/config.rs @@ -24,7 +24,7 @@ //! [build] //! src = "out" //! -//! [other-table.foo] +//! [preprocessor.my-preprocessor] //! bar = 123 //! "#; //! @@ -32,24 +32,24 @@ //! let mut cfg = Config::from_str(src)?; //! //! // retrieve a nested value -//! let bar = cfg.get("other-table.foo.bar").cloned(); -//! assert_eq!(bar, Some(Value::Integer(123))); +//! let bar = cfg.get::("preprocessor.my-preprocessor.bar")?; +//! assert_eq!(bar, Some(123)); //! //! // Set the `output.html.theme` directory -//! assert!(cfg.get("output.html").is_none()); +//! assert!(cfg.get::("output.html")?.is_none()); //! cfg.set("output.html.theme", "./themes"); //! //! // then load it again, automatically deserializing to a `PathBuf`. -//! let got: Option = cfg.get_deserialized_opt("output.html.theme")?; +//! let got = cfg.get("output.html.theme")?; //! assert_eq!(got, Some(PathBuf::from("./themes"))); //! # Ok(()) //! # } //! # run().unwrap() //! ``` +use crate::utils::TomlExt; use crate::utils::log_backtrace; -use crate::utils::toml_ext::TomlExt; -use anyhow::{Context, Error, Result, bail}; +use anyhow::{Context, Error, Result}; use log::{debug, trace, warn}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; @@ -154,18 +154,28 @@ impl Config { } } - /// Fetch an arbitrary item from the `Config` as a `toml::Value`. + /// Get a value from the configuration. /// - /// You can use dotted indices to access nested items (e.g. - /// `output.html.playground` will fetch the "playground" out of the html output - /// table). - pub fn get(&self, key: &str) -> Option<&Value> { - self.rest.read(key) - } - - /// Fetch a value from the `Config` so you can mutate it. - pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> { - self.rest.read_mut(key) + /// This fetches a value from the book configuration. The key can have + /// 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. + /// + /// 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> { + self.rest + .read(name) + .map(|value| { + value + .clone() + .try_into() + .with_context(|| "Couldn't deserialize the value") + }) + .transpose() } /// Convenience method for getting the html renderer's configuration. @@ -177,7 +187,7 @@ impl Config { #[doc(hidden)] pub fn html_config(&self) -> Option { match self - .get_deserialized_opt("output.html") + .get("output.html") .with_context(|| "Parsing configuration [output.html]") { Ok(Some(config)) => Some(config), @@ -189,35 +199,12 @@ impl Config { } } - /// Deprecated, use get_deserialized_opt instead. - #[deprecated = "use get_deserialized_opt instead"] - pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef>(&self, name: S) -> Result { - let name = name.as_ref(); - match self.get_deserialized_opt(name)? { - Some(value) => Ok(value), - None => bail!("Key not found, {:?}", name), - } - } - - /// Convenience function to fetch a value from the config and deserialize it - /// into some arbitrary type. - pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef>( - &self, - name: S, - ) -> Result> { - let name = name.as_ref(); - self.get(name) - .map(|value| { - value - .clone() - .try_into() - .with_context(|| "Couldn't deserialize the value") - }) - .transpose() - } - /// Set a config key, clobbering any existing values along the way. /// + /// The key can have dotted indices for nested items (e.g. + /// `output.html.playground` will set the "playground" in the html output + /// table). + /// /// The only way this can fail is if we can't serialize `value` into a /// `toml::Value`. pub fn set>(&mut self, index: I, value: S) -> Result<()> { @@ -239,18 +226,6 @@ impl Config { Ok(()) } - /// Get the table associated with a particular renderer. - pub fn get_renderer>(&self, index: I) -> Option<&Table> { - let key = format!("output.{}", index.as_ref()); - self.get(&key).and_then(Value::as_table) - } - - /// Get the table associated with a particular preprocessor. - pub fn get_preprocessor>(&self, index: I) -> Option<&Table> { - let key = format!("preprocessor.{}", index.as_ref()); - self.get(&key).and_then(Value::as_table) - } - fn from_legacy(mut table: Value) -> Config { let mut cfg = Config::default(); @@ -1021,32 +996,16 @@ mod tests { }; let cfg = Config::from_str(src).unwrap(); - let got: RandomOutput = cfg.get_deserialized_opt("output.random").unwrap().unwrap(); + let got: RandomOutput = cfg.get("output.random").unwrap().unwrap(); assert_eq!(got, should_be); - let got_baz: Vec = cfg - .get_deserialized_opt("output.random.baz") - .unwrap() - .unwrap(); + let got_baz: Vec = cfg.get("output.random.baz").unwrap().unwrap(); let baz_should_be = vec![true, true, false]; assert_eq!(got_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.playground.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)); - } - /// The config file format has slightly changed (metadata stuff is now under /// the `book` table instead of being at the top level) so we're adding a /// **temporary** compatibility check. You should be able to still load the @@ -1106,10 +1065,10 @@ mod tests { let key = "foo.bar.baz"; let value = "Something Interesting"; - assert!(cfg.get(key).is_none()); + assert!(cfg.get::(key).unwrap().is_none()); cfg.set(key, value).unwrap(); - let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap(); + let got: String = cfg.get(key).unwrap().unwrap(); assert_eq!(got, value); } @@ -1159,7 +1118,7 @@ mod tests { let key = "foo.bar"; let value = "baz"; - assert!(cfg.get(key).is_none()); + assert!(cfg.get::(key).unwrap().is_none()); let encoded_key = encode_env_var(key); // TODO: This is unsafe, and should be rewritten to use a process. @@ -1167,10 +1126,7 @@ mod tests { cfg.update_from_env(); - assert_eq!( - cfg.get_deserialized_opt::(key).unwrap().unwrap(), - value - ); + assert_eq!(cfg.get::(key).unwrap().unwrap(), value); } #[test] @@ -1180,7 +1136,7 @@ mod tests { let value = json!({"array": [1, 2, 3], "number": 13.37}); let value_str = serde_json::to_string(&value).unwrap(); - assert!(cfg.get(key).is_none()); + assert!(cfg.get::(key).unwrap().is_none()); let encoded_key = encode_env_var(key); // TODO: This is unsafe, and should be rewritten to use a process. @@ -1188,12 +1144,7 @@ mod tests { cfg.update_from_env(); - assert_eq!( - cfg.get_deserialized_opt::(key) - .unwrap() - .unwrap(), - value - ); + assert_eq!(cfg.get::(key).unwrap().unwrap(), value); } #[test] diff --git a/crates/mdbook-core/src/utils/mod.rs b/crates/mdbook-core/src/utils/mod.rs index caca5dcd..99a8e543 100644 --- a/crates/mdbook-core/src/utils/mod.rs +++ b/crates/mdbook-core/src/utils/mod.rs @@ -9,7 +9,9 @@ use std::sync::LazyLock; pub mod fs; mod string; -pub mod toml_ext; +mod toml_ext; + +pub(crate) use self::toml_ext::TomlExt; pub use self::string::{ take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines, diff --git a/crates/mdbook-core/src/utils/toml_ext.rs b/crates/mdbook-core/src/utils/toml_ext.rs index 7975ed57..51ed8435 100644 --- a/crates/mdbook-core/src/utils/toml_ext.rs +++ b/crates/mdbook-core/src/utils/toml_ext.rs @@ -3,11 +3,9 @@ use toml::value::{Table, Value}; /// Helper for working with toml types. -pub trait TomlExt { +pub(crate) trait TomlExt { /// Read a dotted key. fn read(&self, key: &str) -> Option<&Value>; - /// Read a dotted key for a mutable value. - fn read_mut(&mut self, key: &str) -> Option<&mut Value>; /// Insert with a dotted key. fn insert(&mut self, key: &str, value: Value); /// Delete a dotted key value. @@ -23,14 +21,6 @@ impl TomlExt for Value { } } - fn read_mut(&mut self, key: &str) -> Option<&mut Value> { - if let Some((head, tail)) = split(key) { - self.get_mut(head)?.read_mut(tail) - } else { - self.get_mut(key) - } - } - fn insert(&mut self, key: &str, value: Value) { if !self.is_table() { *self = Value::Table(Table::new()); diff --git a/crates/mdbook-driver/Cargo.toml b/crates/mdbook-driver/Cargo.toml index 5e9b348e..f7edc053 100644 --- a/crates/mdbook-driver/Cargo.toml +++ b/crates/mdbook-driver/Cargo.toml @@ -17,6 +17,7 @@ mdbook-preprocessor.workspace = true mdbook-renderer.workspace = true mdbook-summary.workspace = true regex.workspace = true +serde.workspace = true serde_json.workspace = true shlex.workspace = true tempfile.workspace = true diff --git a/crates/mdbook-driver/src/builtin_renderers/mod.rs b/crates/mdbook-driver/src/builtin_renderers/mod.rs index 0218a62d..6ac1ee24 100644 --- a/crates/mdbook-driver/src/builtin_renderers/mod.rs +++ b/crates/mdbook-driver/src/builtin_renderers/mod.rs @@ -10,7 +10,6 @@ use std::fs; use std::io::{self, ErrorKind}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use toml::Value; pub use self::markdown_renderer::MarkdownRenderer; @@ -108,8 +107,9 @@ impl CmdRenderer { let optional_key = format!("output.{}.optional", self.name); let is_optional = match ctx.config.get(&optional_key) { - Some(Value::Boolean(value)) => *value, - _ => false, + Ok(Some(value)) => value, + Err(e) => bail!("expected bool for `{optional_key}`: {e}"), + Ok(None) => false, }; if is_optional { diff --git a/crates/mdbook-driver/src/mdbook.rs b/crates/mdbook-driver/src/mdbook.rs index 701a0161..533a1d84 100644 --- a/crates/mdbook-driver/src/mdbook.rs +++ b/crates/mdbook-driver/src/mdbook.rs @@ -13,12 +13,13 @@ use mdbook_html::HtmlHandlebars; 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}; use std::process::Command; use tempfile::Builder as TempFileBuilder; -use toml::Value; use topological_sort::TopologicalSort; #[cfg(test)] @@ -99,7 +100,7 @@ impl MDBook { let src_dir = root.join(&config.book.src); let book = load_book(src_dir, &config.build)?; - let renderers = determine_renderers(&config); + let renderers = determine_renderers(&config)?; let preprocessors = determine_preprocessors(&config)?; Ok(MDBook { @@ -122,7 +123,7 @@ impl MDBook { let src_dir = root.join(&config.book.src); let book = load_book_from_disk(&summary, src_dir)?; - let renderers = determine_renderers(&config); + let renderers = determine_renderers(&config)?; let preprocessors = determine_preprocessors(&config)?; Ok(MDBook { @@ -201,7 +202,7 @@ impl MDBook { ); let mut preprocessed_book = self.book.clone(); for preprocessor in &self.preprocessors { - if preprocessor_should_run(&**preprocessor, renderer, &self.config) { + if preprocessor_should_run(&**preprocessor, renderer, &self.config)? { debug!("Running the {} preprocessor.", preprocessor.name()); preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?; } @@ -420,20 +421,31 @@ impl MDBook { } } +/// An `output` table. +#[derive(Deserialize)] +struct OutputConfig { + command: Option, +} + /// Look at the `Config` and try to figure out what renderers to use. -fn determine_renderers(config: &Config) -> Vec> { +fn determine_renderers(config: &Config) -> Result>> { let mut renderers = Vec::new(); - if let Some(output_table) = config.get("output").and_then(Value::as_table) { - renderers.extend(output_table.iter().map(|(key, table)| { - if key == "html" { - Box::new(HtmlHandlebars::new()) as Box - } else if key == "markdown" { - Box::new(MarkdownRenderer::new()) as Box - } else { - interpret_custom_renderer(key, table) - } - })); + match config.get::>("output") { + Ok(Some(output_table)) => { + renderers.extend(output_table.into_iter().map(|(key, table)| { + if key == "html" { + Box::new(HtmlHandlebars::new()) as Box + } else if key == "markdown" { + Box::new(MarkdownRenderer::new()) as Box + } 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 @@ -441,7 +453,7 @@ fn determine_renderers(config: &Config) -> Vec> { renderers.push(Box::new(HtmlHandlebars::new())); } - renderers + Ok(renderers) } const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"]; @@ -451,6 +463,16 @@ fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool { name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME } +/// A `preprocessor` table. +#[derive(Deserialize)] +struct PreprocessorConfig { + command: Option, + #[serde(default)] + before: Vec, + #[serde(default)] + after: Vec, +} + /// Look at the `MDBook` and try to figure out what preprocessors to run. fn determine_preprocessors(config: &Config) -> Result>> { // Collect the names of all preprocessors intended to be run, and the order @@ -463,62 +485,43 @@ fn determine_preprocessors(config: &Config) -> Result> } } - if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) { - for (name, table) in preprocessor_table.iter() { - preprocessor_names.insert(name.to_string()); + let preprocessor_table = match config.get::>("preprocessor") + { + Ok(Some(preprocessor_table)) => preprocessor_table, + Ok(None) => HashMap::new(), + Err(e) => bail!("failed to get preprocessor table config: {e}"), + }; - let exists = |name| { - (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name)) - || preprocessor_table.contains_key(name) - }; + for (name, table) in preprocessor_table.iter() { + preprocessor_names.insert(name.to_string()); - if let Some(before) = table.get("before") { - let before = before.as_array().ok_or_else(|| { - Error::msg(format!( - "Expected preprocessor.{name}.before to be an array" - )) - })?; - for after in before { - let after = after.as_str().ok_or_else(|| { - Error::msg(format!( - "Expected preprocessor.{name}.before to contain strings" - )) - })?; + let exists = |name| { + (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name)) + || preprocessor_table.contains_key(name) + }; - if !exists(after) { - // Only warn so that preprocessors can be toggled on and off (e.g. for - // troubleshooting) without having to worry about order too much. - warn!( - "preprocessor.{}.after contains \"{}\", which was not found", - name, after - ); - } else { - preprocessor_names.add_dependency(name, after); - } - } + for after in &table.before { + if !exists(&after) { + // Only warn so that preprocessors can be toggled on and off (e.g. for + // troubleshooting) without having to worry about order too much. + warn!( + "preprocessor.{}.after contains \"{}\", which was not found", + name, after + ); + } else { + preprocessor_names.add_dependency(name, after); } + } - if let Some(after) = table.get("after") { - let after = after.as_array().ok_or_else(|| { - Error::msg(format!("Expected preprocessor.{name}.after to be an array")) - })?; - for before in after { - let before = before.as_str().ok_or_else(|| { - Error::msg(format!( - "Expected preprocessor.{name}.after to contain strings" - )) - })?; - - if !exists(before) { - // See equivalent warning above for rationale - warn!( - "preprocessor.{}.before contains \"{}\", which was not found", - name, before - ); - } else { - preprocessor_names.add_dependency(before, name); - } - } + for before in &table.after { + if !exists(&before) { + // See equivalent warning above for rationale + warn!( + "preprocessor.{}.before contains \"{}\", which was not found", + name, before + ); + } else { + preprocessor_names.add_dependency(before, name); } } } @@ -544,8 +547,11 @@ fn determine_preprocessors(config: &Config) -> Result> _ => { // The only way to request a custom preprocessor is through the `preprocessor` // table, so it must exist, be a table, and contain the key. - let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name]; - let command = get_custom_preprocessor_cmd(&name, table); + let table = &preprocessor_table[&name]; + let command = table + .command + .to_owned() + .unwrap_or_else(|| format!("mdbook-{name}")); Box::new(CmdPreprocessor::new(name, command)) } }; @@ -562,27 +568,6 @@ fn determine_preprocessors(config: &Config) -> Result> } } -fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String { - table - .get("command") - .and_then(Value::as_str) - .map(ToString::to_string) - .unwrap_or_else(|| format!("mdbook-{key}")) -} - -fn interpret_custom_renderer(key: &str, table: &Value) -> Box { - // look for the `command` field, falling back to using the key - // prepended by "mdbook-" - let table_dot_command = table - .get("command") - .and_then(Value::as_str) - .map(ToString::to_string); - - let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{key}")); - - Box::new(CmdRenderer::new(key.to_string(), command)) -} - /// Check whether we should run a particular `Preprocessor` in combination /// with the renderer, falling back to `Preprocessor::supports_renderer()` /// method if the user doesn't say anything. @@ -593,21 +578,20 @@ fn preprocessor_should_run( preprocessor: &dyn Preprocessor, renderer: &dyn Renderer, cfg: &Config, -) -> bool { +) -> Result { // default preprocessors should be run by default (if supported) if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) { - return preprocessor.supports_renderer(renderer.name()); + return Ok(preprocessor.supports_renderer(renderer.name())); } let key = format!("preprocessor.{}.renderers", preprocessor.name()); let renderer_name = renderer.name(); - if let Some(Value::Array(explicit_renderers)) = cfg.get(&key) { - return explicit_renderers - .iter() - .filter_map(Value::as_str) - .any(|name| name == renderer_name); + match cfg.get::>(&key) { + Ok(Some(explicit_renderers)) => { + Ok(explicit_renderers.iter().any(|name| name == renderer_name)) + } + Ok(None) => Ok(preprocessor.supports_renderer(renderer_name)), + Err(e) => bail!("failed to get `{key}`: {e}"), } - - preprocessor.supports_renderer(renderer_name) } diff --git a/crates/mdbook-driver/src/mdbook/tests.rs b/crates/mdbook-driver/src/mdbook/tests.rs index 8040217d..c5caf36f 100644 --- a/crates/mdbook-driver/src/mdbook/tests.rs +++ b/crates/mdbook-driver/src/mdbook/tests.rs @@ -1,15 +1,15 @@ use super::*; use std::str::FromStr; -use toml::value::Table; +use toml::value::{Table, Value}; #[test] 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("output").is_none()); + assert!(cfg.get::("output").unwrap().is_none()); - let got = determine_renderers(&cfg); + let got = determine_renderers(&cfg).unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].name(), "html"); @@ -20,7 +20,7 @@ fn add_a_random_renderer_to_the_config() { let mut cfg = Config::default(); cfg.set("output.random", Table::new()).unwrap(); - let got = determine_renderers(&cfg); + let got = determine_renderers(&cfg).unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].name(), "random"); @@ -34,7 +34,7 @@ fn add_a_random_renderer_with_custom_command_to_the_config() { table.insert("command".to_string(), Value::String("false".to_string())); cfg.set("output.random", table).unwrap(); - let got = determine_renderers(&cfg); + let got = determine_renderers(&cfg).unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].name(), "random"); @@ -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("preprocessor").is_none()); + assert!(cfg.get::("preprocessor").unwrap().is_none()); let got = determine_preprocessors(&cfg); @@ -81,7 +81,7 @@ fn can_determine_third_party_preprocessors() { let cfg = Config::from_str(cfg_str).unwrap(); // make sure the `preprocessor.random` table exists - assert!(cfg.get_preprocessor("random").is_some()); + assert!(cfg.get::("preprocessor.random").unwrap().is_some()); let got = determine_preprocessors(&cfg).unwrap(); @@ -98,10 +98,11 @@ fn preprocessors_can_provide_their_own_commands() { let cfg = Config::from_str(cfg_str).unwrap(); // make sure the `preprocessor.random` table exists - let random = cfg.get_preprocessor("random").unwrap(); - let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone())); - - assert_eq!(random, "python random.py"); + let random = cfg + .get::("preprocessor.random") + .unwrap() + .unwrap(); + assert_eq!(random.command, Some("python random.py".to_string())); } #[test] @@ -229,19 +230,10 @@ fn config_respects_preprocessor_selection() { let cfg = Config::from_str(cfg_str).unwrap(); - // double-check that we can access preprocessor.links.renderers[0] - let html = cfg - .get_preprocessor("links") - .and_then(|links| links.get("renderers")) - .and_then(Value::as_array) - .and_then(|renderers| renderers.get(0)) - .and_then(Value::as_str) - .unwrap(); - assert_eq!(html, "html"); let html_renderer = HtmlHandlebars; let pre = LinkPreprocessor::new(); - let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg); + let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg).unwrap(); assert!(should_run); } @@ -266,10 +258,10 @@ fn preprocessor_should_run_falls_back_to_supports_renderer_method() { let html = HtmlHandlebars::new(); let should_be = true; - let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); + let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap(); assert_eq!(got, should_be); let should_be = false; - let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); + let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap(); assert_eq!(got, should_be); } diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 1c599d6d..ffb19e1c 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -92,10 +92,13 @@ mod nop_lib { fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result { // In testing we want to tell the preprocessor to blow up by setting a // particular config value - if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) { - if nop_cfg.contains_key("blow-up") { - anyhow::bail!("Boom!!1!"); - } + match ctx + .config + .get::("preprocessor.nop-preprocessor.blow-up") + { + Ok(Some(true)) => anyhow::bail!("Boom!!1!"), + Ok(_) => {} + Err(e) => anyhow::bail!("expect bool for blow-up: {e}"), } // we *are* a no-op preprocessor after all diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index d1f5d0e4..a8751c5a 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -2,7 +2,7 @@ use super::command_prelude::*; #[cfg(feature = "watch")] use super::watch; use crate::{get_book_dir, open}; -use anyhow::Result; +use anyhow::{Result, bail}; use axum::Router; use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::routing::get; @@ -76,11 +76,10 @@ 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 = book - .config - .get("output.html.input-404") - .and_then(toml::Value::as_str) - .map(ToString::to_string); + let input_404 = match book.config.get::("output.html.input-404") { + Ok(v) => v, + Err(e) => bail!("expected string for output.html.input-404: {e}"), + }; let file_404 = get_404_output_file(&input_404); // A channel used to broadcast to any websockets to reload when a file changes.