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}