//! Processing a `ConfigurationTree` into a validated configuration //! //! This module, and particularly [`resolve`], takes care of: //! //! * Deserializing a [`ConfigurationTree`] into various `FooConfigBuilder` //! * Calling the `build()` methods to get various `FooConfig`. //! * Reporting unrecognised configuration keys //! (eg to help the user detect misspellings). //! //! This is step 3 of the overall config processing, //! as described in the [crate-level documentation](crate). //! //! # Starting points //! //! To use this, you will need to: //! //! * `#[derive(Builder)]` and use [`impl_standard_builder!`](crate::impl_standard_builder) //! for all of your configuration structures, //! using `#[sub_builder]` etc. sa appropriate, //! and making your builders [`Deserialize`](serde::Deserialize). //! //! * [`impl TopLevel`](TopLevel) for your *top level* structures (only). //! //! * Call [`resolve`] (or one of its variants) with a `ConfigurationTree`, //! to obtain your top-level configuration(s). //! //! # Example //! //! In this example the developers are embedding `arti`, `arti_client`, etc., //! into a program of their own. The example code shown: //! //! * Defines a configuration structure `EmbedderConfig`, //! for additional configuration settings for the added features. //! * Establishes some configuration sources //! (the trivial empty `ConfigSources`, to avoid clutter in the example) //! * Reads those sources into a single configuration taxonomy [`ConfigurationTree`]. //! * Processes that configuration into a 3-tuple of configuration //! structs for the three components, namely: //! - `TorClientConfig`, the configuration for the `arti_client` crate's `TorClient` //! - `ArtiConfig`, for behaviours in the `arti` command line utility //! - `EmbedderConfig`. //! * Will report a warning to the user about config settings found in the config files, //! but not recognized by *any* of the three config consumers, //! //! ``` //! # fn main() -> Result<(), tor_config::load::ConfigResolveError> { //! use derive_builder::Builder; //! use tor_config::{impl_standard_builder, resolve, ConfigBuildError, ConfigurationSources}; //! use tor_config::load::TopLevel; //! use serde::{Deserialize, Serialize}; //! //! #[derive(Debug, Clone, Builder, Eq, PartialEq)] //! #[builder(build_fn(error = "ConfigBuildError"))] //! #[builder(derive(Debug, Serialize, Deserialize))] //! struct EmbedderConfig { //! // .... //! } //! impl_standard_builder! { EmbedderConfig } //! impl TopLevel for EmbedderConfig { //! type Builder = EmbedderConfigBuilder; //! } //! # //! # #[derive(Debug, Clone, Builder, Eq, PartialEq)] //! # #[builder(build_fn(error = "ConfigBuildError"))] //! # #[builder(derive(Debug, Serialize, Deserialize))] //! # struct TorClientConfig { } //! # impl_standard_builder! { TorClientConfig } //! # impl TopLevel for TorClientConfig { type Builder = TorClientConfigBuilder; } //! # //! # #[derive(Debug, Clone, Builder, Eq, PartialEq)] //! # #[builder(build_fn(error = "ConfigBuildError"))] //! # #[builder(derive(Debug, Serialize, Deserialize))] //! # struct ArtiConfig { } //! # impl_standard_builder! { ArtiConfig } //! # impl TopLevel for ArtiConfig { type Builder = ArtiConfigBuilder; } //! //! let cfg_sources = ConfigurationSources::new_empty(); // In real program, use from_cmdline //! let cfg = cfg_sources.load()?; //! //! let (tcc, arti_config, embedder_config) = //! tor_config::resolve::<(TorClientConfig, ArtiConfig, EmbedderConfig)>(cfg)?; //! //! let _: EmbedderConfig = embedder_config; // etc. //! //! # Ok(()) //! # } //! ``` use std::collections::BTreeSet; use std::fmt::{self, Display}; use std::iter; use std::mem; use itertools::{chain, izip, Itertools}; use serde::de::DeserializeOwned; use thiserror::Error; use tracing::warn; use crate::{ConfigBuildError, ConfigurationTree}; /// Error resolving a configuration (during deserialize, or build) #[derive(Error, Debug)] #[non_exhaustive] pub enum ConfigResolveError { /// Deserialize failed #[error("Config contents not as expected")] Deserialize(#[from] crate::ConfigError), /// Build failed #[error("Config semantically incorrect")] Build(#[from] ConfigBuildError), } /// A type that can be built from a builder via a build method pub trait Builder { /// The type that this builder constructs type Built; /// Build into a `Built` /// /// Often shadows an inherent `build` method fn build(&self) -> Result; } /// Collection of configuration settings that can be deserialized and then built /// /// *Do not implement directly.* /// Instead, implement [`TopLevel`]: doing so engages the blanket impl /// for (loosely) `TopLevel + Builder`. /// /// Each `Resolvable` corresponds to one or more configuration consumers. /// /// Ultimately, one `Resolvable` for all the configuration consumers in an entire /// program will be resolved from a single configuration tree (usually parsed from TOML). /// /// Multiple config collections can be resolved from the same configuration, /// via the implementation of `Resolvable` on tuples of `Resolvable`s. /// Use this rather than `#[serde(flatten)]`; the latter prevents useful introspection /// (necessary for reporting unrecognized configuration keys, and testing). /// /// (The `resolve` method will be called only from within the `tor_config::load` module.) pub trait Resolvable: Sized { /// Deserialize and build from a configuration // // Implementations must do the following: // // 1. Deserializes the input (cloning it to be able to do this) // into the `Builder`. // // 2. Having used `serde_ignored` to detect unrecognized keys, // intersects those with the unrecognized keys recorded in the context. // // 3. Calls `build` on the `Builder` to get `Self`. // // We provide impls for TopLevels, and tuples of Resolvable. // // Cannot be implemented outside this module (except eg as a wrapper or something), // because that would somehow involve creating `Self` from `ResolveContext` // but `ResolveContext` is completely opaque outside this module. fn resolve(input: &mut ResolveContext) -> Result; /// Return a list of deprecated config keys, as "."-separated strings fn enumerate_deprecated_keys(f: &mut F) where F: FnMut(&'static [&'static str]); } /// Top-level configuration struct, made from a deserializable builder /// /// One configuration consumer's configuration settings. /// /// Implementing this trait only for top-level configurations, /// which are to be parsed at the root level of a (TOML) config file taxonomy. /// /// This trait exists to: /// /// * Mark the toplevel configuration structures as suitable for use with [`resolve`] /// * Provide the type of the `Builder` for use by Rust generic code pub trait TopLevel { /// The `Builder` which can be used to make a `Self` /// /// Should satisfy `&'_ Self::Builder: Builder` type Builder: DeserializeOwned; /// Deprecated config keys, as "."-separates strings const DEPRECATED_KEYS: &'static [&'static str] = &[]; } /// `impl Resolvable for (A,B..) where A: Resolvable, B: Resolvable ...` /// /// The implementation simply calls `Resolvable::resolve` for each output tuple member. /// /// `define_for_tuples!{ A B - C D.. }` /// /// expands to /// 1. `define_for_tuples!{ A B - }`: defines for tuple `(A,B,)` /// 2. `define_for_tuples!{ A B C - D.. }`: recurses to generate longer tuples macro_rules! define_for_tuples { { $( $A:ident )* - $B:ident $( $C:ident )* } => { define_for_tuples!{ $($A)* - } define_for_tuples!{ $($A)* $B - $($C)* } }; { $( $A:ident )* - } => { impl < $($A,)* > Resolvable for ( $($A,)* ) where $( $A: Resolvable, )* { fn resolve(cfg: &mut ResolveContext) -> Result { Ok(( $( $A::resolve(cfg)?, )* )) } fn enumerate_deprecated_keys(f: &mut NF) where NF: FnMut(&'static [&'static str]) { $( $A::enumerate_deprecated_keys(f); )* } } }; } // We could avoid recursion by writing out A B C... several times (in a "triangle") but this // would make it tiresome and error-prone to extend the impl to longer tuples. define_for_tuples! { A - B C D E } /// Config resolution context, not used outside `tor_config::load` /// /// This is public only because it appears in the [`Resolvable`] trait. /// You don't want to try to obtain one. pub struct ResolveContext { /// The input input: ConfigurationTree, /// Paths unrecognized by all deserializations /// /// None means we haven't deserialized anything yet, ie means the universal set. /// /// Empty is used to disable this feature. unrecognized: UnrecognizedKeys, } /// Keys we have *not* recognized so far /// /// Initially `AllKeys`, since we haven't recognized any. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] enum UnrecognizedKeys { /// No keys have yet been recognized, so everything in the config is unrecognized AllKeys, /// The keys which remain unrecognized by any consumer /// /// If this is empty, we do not (need to) do any further tracking. These(BTreeSet), } use UnrecognizedKeys as UK; impl UnrecognizedKeys { /// Does it represent the empty set fn is_empty(&self) -> bool { match self { UK::AllKeys => false, UK::These(ign) => ign.is_empty(), } } /// Update in place, intersecting with `other` fn intersect_with(&mut self, other: BTreeSet) { match self { UK::AllKeys => *self = UK::These(other), UK::These(self_) => { let tign = mem::take(self_); *self_ = intersect_unrecognized_lists(tign, other); } } } /// Remove every element of this set. fn clear(&mut self) { *self = UK::These(BTreeSet::new()); } } /// Key in config file(s) which is disfavoured (unrecognized or deprecated) /// /// [`Display`]s in an approximation to TOML format. /// You can use the [`to_string()`](ToString::to_string) method to obtain /// a string containing a TOML key path. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct DisfavouredKey { /// Can be empty only before returned from this module path: Vec, } /// Element of an DisfavouredKey #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] enum PathEntry { /// Array index /// ArrayIndex(usize), /// Map entry /// /// string value is unquoted, needs quoting for display MapEntry(String), } /// Deserialize and build overall configuration from config sources /// /// Inner function used by all the `resolve_*` family fn resolve_inner( input: ConfigurationTree, want_disfavoured: bool, ) -> Result, ConfigResolveError> where T: Resolvable, { let mut deprecated = BTreeSet::new(); if want_disfavoured { T::enumerate_deprecated_keys(&mut |l: &[&str]| { for key in l { match input.0.find_value(key) { Err(_) => {} Ok(_) => { deprecated.insert(key); } } } }); } let mut lc = ResolveContext { input, unrecognized: if want_disfavoured { UK::AllKeys } else { UK::These(BTreeSet::new()) }, }; let value = Resolvable::resolve(&mut lc)?; let unrecognized = match lc.unrecognized { UK::AllKeys => panic!("all unrecognized, as if we had processed nothing"), UK::These(ign) => ign, } .into_iter() .filter(|ip| !ip.path.is_empty()) .collect_vec(); let deprecated = deprecated .into_iter() .map(|key| { let path = key .split('.') .map(|e| PathEntry::MapEntry(e.into())) .collect_vec(); DisfavouredKey { path } }) .collect_vec(); Ok(ResolutionResults { value, unrecognized, deprecated, }) } /// Deserialize and build overall configuration from config sources /// /// Unrecognized config keys are reported as log warning messages. /// /// Resolve the whole configuration in one go, using the `Resolvable` impl on `(A,B)` /// if necessary, so that unrecognized config key processing works correctly. /// /// This performs step 3 of the overall config processing, /// as described in the [`tor_config` crate-level documentation](crate). /// /// For an example, see the /// [`tor_config::load` module-level documentation](self). pub fn resolve(input: ConfigurationTree) -> Result where T: Resolvable, { let ResolutionResults { value, unrecognized, deprecated, } = resolve_inner(input, true)?; for depr in deprecated { warn!("deprecated configuration key: {}", &depr); } for ign in unrecognized { warn!("unrecognized configuration key: {}", &ign); } Ok(value) } /// Deserialize and build overall configuration, reporting unrecognized keys in the return value pub fn resolve_return_results( input: ConfigurationTree, ) -> Result, ConfigResolveError> where T: Resolvable, { resolve_inner(input, true) } /// Results of a successful `resolve_return_disfavoured` #[derive(Debug, Clone)] #[non_exhaustive] pub struct ResolutionResults { /// The configuration, successfully parsed pub value: T, /// Any config keys which were found in the input, but not recognized (and so, ignored) pub unrecognized: Vec, /// Any config keys which were found, but have been declared deprecated pub deprecated: Vec, } /// Deserialize and build overall configuration, silently ignoring unrecognized config keys pub fn resolve_ignore_warnings(input: ConfigurationTree) -> Result where T: Resolvable, { Ok(resolve_inner(input, false)?.value) } /// Wrapper around T that collects ignored keys as we deserialize it. /// /// (We need a helper type here since figment does not expose a `Deserializer` /// implementation directly.) struct Des { /// A set of the ignored keys that we found nign: BTreeSet, /// The underlying value we're deserializing. value: T, } impl<'de, T> serde::Deserialize<'de> for Des where T: serde::Deserialize<'de>, { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let mut nign = BTreeSet::new(); let mut recorder = |path: serde_ignored::Path<'_>| { nign.insert(copy_path(&path)); }; let deser = serde_ignored::Deserializer::new(deserializer, &mut recorder); let ret = serde::Deserialize::deserialize(deser); Ok(Des { nign, value: ret? }) } } impl Resolvable for T where T: TopLevel, T::Builder: Builder, { fn resolve(input: &mut ResolveContext) -> Result { let deser = input.input.clone(); let builder: Result = { // If input.unrecognized.is_empty() then we don't bother tracking the // unrecognized keys since we would intersect with the empty set. // That is how this tracking is disabled when we want it to be. let want_unrecognized = !input.unrecognized.is_empty(); if !want_unrecognized { deser.0.extract_lossy() } else { let ret: Result::Builder>, _> = deser.0.extract_lossy(); match ret { Ok(Des { nign, value }) => { input.unrecognized.intersect_with(nign); Ok(value) } Err(e) => { // If we got an error, the config might only have been partially processed, // so we might get false positives. Disable the unrecognized tracking. input.unrecognized.clear(); Err(e) } } } }; let built = builder.map_err(crate::ConfigError::from_cfg_err)?.build()?; Ok(built) } fn enumerate_deprecated_keys(f: &mut NF) where NF: FnMut(&'static [&'static str]), { f(T::DEPRECATED_KEYS); } } /// Turns a [`serde_ignored::Path`] (which is borrowed) into an owned `DisfavouredKey` fn copy_path(mut path: &serde_ignored::Path) -> DisfavouredKey { use serde_ignored::Path as SiP; use PathEntry as PE; let mut descend = vec![]; loop { let (new_path, ent) = match path { SiP::Root => break, SiP::Seq { parent, index } => (parent, Some(PE::ArrayIndex(*index))), SiP::Map { parent, key } => (parent, Some(PE::MapEntry(key.clone()))), SiP::Some { parent } | SiP::NewtypeStruct { parent } | SiP::NewtypeVariant { parent } => (parent, None), }; descend.extend(ent); path = new_path; } descend.reverse(); DisfavouredKey { path: descend } } /// Computes the intersection, resolving ignorances at different depths /// /// Eg if `a` contains `application.wombat` and `b` contains `application`, /// we need to return `application.wombat`. /// /// # Formally /// /// A configuration key (henceforth "key") is a sequence of `PathEntry`, /// interpreted as denoting a place in a tree-like hierarchy. /// /// Each input `BTreeSet` denotes a subset of the configuration key space. /// Any key in the set denotes itself, but also all possible keys which have it as a prefix. /// We say a s set is "minimal" if it doesn't have entries made redundant by this rule. /// /// This function computes a minimal intersection of two minimal inputs. /// If the inputs are not minimal, the output may not be either /// (although `serde_ignored` gives us minimal sets, so that case is not important). fn intersect_unrecognized_lists( al: BTreeSet, bl: BTreeSet, ) -> BTreeSet { //eprintln!("INTERSECT:"); //for ai in &al { eprintln!("A: {}", ai); } //for bi in &bl { eprintln!("B: {}", bi); } // This function is written to never talk about "a" and "b". // That (i) avoids duplication of code for handling ab, etc. // (ii) make impossible bugs where a was written but b was intended, etc. // The price is that the result is iterator combinator soup. let mut inputs: [_; 2] = [al, bl].map(|input| input.into_iter().peekable()); let mut output = BTreeSet::new(); // The BTreeSets produce items in sort order. // // We maintain the following invariants (valid at the top of the loop): // // For every possible key *strictly earlier* than those remaining in either input, // the output contains the key iff it was in the intersection. // // No other keys appear in the output. // // We peek at the next two items. The possible cases are: // // 0. One or both inputs is used up. In that case none of any remaining input // can be in the intersection and we are done. // // 1. The two inputs have the same next item. In that case the item is in the // intersection. If the inputs are minimal, no children of that item can appear // in either input, so we can make our own output minimal without thinking any // more about this item from the point of view of either list. // // 2. One of the inputs is a prefix of the other. In this case the longer item is // in the intersection - as are all subsequent items from the same input which // also share that prefix. Then, we must discard the shorter item (which denotes // the whole subspace of which only part is in the intersection). // // 3. Otherwise, the earlier item is definitely not in the intersection and // we can munch it. // Peek one from each, while we can. while let Ok(items) = { // Ideally we would use array::try_map but it's nightly-only <[_; 2]>::try_from( inputs .iter_mut() .flat_map(|input: &'_ mut _| input.peek()) // keep the Somes .collect::>(), // if we had 2 Somes we can make a [_; 2] from this ) } { let shorter_len = items.iter().map(|i| i.path.len()).min().expect("wrong #"); let earlier_i = items .iter() .enumerate() .min_by_key(|&(_i, item)| *item) .expect("wrong #") .0; let later_i = 1 - earlier_i; if items.iter().all_equal() { // Case 0. above. // // Take the identical items off the front of both iters, // and put one into the output (the last will do nicely). //dbg!(items); let item = inputs .iter_mut() .map(|input| input.next().expect("but peeked")) .last() .expect("wrong #"); output.insert(item); continue; } else if items .iter() .map(|item| &item.path[0..shorter_len]) .all_equal() { // Case 2. One is a prefix of the other. earlier_i is the shorter one. let shorter_item = items[earlier_i]; let prefix = shorter_item.path.clone(); // borrowck can't prove disjointness // Keep copying items from the side with the longer entries, // so long as they fall within (have the prefix of) the shorter entry. //dbg!(items, shorter_item, &prefix); while let Some(longer_item) = inputs[later_i].peek() { if !longer_item.path.starts_with(&prefix) { break; } let longer_item = inputs[later_i].next().expect("but peeked"); output.insert(longer_item); } // We've "used up" the shorter item. let _ = inputs[earlier_i].next().expect("but peeked"); } else { // Case 3. The items are just different. Eat the earlier one. //dbg!(items, earlier_i); let _ = inputs[earlier_i].next().expect("but peeked"); } } // Case 0. At least one of the lists is empty, giving Err() from the array //for oi in &ol { eprintln!("O: {}", oi); } output } impl Display for DisfavouredKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use PathEntry as PE; if self.path.is_empty() { // shouldn't happen with calls outside this module, and shouldn't be used inside // but handle it anyway write!(f, r#""""#)?; } else { let delims = chain!(iter::once(""), iter::repeat(".")); for (delim, ent) in izip!(delims, self.path.iter()) { match ent { PE::ArrayIndex(index) => write!(f, "[{}]", index)?, PE::MapEntry(s) => { if ok_unquoted(s) { write!(f, "{}{}", delim, s)?; } else { write!(f, "{}{:?}", delim, s)?; } } } } } Ok(()) } } /// Would `s` be OK to use unquoted as a key in a TOML file? fn ok_unquoted(s: &str) -> bool { let mut chars = s.chars(); if let Some(c) = chars.next() { c.is_ascii_alphanumeric() && chars.all(|c| c == '_' || c == '-' || c.is_ascii_alphanumeric()) } else { false } } #[cfg(test)] #[allow(unreachable_pub)] // impl_standard_builder wants to make pub fns mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_duration_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! use super::*; use crate::*; use derive_builder::Builder; use serde::{Deserialize, Serialize}; fn parse_test_set(l: &[&str]) -> BTreeSet { l.iter() .map(|s| DisfavouredKey { path: s .split('.') .map(|s| PathEntry::MapEntry(s.into())) .collect_vec(), }) .collect() } #[test] #[rustfmt::skip] // preserve the layout so we can match vertically by eye fn test_intersect_unrecognized_list() { let chk = |a, b, exp| { let got = intersect_unrecognized_lists(parse_test_set(a), parse_test_set(b)); let exp = parse_test_set(exp); assert_eq! { got, exp }; let got = intersect_unrecognized_lists(parse_test_set(b), parse_test_set(a)); assert_eq! { got, exp }; }; chk(&[ "a", "b", ], &[ "a", "c" ], &[ "a" ]); chk(&[ "a", "b", "d" ], &[ "a", "c", "d" ], &[ "a", "d" ]); chk(&[ "x.a", "x.b", ], &[ "x.a", "x.c" ], &[ "x.a" ]); chk(&[ "t", "u", "v", "w" ], &[ "t", "v.a", "v.b", "x" ], &[ "t", "v.a", "v.b", ]); chk(&[ "t", "v", "x" ], &[ "t", "u", "v.a", "v.b", "w" ], &[ "t", "v.a", "v.b", ]); } #[test] #[allow(clippy::bool_assert_comparison)] // much clearer this way IMO fn test_ok_unquoted() { assert_eq! { false, ok_unquoted("") }; assert_eq! { false, ok_unquoted("_") }; assert_eq! { false, ok_unquoted(".") }; assert_eq! { false, ok_unquoted("-") }; assert_eq! { false, ok_unquoted("_a") }; assert_eq! { false, ok_unquoted(".a") }; assert_eq! { false, ok_unquoted("-a") }; assert_eq! { false, ok_unquoted("a.") }; assert_eq! { true, ok_unquoted("a") }; assert_eq! { true, ok_unquoted("1") }; assert_eq! { true, ok_unquoted("z") }; assert_eq! { true, ok_unquoted("aa09_-") }; } #[test] fn test_display_key() { let chk = |exp, path: &[PathEntry]| { assert_eq! { DisfavouredKey { path: path.into() }.to_string(), exp }; }; let me = |s: &str| PathEntry::MapEntry(s.into()); use PathEntry::ArrayIndex as AI; chk(r#""""#, &[]); chk(r#""@""#, &[me("@")]); chk(r#""\\""#, &[me(r#"\"#)]); chk(r#"foo"#, &[me("foo")]); chk(r#"foo.bar"#, &[me("foo"), me("bar")]); chk(r#"foo[10]"#, &[me("foo"), AI(10)]); chk(r#"[10].bar"#, &[AI(10), me("bar")]); // weird } #[derive(Debug, Clone, Builder, Eq, PartialEq)] #[builder(build_fn(error = "ConfigBuildError"))] #[builder(derive(Debug, Serialize, Deserialize))] struct TestConfigA { #[builder(default)] a: String, } impl_standard_builder! { TestConfigA } impl TopLevel for TestConfigA { type Builder = TestConfigABuilder; } #[derive(Debug, Clone, Builder, Eq, PartialEq)] #[builder(build_fn(error = "ConfigBuildError"))] #[builder(derive(Debug, Serialize, Deserialize))] struct TestConfigB { #[builder(default)] b: String, #[builder(default)] old: bool, } impl_standard_builder! { TestConfigB } impl TopLevel for TestConfigB { type Builder = TestConfigBBuilder; const DEPRECATED_KEYS: &'static [&'static str] = &["old"]; } #[test] fn test_resolve() { let test_data = r#" wombat = 42 a = "hi" old = true "#; let cfg = { let mut sources = crate::ConfigurationSources::new_empty(); sources.push_source( crate::ConfigurationSource::from_verbatim(test_data.to_string()), crate::sources::MustRead::MustRead, ); sources.load().unwrap() }; let _: (TestConfigA, TestConfigB) = resolve_ignore_warnings(cfg.clone()).unwrap(); let resolved: ResolutionResults<(TestConfigA, TestConfigB)> = resolve_return_results(cfg).unwrap(); let (a, b) = resolved.value; let mk_strings = |l: Vec| l.into_iter().map(|ik| ik.to_string()).collect_vec(); let ign = mk_strings(resolved.unrecognized); let depr = mk_strings(resolved.deprecated); assert_eq! { &a, &TestConfigA { a: "hi".into() } }; assert_eq! { &b, &TestConfigB { b: "".into(), old: true } }; assert_eq! { ign, &["wombat"] }; assert_eq! { depr, &["old"] }; let _ = TestConfigA::builder(); let _ = TestConfigB::builder(); } #[derive(Debug, Clone, Builder, Eq, PartialEq)] #[builder(build_fn(error = "ConfigBuildError"))] #[builder(derive(Debug, Serialize, Deserialize))] struct TestConfigC { #[builder(default)] c: u32, } impl_standard_builder! { TestConfigC } impl TopLevel for TestConfigC { type Builder = TestConfigCBuilder; } #[test] fn build_error() { // Make sure that errors are propagated correctly. let test_data = r#" # wombat is not a number. c = "wombat" # this _would_ be unrecognized, but for the errors. persimmons = "sweet" "#; // suppress a dead-code warning. let _b = TestConfigC::builder(); let cfg = { let mut sources = crate::ConfigurationSources::new_empty(); sources.push_source( crate::ConfigurationSource::from_verbatim(test_data.to_string()), crate::sources::MustRead::MustRead, ); sources.load().unwrap() }; { // First try "A", then "C". let res1: Result, _> = resolve_return_results(cfg.clone()); assert!(res1.is_err()); assert!(matches!(res1, Err(ConfigResolveError::Deserialize(_)))); } { // Now the other order: first try "C", then "A". let res2: Result, _> = resolve_return_results(cfg.clone()); assert!(res2.is_err()); assert!(matches!(res2, Err(ConfigResolveError::Deserialize(_)))); } // Try manually, to make sure unrecognized fields are removed. let mut ctx = ResolveContext { input: cfg, unrecognized: UnrecognizedKeys::AllKeys, }; let _res3 = TestConfigA::resolve(&mut ctx); // After resolving A, some fields are unrecognized. assert!(matches!(&ctx.unrecognized, UnrecognizedKeys::These(k) if !k.is_empty())); { let res4 = TestConfigC::resolve(&mut ctx); assert!(matches!(res4, Err(ConfigResolveError::Deserialize(_)))); } { // After resolving C with an error, the unrecognized-field list is cleared. assert!(matches!(&ctx.unrecognized, UnrecognizedKeys::These(k) if k.is_empty())); } } }