Skip to main content

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