tor_config/
load.rs

1//! Processing a `ConfigurationTree` into a validated configuration
2//!
3//! This module, and particularly [`resolve`], takes care of:
4//!
5//!   * Deserializing a [`ConfigurationTree`] into various `FooConfigBuilder`
6//!   * Calling the `build()` methods to get various `FooConfig`.
7//!   * Reporting unrecognised configuration keys
8//!     (eg to help the user detect misspellings).
9//!
10//! This is step 3 of the overall config processing,
11//! as described in the [crate-level documentation](crate).
12//!
13//! # Starting points
14//!
15//! To use this, you will need to:
16//!
17//!   * `#[derive(Builder)]` and use [`impl_standard_builder!`](crate::impl_standard_builder)
18//!     for all of your configuration structures,
19//!     using `#[sub_builder]` etc. sa appropriate,
20//!     and making your builders [`Deserialize`](serde::Deserialize).
21//!
22//!   * [`impl TopLevel`](TopLevel) for your *top level* structures (only).
23//!
24//!   * Call [`resolve`] (or one of its variants) with a `ConfigurationTree`,
25//!     to obtain your top-level configuration(s).
26//!
27//! # Example
28//!
29//! In this example the developers are embedding `arti`, `arti_client`, etc.,
30//! into a program of their own.  The example code shown:
31//!
32//!  * Defines a configuration structure `EmbedderConfig`,
33//!    for additional configuration settings for the added features.
34//!  * Establishes some configuration sources
35//!    (the trivial empty `ConfigSources`, to avoid clutter in the example)
36//!  * Reads those sources into a single configuration taxonomy [`ConfigurationTree`].
37//!  * Processes that configuration into a 3-tuple of configuration
38//!    structs for the three components, namely:
39//!      - `TorClientConfig`, the configuration for the `arti_client` crate's `TorClient`
40//!      - `ArtiConfig`, for behaviours in the `arti` command line utility
41//!      - `EmbedderConfig`.
42//!  * Will report a warning to the user about config settings found in the config files,
43//!    but not recognized by *any* of the three config consumers,
44//!
45//! ```
46//! # fn main() -> Result<(), tor_config::load::ConfigResolveError> {
47//! use derive_builder::Builder;
48//! use tor_config::{impl_standard_builder, resolve, ConfigBuildError, ConfigurationSources};
49//! use tor_config::load::TopLevel;
50//! use serde::{Deserialize, Serialize};
51//!
52//! #[derive(Debug, Clone, Builder, Eq, PartialEq)]
53//! #[builder(build_fn(error = "ConfigBuildError"))]
54//! #[builder(derive(Debug, Serialize, Deserialize))]
55//! struct EmbedderConfig {
56//!     // ....
57//! }
58//! impl_standard_builder! { EmbedderConfig }
59//! impl TopLevel for EmbedderConfig {
60//!     type Builder = EmbedderConfigBuilder;
61//! }
62//! #
63//! # #[derive(Debug, Clone, Builder, Eq, PartialEq)]
64//! # #[builder(build_fn(error = "ConfigBuildError"))]
65//! # #[builder(derive(Debug, Serialize, Deserialize))]
66//! # struct TorClientConfig { }
67//! # impl_standard_builder! { TorClientConfig }
68//! # impl TopLevel for TorClientConfig { type Builder = TorClientConfigBuilder; }
69//! #
70//! # #[derive(Debug, Clone, Builder, Eq, PartialEq)]
71//! # #[builder(build_fn(error = "ConfigBuildError"))]
72//! # #[builder(derive(Debug, Serialize, Deserialize))]
73//! # struct ArtiConfig { }
74//! # impl_standard_builder! { ArtiConfig }
75//! # impl TopLevel for ArtiConfig { type Builder = ArtiConfigBuilder; }
76//!
77//! let cfg_sources = ConfigurationSources::new_empty(); // In real program, use from_cmdline
78//! let cfg = cfg_sources.load()?;
79//!
80//! let (tcc, arti_config, embedder_config) =
81//!      tor_config::resolve::<(TorClientConfig, ArtiConfig, EmbedderConfig)>(cfg)?;
82//!
83//! let _: EmbedderConfig = embedder_config; // etc.
84//!
85//! # Ok(())
86//! # }
87//! ```
88
89use std::collections::BTreeSet;
90use std::fmt::{self, Display};
91use std::iter;
92use std::mem;
93
94use itertools::{chain, izip, Itertools};
95use serde::de::DeserializeOwned;
96use thiserror::Error;
97use tracing::warn;
98
99use crate::{ConfigBuildError, ConfigurationTree};
100
101/// Error resolving a configuration (during deserialize, or build)
102#[derive(Error, Debug)]
103#[non_exhaustive]
104pub enum ConfigResolveError {
105    /// Deserialize failed
106    #[error("Config contents not as expected")]
107    Deserialize(#[from] crate::ConfigError),
108
109    /// Build failed
110    #[error("Config semantically incorrect")]
111    Build(#[from] ConfigBuildError),
112}
113
114/// A type that can be built from a builder via a build method
115pub trait Builder {
116    /// The type that this builder constructs
117    type Built;
118    /// Build into a `Built`
119    ///
120    /// Often shadows an inherent `build` method
121    fn build(&self) -> Result<Self::Built, ConfigBuildError>;
122}
123
124/// Collection of configuration settings that can be deserialized and then built
125///
126/// *Do not implement directly.*
127/// Instead, implement [`TopLevel`]: doing so engages the blanket impl
128/// for (loosely) `TopLevel + Builder`.
129///
130/// Each `Resolvable` corresponds to one or more configuration consumers.
131///
132/// Ultimately, one `Resolvable` for all the configuration consumers in an entire
133/// program will be resolved from a single configuration tree (usually parsed from TOML).
134///
135/// Multiple config collections can be resolved from the same configuration,
136/// via the implementation of `Resolvable` on tuples of `Resolvable`s.
137/// Use this rather than `#[serde(flatten)]`; the latter prevents useful introspection
138/// (necessary for reporting unrecognized configuration keys, and testing).
139///
140/// (The `resolve` method will be called only from within the `tor_config::load` module.)
141pub trait Resolvable: Sized {
142    /// Deserialize and build from a configuration
143    //
144    // Implementations must do the following:
145    //
146    //  1. Deserializes the input (cloning it to be able to do this)
147    //     into the `Builder`.
148    //
149    //  2. Having used `serde_ignored` to detect unrecognized keys,
150    //     intersects those with the unrecognized keys recorded in the context.
151    //
152    //  3. Calls `build` on the `Builder` to get `Self`.
153    //
154    // We provide impls for TopLevels, and tuples of Resolvable.
155    //
156    // Cannot be implemented outside this module (except eg as a wrapper or something),
157    // because that would somehow involve creating `Self` from `ResolveContext`
158    // but `ResolveContext` is completely opaque outside this module.
159    fn resolve(input: &mut ResolveContext) -> Result<Self, ConfigResolveError>;
160
161    /// Return a list of deprecated config keys, as "."-separated strings
162    fn enumerate_deprecated_keys<F>(f: &mut F)
163    where
164        F: FnMut(&'static [&'static str]);
165}
166
167/// Top-level configuration struct, made from a deserializable builder
168///
169/// One configuration consumer's configuration settings.
170///
171/// Implementing this trait only for top-level configurations,
172/// which are to be parsed at the root level of a (TOML) config file taxonomy.
173///
174/// This trait exists to:
175///
176///  * Mark the toplevel configuration structures as suitable for use with [`resolve`]
177///  * Provide the type of the `Builder` for use by Rust generic code
178pub trait TopLevel {
179    /// The `Builder` which can be used to make a `Self`
180    ///
181    /// Should satisfy `&'_ Self::Builder: Builder<Built=Self>`
182    type Builder: DeserializeOwned;
183
184    /// Deprecated config keys, as "."-separates strings
185    const DEPRECATED_KEYS: &'static [&'static str] = &[];
186}
187
188/// `impl Resolvable for (A,B..) where A: Resolvable, B: Resolvable ...`
189///
190/// The implementation simply calls `Resolvable::resolve` for each output tuple member.
191///
192/// `define_for_tuples!{ A B - C D.. }`
193///
194/// expands to
195///  1. `define_for_tuples!{ A B - }`: defines for tuple `(A,B,)`
196///  2. `define_for_tuples!{ A B C - D.. }`: recurses to generate longer tuples
197macro_rules! define_for_tuples {
198    { $( $A:ident )* - $B:ident $( $C:ident )* } => {
199        define_for_tuples!{ $($A)* - }
200        define_for_tuples!{ $($A)* $B - $($C)* }
201    };
202    { $( $A:ident )* - } => {
203        impl < $($A,)* > Resolvable for ( $($A,)* )
204        where $( $A: Resolvable, )*
205        {
206            fn resolve(cfg: &mut ResolveContext) -> Result<Self, ConfigResolveError> {
207                Ok(( $( $A::resolve(cfg)?, )* ))
208            }
209            fn enumerate_deprecated_keys<NF>(f: &mut NF)
210            where NF: FnMut(&'static [&'static str]) {
211                $( $A::enumerate_deprecated_keys(f); )*
212            }
213        }
214
215    };
216}
217// We could avoid recursion by writing out A B C... several times (in a "triangle") but this
218// would make it tiresome and error-prone to extend the impl to longer tuples.
219define_for_tuples! { A - B C D E }
220
221/// Config resolution context, not used outside `tor_config::load`
222///
223/// This is public only because it appears in the [`Resolvable`] trait.
224/// You don't want to try to obtain one.
225pub struct ResolveContext {
226    /// The input
227    input: ConfigurationTree,
228
229    /// Paths unrecognized by all deserializations
230    ///
231    /// None means we haven't deserialized anything yet, ie means the universal set.
232    ///
233    /// Empty is used to disable this feature.
234    unrecognized: UnrecognizedKeys,
235}
236
237/// Keys we have *not* recognized so far
238///
239/// Initially `AllKeys`, since we haven't recognized any.
240#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
241enum UnrecognizedKeys {
242    /// No keys have yet been recognized, so everything in the config is unrecognized
243    AllKeys,
244
245    /// The keys which remain unrecognized by any consumer
246    ///
247    /// If this is empty, we do not (need to) do any further tracking.
248    These(BTreeSet<DisfavouredKey>),
249}
250use UnrecognizedKeys as UK;
251
252impl UnrecognizedKeys {
253    /// Does it represent the empty set
254    fn is_empty(&self) -> bool {
255        match self {
256            UK::AllKeys => false,
257            UK::These(ign) => ign.is_empty(),
258        }
259    }
260
261    /// Update in place, intersecting with `other`
262    fn intersect_with(&mut self, other: BTreeSet<DisfavouredKey>) {
263        match self {
264            UK::AllKeys => *self = UK::These(other),
265            UK::These(self_) => {
266                let tign = mem::take(self_);
267                *self_ = intersect_unrecognized_lists(tign, other);
268            }
269        }
270    }
271
272    /// Remove every element of this set.
273    fn clear(&mut self) {
274        *self = UK::These(BTreeSet::new());
275    }
276}
277
278/// Key in config file(s) which is disfavoured (unrecognized or deprecated)
279///
280/// [`Display`]s in an approximation to TOML format.
281/// You can use the [`to_string()`](ToString::to_string) method to obtain
282/// a string containing a TOML key path.
283#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
284pub struct DisfavouredKey {
285    /// Can be empty only before returned from this module
286    path: Vec<PathEntry>,
287}
288
289/// Element of an DisfavouredKey
290#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
291enum PathEntry {
292    /// Array index
293    ///
294    ArrayIndex(usize),
295    /// Map entry
296    ///
297    /// string value is unquoted, needs quoting for display
298    MapEntry(String),
299}
300
301/// Deserialize and build overall configuration from config sources
302///
303/// Inner function used by all the `resolve_*` family
304fn resolve_inner<T>(
305    input: ConfigurationTree,
306    want_disfavoured: bool,
307) -> Result<ResolutionResults<T>, ConfigResolveError>
308where
309    T: Resolvable,
310{
311    let mut deprecated = BTreeSet::new();
312
313    if want_disfavoured {
314        T::enumerate_deprecated_keys(&mut |l: &[&str]| {
315            for key in l {
316                match input.0.find_value(key) {
317                    Err(_) => {}
318                    Ok(_) => {
319                        deprecated.insert(key);
320                    }
321                }
322            }
323        });
324    }
325
326    let mut lc = ResolveContext {
327        input,
328        unrecognized: if want_disfavoured {
329            UK::AllKeys
330        } else {
331            UK::These(BTreeSet::new())
332        },
333    };
334
335    let value = Resolvable::resolve(&mut lc)?;
336
337    let unrecognized = match lc.unrecognized {
338        UK::AllKeys => panic!("all unrecognized, as if we had processed nothing"),
339        UK::These(ign) => ign,
340    }
341    .into_iter()
342    .filter(|ip| !ip.path.is_empty())
343    .collect_vec();
344
345    let deprecated = deprecated
346        .into_iter()
347        .map(|key| {
348            let path = key
349                .split('.')
350                .map(|e| PathEntry::MapEntry(e.into()))
351                .collect_vec();
352            DisfavouredKey { path }
353        })
354        .collect_vec();
355
356    Ok(ResolutionResults {
357        value,
358        unrecognized,
359        deprecated,
360    })
361}
362
363/// Deserialize and build overall configuration from config sources
364///
365/// Unrecognized config keys are reported as log warning messages.
366///
367/// Resolve the whole configuration in one go, using the `Resolvable` impl on `(A,B)`
368/// if necessary, so that unrecognized config key processing works correctly.
369///
370/// This performs step 3 of the overall config processing,
371/// as described in the [`tor_config` crate-level documentation](crate).
372///
373/// For an example, see the
374/// [`tor_config::load` module-level documentation](self).
375pub fn resolve<T>(input: ConfigurationTree) -> Result<T, ConfigResolveError>
376where
377    T: Resolvable,
378{
379    let ResolutionResults {
380        value,
381        unrecognized,
382        deprecated,
383    } = resolve_inner(input, true)?;
384    for depr in deprecated {
385        warn!("deprecated configuration key: {}", &depr);
386    }
387    for ign in unrecognized {
388        warn!("unrecognized configuration key: {}", &ign);
389    }
390    Ok(value)
391}
392
393/// Deserialize and build overall configuration, reporting unrecognized keys in the return value
394pub fn resolve_return_results<T>(
395    input: ConfigurationTree,
396) -> Result<ResolutionResults<T>, ConfigResolveError>
397where
398    T: Resolvable,
399{
400    resolve_inner(input, true)
401}
402
403/// Results of a successful `resolve_return_disfavoured`
404#[derive(Debug, Clone)]
405#[non_exhaustive]
406pub struct ResolutionResults<T> {
407    /// The configuration, successfully parsed
408    pub value: T,
409
410    /// Any config keys which were found in the input, but not recognized (and so, ignored)
411    pub unrecognized: Vec<DisfavouredKey>,
412
413    /// Any config keys which were found, but have been declared deprecated
414    pub deprecated: Vec<DisfavouredKey>,
415}
416
417/// Deserialize and build overall configuration, silently ignoring unrecognized config keys
418pub fn resolve_ignore_warnings<T>(input: ConfigurationTree) -> Result<T, ConfigResolveError>
419where
420    T: Resolvable,
421{
422    Ok(resolve_inner(input, false)?.value)
423}
424
425/// Wrapper around T that collects ignored keys as we deserialize it.
426///
427/// (We need a helper type here since figment does not expose a `Deserializer`
428/// implementation directly.)
429struct Des<T> {
430    /// A set of the ignored keys that we found
431    nign: BTreeSet<DisfavouredKey>,
432    /// The underlying value we're deserializing.
433    value: T,
434}
435impl<'de, T> serde::Deserialize<'de> for Des<T>
436where
437    T: serde::Deserialize<'de>,
438{
439    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
440    where
441        D: serde::Deserializer<'de>,
442    {
443        let mut nign = BTreeSet::new();
444        let mut recorder = |path: serde_ignored::Path<'_>| {
445            nign.insert(copy_path(&path));
446        };
447        let deser = serde_ignored::Deserializer::new(deserializer, &mut recorder);
448        let ret = serde::Deserialize::deserialize(deser);
449        Ok(Des { nign, value: ret? })
450    }
451}
452
453impl<T> Resolvable for T
454where
455    T: TopLevel,
456    T::Builder: Builder<Built = Self>,
457{
458    fn resolve(input: &mut ResolveContext) -> Result<T, ConfigResolveError> {
459        let deser = input.input.clone();
460        let builder: Result<T::Builder, _> = {
461            // If input.unrecognized.is_empty() then we don't bother tracking the
462            // unrecognized keys since we would intersect with the empty set.
463            // That is how this tracking is disabled when we want it to be.
464            let want_unrecognized = !input.unrecognized.is_empty();
465            if !want_unrecognized {
466                deser.0.extract_lossy()
467            } else {
468                let ret: Result<Des<<T as TopLevel>::Builder>, _> = deser.0.extract_lossy();
469
470                match ret {
471                    Ok(Des { nign, value }) => {
472                        input.unrecognized.intersect_with(nign);
473                        Ok(value)
474                    }
475                    Err(e) => {
476                        // If we got an error, the config might only have been partially processed,
477                        // so we might get false positives.  Disable the unrecognized tracking.
478                        input.unrecognized.clear();
479                        Err(e)
480                    }
481                }
482            }
483        };
484        let built = builder.map_err(crate::ConfigError::from_cfg_err)?.build()?;
485        Ok(built)
486    }
487
488    fn enumerate_deprecated_keys<NF>(f: &mut NF)
489    where
490        NF: FnMut(&'static [&'static str]),
491    {
492        f(T::DEPRECATED_KEYS);
493    }
494}
495
496/// Turns a [`serde_ignored::Path`] (which is borrowed) into an owned `DisfavouredKey`
497fn copy_path(mut path: &serde_ignored::Path) -> DisfavouredKey {
498    use serde_ignored::Path as SiP;
499    use PathEntry as PE;
500
501    let mut descend = vec![];
502    loop {
503        let (new_path, ent) = match path {
504            SiP::Root => break,
505            SiP::Seq { parent, index } => (parent, Some(PE::ArrayIndex(*index))),
506            SiP::Map { parent, key } => (parent, Some(PE::MapEntry(key.clone()))),
507            SiP::Some { parent }
508            | SiP::NewtypeStruct { parent }
509            | SiP::NewtypeVariant { parent } => (parent, None),
510        };
511        descend.extend(ent);
512        path = new_path;
513    }
514    descend.reverse();
515    DisfavouredKey { path: descend }
516}
517
518/// Computes the intersection, resolving ignorances at different depths
519///
520/// Eg if `a` contains `application.wombat` and `b` contains `application`,
521/// we need to return `application.wombat`.
522///
523/// # Formally
524///
525/// A configuration key (henceforth "key") is a sequence of `PathEntry`,
526/// interpreted as denoting a place in a tree-like hierarchy.
527///
528/// Each input `BTreeSet` denotes a subset of the configuration key space.
529/// Any key in the set denotes itself, but also all possible keys which have it as a prefix.
530/// We say a s set is "minimal" if it doesn't have entries made redundant by this rule.
531///
532/// This function computes a minimal intersection of two minimal inputs.
533/// If the inputs are not minimal, the output may not be either
534/// (although `serde_ignored` gives us minimal sets, so that case is not important).
535fn intersect_unrecognized_lists(
536    al: BTreeSet<DisfavouredKey>,
537    bl: BTreeSet<DisfavouredKey>,
538) -> BTreeSet<DisfavouredKey> {
539    //eprintln!("INTERSECT:");
540    //for ai in &al { eprintln!("A: {}", ai); }
541    //for bi in &bl { eprintln!("B: {}", bi); }
542
543    // This function is written to never talk about "a" and "b".
544    // That (i) avoids duplication of code for handling a<b vs a>b, etc.
545    // (ii) make impossible bugs where a was written but b was intended, etc.
546    // The price is that the result is iterator combinator soup.
547
548    let mut inputs: [_; 2] = [al, bl].map(|input| input.into_iter().peekable());
549    let mut output = BTreeSet::new();
550
551    // The BTreeSets produce items in sort order.
552    //
553    // We maintain the following invariants (valid at the top of the loop):
554    //
555    //   For every possible key *strictly earlier* than those remaining in either input,
556    //   the output contains the key iff it was in the intersection.
557    //
558    //   No other keys appear in the output.
559    //
560    // We peek at the next two items.  The possible cases are:
561    //
562    //   0. One or both inputs is used up.  In that case none of any remaining input
563    //      can be in the intersection and we are done.
564    //
565    //   1. The two inputs have the same next item.  In that case the item is in the
566    //      intersection.  If the inputs are minimal, no children of that item can appear
567    //      in either input, so we can make our own output minimal without thinking any
568    //      more about this item from the point of view of either list.
569    //
570    //   2. One of the inputs is a prefix of the other.  In this case the longer item is
571    //      in the intersection - as are all subsequent items from the same input which
572    //      also share that prefix.  Then, we must discard the shorter item (which denotes
573    //      the whole subspace of which only part is in the intersection).
574    //
575    //   3. Otherwise, the earlier item is definitely not in the intersection and
576    //      we can munch it.
577
578    // Peek one from each, while we can.
579    while let Ok(items) = {
580        // Ideally we would use array::try_map but it's nightly-only
581        <[_; 2]>::try_from(
582            inputs
583                .iter_mut()
584                .flat_map(|input: &'_ mut _| input.peek()) // keep the Somes
585                .collect::<Vec<_>>(), // if we had 2 Somes we can make a [_; 2] from this
586        )
587    } {
588        let shorter_len = items.iter().map(|i| i.path.len()).min().expect("wrong #");
589        let earlier_i = items
590            .iter()
591            .enumerate()
592            .min_by_key(|&(_i, item)| *item)
593            .expect("wrong #")
594            .0;
595        let later_i = 1 - earlier_i;
596
597        if items.iter().all_equal() {
598            // Case 0. above.
599            //
600            // Take the identical items off the front of both iters,
601            // and put one into the output (the last will do nicely).
602            //dbg!(items);
603            let item = inputs
604                .iter_mut()
605                .map(|input| input.next().expect("but peeked"))
606                .next_back()
607                .expect("wrong #");
608            output.insert(item);
609            continue;
610        } else if items
611            .iter()
612            .map(|item| &item.path[0..shorter_len])
613            .all_equal()
614        {
615            // Case 2.  One is a prefix of the other.   earlier_i is the shorter one.
616            let shorter_item = items[earlier_i];
617            let prefix = shorter_item.path.clone(); // borrowck can't prove disjointness
618
619            // Keep copying items from the side with the longer entries,
620            // so long as they fall within (have the prefix of) the shorter entry.
621            //dbg!(items, shorter_item, &prefix);
622            while let Some(longer_item) = inputs[later_i].peek() {
623                if !longer_item.path.starts_with(&prefix) {
624                    break;
625                }
626                let longer_item = inputs[later_i].next().expect("but peeked");
627                output.insert(longer_item);
628            }
629            // We've "used up" the shorter item.
630            let _ = inputs[earlier_i].next().expect("but peeked");
631        } else {
632            // Case 3.  The items are just different.  Eat the earlier one.
633            //dbg!(items, earlier_i);
634            let _ = inputs[earlier_i].next().expect("but peeked");
635        }
636    }
637    // Case 0.  At least one of the lists is empty, giving Err() from the array
638
639    //for oi in &ol { eprintln!("O: {}", oi); }
640    output
641}
642
643impl Display for DisfavouredKey {
644    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
645        use PathEntry as PE;
646        if self.path.is_empty() {
647            // shouldn't happen with calls outside this module, and shouldn't be used inside
648            // but handle it anyway
649            write!(f, r#""""#)?;
650        } else {
651            let delims = chain!(iter::once(""), iter::repeat("."));
652            for (delim, ent) in izip!(delims, self.path.iter()) {
653                match ent {
654                    PE::ArrayIndex(index) => write!(f, "[{}]", index)?,
655                    PE::MapEntry(s) => {
656                        if ok_unquoted(s) {
657                            write!(f, "{}{}", delim, s)?;
658                        } else {
659                            write!(f, "{}{:?}", delim, s)?;
660                        }
661                    }
662                }
663            }
664        }
665        Ok(())
666    }
667}
668
669/// Would `s` be OK to use unquoted as a key in a TOML file?
670fn ok_unquoted(s: &str) -> bool {
671    let mut chars = s.chars();
672    if let Some(c) = chars.next() {
673        c.is_ascii_alphanumeric()
674            && chars.all(|c| c == '_' || c == '-' || c.is_ascii_alphanumeric())
675    } else {
676        false
677    }
678}
679
680#[cfg(test)]
681#[allow(unreachable_pub)] // impl_standard_builder wants to make pub fns
682mod test {
683    // @@ begin test lint list maintained by maint/add_warning @@
684    #![allow(clippy::bool_assert_comparison)]
685    #![allow(clippy::clone_on_copy)]
686    #![allow(clippy::dbg_macro)]
687    #![allow(clippy::mixed_attributes_style)]
688    #![allow(clippy::print_stderr)]
689    #![allow(clippy::print_stdout)]
690    #![allow(clippy::single_char_pattern)]
691    #![allow(clippy::unwrap_used)]
692    #![allow(clippy::unchecked_duration_subtraction)]
693    #![allow(clippy::useless_vec)]
694    #![allow(clippy::needless_pass_by_value)]
695    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
696    use super::*;
697    use crate::*;
698    use derive_builder::Builder;
699    use serde::{Deserialize, Serialize};
700
701    fn parse_test_set(l: &[&str]) -> BTreeSet<DisfavouredKey> {
702        l.iter()
703            .map(|s| DisfavouredKey {
704                path: s
705                    .split('.')
706                    .map(|s| PathEntry::MapEntry(s.into()))
707                    .collect_vec(),
708            })
709            .collect()
710    }
711
712    #[test]
713    #[rustfmt::skip] // preserve the layout so we can match vertically by eye
714    fn test_intersect_unrecognized_list() {
715        let chk = |a, b, exp| {
716            let got = intersect_unrecognized_lists(parse_test_set(a), parse_test_set(b));
717            let exp = parse_test_set(exp);
718            assert_eq! { got, exp };
719
720            let got = intersect_unrecognized_lists(parse_test_set(b), parse_test_set(a));
721            assert_eq! { got, exp };
722        };
723
724        chk(&[ "a", "b",     ],
725            &[ "a",      "c" ],
726            &[ "a" ]);
727
728        chk(&[ "a", "b",      "d" ],
729            &[ "a",      "c", "d" ],
730            &[ "a",           "d" ]);
731
732        chk(&[ "x.a", "x.b",     ],
733            &[ "x.a",      "x.c" ],
734            &[ "x.a" ]);
735
736        chk(&[ "t", "u", "v",          "w"     ],
737            &[ "t",      "v.a", "v.b",     "x" ],
738            &[ "t",      "v.a", "v.b",         ]);
739
740        chk(&[ "t",      "v",              "x" ],
741            &[ "t", "u", "v.a", "v.b", "w"     ],
742            &[ "t",      "v.a", "v.b",         ]);
743    }
744
745    #[test]
746    #[allow(clippy::bool_assert_comparison)] // much clearer this way IMO
747    fn test_ok_unquoted() {
748        assert_eq! { false, ok_unquoted("") };
749        assert_eq! { false, ok_unquoted("_") };
750        assert_eq! { false, ok_unquoted(".") };
751        assert_eq! { false, ok_unquoted("-") };
752        assert_eq! { false, ok_unquoted("_a") };
753        assert_eq! { false, ok_unquoted(".a") };
754        assert_eq! { false, ok_unquoted("-a") };
755        assert_eq! { false, ok_unquoted("a.") };
756        assert_eq! { true, ok_unquoted("a") };
757        assert_eq! { true, ok_unquoted("1") };
758        assert_eq! { true, ok_unquoted("z") };
759        assert_eq! { true, ok_unquoted("aa09_-") };
760    }
761
762    #[test]
763    fn test_display_key() {
764        let chk = |exp, path: &[PathEntry]| {
765            assert_eq! { DisfavouredKey { path: path.into() }.to_string(), exp };
766        };
767        let me = |s: &str| PathEntry::MapEntry(s.into());
768        use PathEntry::ArrayIndex as AI;
769
770        chk(r#""""#, &[]);
771        chk(r#""@""#, &[me("@")]);
772        chk(r#""\\""#, &[me(r#"\"#)]);
773        chk(r#"foo"#, &[me("foo")]);
774        chk(r#"foo.bar"#, &[me("foo"), me("bar")]);
775        chk(r#"foo[10]"#, &[me("foo"), AI(10)]);
776        chk(r#"[10].bar"#, &[AI(10), me("bar")]); // weird
777    }
778
779    #[derive(Debug, Clone, Builder, Eq, PartialEq)]
780    #[builder(build_fn(error = "ConfigBuildError"))]
781    #[builder(derive(Debug, Serialize, Deserialize))]
782    struct TestConfigA {
783        #[builder(default)]
784        a: String,
785    }
786    impl_standard_builder! { TestConfigA }
787    impl TopLevel for TestConfigA {
788        type Builder = TestConfigABuilder;
789    }
790
791    #[derive(Debug, Clone, Builder, Eq, PartialEq)]
792    #[builder(build_fn(error = "ConfigBuildError"))]
793    #[builder(derive(Debug, Serialize, Deserialize))]
794    struct TestConfigB {
795        #[builder(default)]
796        b: String,
797
798        #[builder(default)]
799        old: bool,
800    }
801    impl_standard_builder! { TestConfigB }
802    impl TopLevel for TestConfigB {
803        type Builder = TestConfigBBuilder;
804        const DEPRECATED_KEYS: &'static [&'static str] = &["old"];
805    }
806
807    #[test]
808    fn test_resolve() {
809        let test_data = r#"
810            wombat = 42
811            a = "hi"
812            old = true
813        "#;
814        let cfg = {
815            let mut sources = crate::ConfigurationSources::new_empty();
816            sources.push_source(
817                crate::ConfigurationSource::from_verbatim(test_data.to_string()),
818                crate::sources::MustRead::MustRead,
819            );
820            sources.load().unwrap()
821        };
822
823        let _: (TestConfigA, TestConfigB) = resolve_ignore_warnings(cfg.clone()).unwrap();
824
825        let resolved: ResolutionResults<(TestConfigA, TestConfigB)> =
826            resolve_return_results(cfg).unwrap();
827        let (a, b) = resolved.value;
828
829        let mk_strings =
830            |l: Vec<DisfavouredKey>| l.into_iter().map(|ik| ik.to_string()).collect_vec();
831
832        let ign = mk_strings(resolved.unrecognized);
833        let depr = mk_strings(resolved.deprecated);
834
835        assert_eq! { &a, &TestConfigA { a: "hi".into() } };
836        assert_eq! { &b, &TestConfigB { b: "".into(), old: true } };
837        assert_eq! { ign, &["wombat"] };
838        assert_eq! { depr, &["old"] };
839
840        let _ = TestConfigA::builder();
841        let _ = TestConfigB::builder();
842    }
843
844    #[derive(Debug, Clone, Builder, Eq, PartialEq)]
845    #[builder(build_fn(error = "ConfigBuildError"))]
846    #[builder(derive(Debug, Serialize, Deserialize))]
847    struct TestConfigC {
848        #[builder(default)]
849        c: u32,
850    }
851    impl_standard_builder! { TestConfigC }
852    impl TopLevel for TestConfigC {
853        type Builder = TestConfigCBuilder;
854    }
855
856    #[test]
857    fn build_error() {
858        // Make sure that errors are propagated correctly.
859        let test_data = r#"
860            # wombat is not a number.
861            c = "wombat"
862            # this _would_ be unrecognized, but for the errors.
863            persimmons = "sweet"
864        "#;
865        // suppress a dead-code warning.
866        let _b = TestConfigC::builder();
867
868        let cfg = {
869            let mut sources = crate::ConfigurationSources::new_empty();
870            sources.push_source(
871                crate::ConfigurationSource::from_verbatim(test_data.to_string()),
872                crate::sources::MustRead::MustRead,
873            );
874            sources.load().unwrap()
875        };
876
877        {
878            // First try "A", then "C".
879            let res1: Result<ResolutionResults<(TestConfigA, TestConfigC)>, _> =
880                resolve_return_results(cfg.clone());
881            assert!(res1.is_err());
882            assert!(matches!(res1, Err(ConfigResolveError::Deserialize(_))));
883        }
884        {
885            // Now the other order: first try "C", then "A".
886            let res2: Result<ResolutionResults<(TestConfigC, TestConfigA)>, _> =
887                resolve_return_results(cfg.clone());
888            assert!(res2.is_err());
889            assert!(matches!(res2, Err(ConfigResolveError::Deserialize(_))));
890        }
891        // Try manually, to make sure unrecognized fields are removed.
892        let mut ctx = ResolveContext {
893            input: cfg,
894            unrecognized: UnrecognizedKeys::AllKeys,
895        };
896        let _res3 = TestConfigA::resolve(&mut ctx);
897        // After resolving A, some fields are unrecognized.
898        assert!(matches!(&ctx.unrecognized, UnrecognizedKeys::These(k) if !k.is_empty()));
899        {
900            let res4 = TestConfigC::resolve(&mut ctx);
901            assert!(matches!(res4, Err(ConfigResolveError::Deserialize(_))));
902        }
903        {
904            // After resolving C with an error, the unrecognized-field list is cleared.
905            assert!(matches!(&ctx.unrecognized, UnrecognizedKeys::These(k) if k.is_empty()));
906        }
907    }
908}