use std::collections::BTreeSet;
use std::fmt::{self, Display};
use std::iter;
use std::mem;
use itertools::{chain, izip, Itertools};
use serde::de::DeserializeOwned;
use thiserror::Error;
use tracing::warn;
use crate::ConfigBuildError;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ConfigResolveError {
#[error("Config contents not as expected")]
Deserialize(#[from] crate::ConfigError),
#[error("Config semantically incorrect")]
Build(#[from] ConfigBuildError),
}
impl From<config::ConfigError> for ConfigResolveError {
fn from(err: config::ConfigError) -> Self {
crate::ConfigError::from(err).into()
}
}
pub trait Builder {
type Built;
fn build(&self) -> Result<Self::Built, ConfigBuildError>;
}
pub trait Resolvable: Sized {
fn resolve(input: &mut ResolveContext) -> Result<Self, ConfigResolveError>;
fn enumerate_deprecated_keys<F>(f: &mut F)
where
F: FnMut(&'static [&'static str]);
}
pub trait TopLevel {
type Builder: DeserializeOwned;
const DEPRECATED_KEYS: &'static [&'static str] = &[];
}
macro_rules! define_for_tuples {
{ $( $A:ident )* - $B:ident $( $C:ident )* } => {
define_for_tuples!{ $($A)* - }
define_for_tuples!{ $($A)* $B - $($C)* }
};
{ $( $A:ident )* - } => {
impl < $($A,)* > Resolvable for ( $($A,)* )
where $( $A: Resolvable, )*
{
fn resolve(cfg: &mut ResolveContext) -> Result<Self, ConfigResolveError> {
Ok(( $( $A::resolve(cfg)?, )* ))
}
fn enumerate_deprecated_keys<NF>(f: &mut NF)
where NF: FnMut(&'static [&'static str]) {
$( $A::enumerate_deprecated_keys(f); )*
}
}
};
}
define_for_tuples! { A - B C D E }
pub struct ResolveContext {
input: config::Config,
unrecognized: UnrecognizedKeys,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
enum UnrecognizedKeys {
AllKeys,
These(BTreeSet<DisfavouredKey>),
}
use UnrecognizedKeys as UK;
impl UnrecognizedKeys {
fn is_empty(&self) -> bool {
match self {
UK::AllKeys => false,
UK::These(ign) => ign.is_empty(),
}
}
fn intersect_with(&mut self, other: BTreeSet<DisfavouredKey>) {
match self {
UK::AllKeys => *self = UK::These(other),
UK::These(self_) => {
let tign = mem::take(self_);
*self_ = intersect_unrecognized_lists(tign, other);
}
}
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct DisfavouredKey {
path: Vec<PathEntry>,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
enum PathEntry {
ArrayIndex(usize),
MapEntry(String),
}
fn resolve_inner<T>(
input: config::Config,
want_disfavoured: bool,
) -> Result<ResolutionResults<T>, ConfigResolveError>
where
T: Resolvable,
{
let mut deprecated = BTreeSet::new();
if want_disfavoured {
T::enumerate_deprecated_keys(&mut |l: &[&str]| {
for key in l {
match input.get(key) {
Err(_) => {}
Ok(serde::de::IgnoredAny) => {
deprecated.insert(key);
}
}
}
});
}
let mut lc = ResolveContext {
input,
unrecognized: if want_disfavoured {
UK::AllKeys
} else {
UK::These(BTreeSet::new())
},
};
let value = Resolvable::resolve(&mut lc)?;
let unrecognized = match lc.unrecognized {
UK::AllKeys => panic!("all unrecognized, as if we had processed nothing"),
UK::These(ign) => ign,
}
.into_iter()
.filter(|ip| !ip.path.is_empty())
.collect_vec();
let deprecated = deprecated
.into_iter()
.map(|key| {
let path = key
.split('.')
.map(|e| PathEntry::MapEntry(e.into()))
.collect_vec();
DisfavouredKey { path }
})
.collect_vec();
Ok(ResolutionResults {
value,
unrecognized,
deprecated,
})
}
pub fn resolve<T>(input: config::Config) -> Result<T, ConfigResolveError>
where
T: Resolvable,
{
let ResolutionResults {
value,
unrecognized,
deprecated,
} = resolve_inner(input, true)?;
for depr in deprecated {
warn!("deprecated configuration key: {}", &depr);
}
for ign in unrecognized {
warn!("unrecognized configuration key: {}", &ign);
}
Ok(value)
}
pub fn resolve_return_results<T>(
input: config::Config,
) -> Result<ResolutionResults<T>, ConfigResolveError>
where
T: Resolvable,
{
resolve_inner(input, true)
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ResolutionResults<T> {
pub value: T,
pub unrecognized: Vec<DisfavouredKey>,
pub deprecated: Vec<DisfavouredKey>,
}
pub fn resolve_ignore_warnings<T>(input: config::Config) -> Result<T, ConfigResolveError>
where
T: Resolvable,
{
Ok(resolve_inner(input, false)?.value)
}
impl<T> Resolvable for T
where
T: TopLevel,
T::Builder: Builder<Built = Self>,
{
fn resolve(input: &mut ResolveContext) -> Result<T, ConfigResolveError> {
let deser = input.input.clone();
let builder: T::Builder = {
let want_unrecognized = !input.unrecognized.is_empty();
let ret = if !want_unrecognized {
deser.try_deserialize()
} else {
let mut nign = BTreeSet::new();
let mut recorder = |path: serde_ignored::Path<'_>| {
nign.insert(copy_path(&path));
};
let deser = serde_ignored::Deserializer::new(deser, &mut recorder);
let ret = serde::Deserialize::deserialize(deser);
if ret.is_err() {
nign = BTreeSet::new();
}
input.unrecognized.intersect_with(nign);
ret
};
ret?
};
let built = builder.build()?;
Ok(built)
}
fn enumerate_deprecated_keys<NF>(f: &mut NF)
where
NF: FnMut(&'static [&'static str]),
{
f(T::DEPRECATED_KEYS);
}
}
fn copy_path(mut path: &serde_ignored::Path) -> DisfavouredKey {
use serde_ignored::Path as SiP;
use PathEntry as PE;
let mut descend = vec![];
loop {
let (new_path, ent) = match path {
SiP::Root => break,
SiP::Seq { parent, index } => (parent, Some(PE::ArrayIndex(*index))),
SiP::Map { parent, key } => (parent, Some(PE::MapEntry(key.clone()))),
SiP::Some { parent }
| SiP::NewtypeStruct { parent }
| SiP::NewtypeVariant { parent } => (parent, None),
};
descend.extend(ent);
path = new_path;
}
descend.reverse();
DisfavouredKey { path: descend }
}
fn intersect_unrecognized_lists(
al: BTreeSet<DisfavouredKey>,
bl: BTreeSet<DisfavouredKey>,
) -> BTreeSet<DisfavouredKey> {
let mut inputs: [_; 2] = [al, bl].map(|input| input.into_iter().peekable());
let mut output = BTreeSet::new();
while let Ok(items) = {
<[_; 2]>::try_from(
inputs
.iter_mut()
.flat_map(|input: &'_ mut _| input.peek()) .collect::<Vec<_>>(), )
} {
let shorter_len = items.iter().map(|i| i.path.len()).min().expect("wrong #");
let earlier_i = items
.iter()
.enumerate()
.min_by_key(|&(_i, item)| *item)
.expect("wrong #")
.0;
let later_i = 1 - earlier_i;
if items.iter().all_equal() {
let item = inputs
.iter_mut()
.map(|input| input.next().expect("but peeked"))
.last()
.expect("wrong #");
output.insert(item);
continue;
} else if items
.iter()
.map(|item| &item.path[0..shorter_len])
.all_equal()
{
let shorter_item = items[earlier_i];
let prefix = shorter_item.path.clone();
while let Some(longer_item) = inputs[later_i].peek() {
if !longer_item.path.starts_with(&prefix) {
break;
}
let longer_item = inputs[later_i].next().expect("but peeked");
output.insert(longer_item);
}
let _ = inputs[earlier_i].next().expect("but peeked");
} else {
let _ = inputs[earlier_i].next().expect("but peeked");
}
}
output
}
impl Display for DisfavouredKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use PathEntry as PE;
if self.path.is_empty() {
write!(f, r#""""#)?;
} else {
let delims = chain!(iter::once(""), iter::repeat("."));
for (delim, ent) in izip!(delims, self.path.iter()) {
match ent {
PE::ArrayIndex(index) => write!(f, "[{}]", index)?,
PE::MapEntry(s) => {
if ok_unquoted(s) {
write!(f, "{}{}", delim, s)?;
} else {
write!(f, "{}{:?}", delim, s)?;
}
}
}
}
}
Ok(())
}
}
fn ok_unquoted(s: &str) -> bool {
let mut chars = s.chars();
if let Some(c) = chars.next() {
c.is_ascii_alphanumeric()
&& chars.all(|c| c == '_' || c == '-' || c.is_ascii_alphanumeric())
} else {
false
}
}
#[cfg(test)]
#[allow(unreachable_pub)] #[allow(clippy::unwrap_used)] mod test {
use super::*;
use crate::*;
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
fn parse_test_set(l: &[&str]) -> BTreeSet<DisfavouredKey> {
l.iter()
.map(|s| DisfavouredKey {
path: s
.split('.')
.map(|s| PathEntry::MapEntry(s.into()))
.collect_vec(),
})
.collect()
}
#[test]
#[rustfmt::skip] fn test_intersect_unrecognized_list() {
let chk = |a, b, exp| {
let got = intersect_unrecognized_lists(parse_test_set(a), parse_test_set(b));
let exp = parse_test_set(exp);
assert_eq! { got, exp };
let got = intersect_unrecognized_lists(parse_test_set(b), parse_test_set(a));
assert_eq! { got, exp };
};
chk(&[ "a", "b", ],
&[ "a", "c" ],
&[ "a" ]);
chk(&[ "a", "b", "d" ],
&[ "a", "c", "d" ],
&[ "a", "d" ]);
chk(&[ "x.a", "x.b", ],
&[ "x.a", "x.c" ],
&[ "x.a" ]);
chk(&[ "t", "u", "v", "w" ],
&[ "t", "v.a", "v.b", "x" ],
&[ "t", "v.a", "v.b", ]);
chk(&[ "t", "v", "x" ],
&[ "t", "u", "v.a", "v.b", "w" ],
&[ "t", "v.a", "v.b", ]);
}
#[test]
#[allow(clippy::bool_assert_comparison)] fn test_ok_unquoted() {
assert_eq! { false, ok_unquoted("") };
assert_eq! { false, ok_unquoted("_") };
assert_eq! { false, ok_unquoted(".") };
assert_eq! { false, ok_unquoted("-") };
assert_eq! { false, ok_unquoted("_a") };
assert_eq! { false, ok_unquoted(".a") };
assert_eq! { false, ok_unquoted("-a") };
assert_eq! { false, ok_unquoted("a.") };
assert_eq! { true, ok_unquoted("a") };
assert_eq! { true, ok_unquoted("1") };
assert_eq! { true, ok_unquoted("z") };
assert_eq! { true, ok_unquoted("aa09_-") };
}
#[test]
fn test_display_key() {
let chk = |exp, path: &[PathEntry]| {
assert_eq! { DisfavouredKey { path: path.into() }.to_string(), exp };
};
let me = |s: &str| PathEntry::MapEntry(s.into());
use PathEntry::ArrayIndex as AI;
chk(r#""""#, &[]);
chk(r#""@""#, &[me("@")]);
chk(r#""\\""#, &[me(r#"\"#)]);
chk(r#"foo"#, &[me("foo")]);
chk(r#"foo.bar"#, &[me("foo"), me("bar")]);
chk(r#"foo[10]"#, &[me("foo"), AI(10)]);
chk(r#"[10].bar"#, &[AI(10), me("bar")]); }
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
struct TestConfigA {
#[builder(default)]
a: String,
}
impl_standard_builder! { TestConfigA }
impl TopLevel for TestConfigA {
type Builder = TestConfigABuilder;
}
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
struct TestConfigB {
#[builder(default)]
b: String,
#[builder(default)]
old: bool,
}
impl_standard_builder! { TestConfigB }
impl TopLevel for TestConfigB {
type Builder = TestConfigBBuilder;
const DEPRECATED_KEYS: &'static [&'static str] = &["old"];
}
#[test]
fn test_resolve() {
let test_data = r#"
wombat = 42
a = "hi"
old = true
"#;
let source = config::File::from_str(test_data, config::FileFormat::Toml);
let cfg = config::Config::builder()
.add_source(source)
.build()
.unwrap();
let _: (TestConfigA, TestConfigB) = resolve_ignore_warnings(cfg.clone()).unwrap();
let resolved: ResolutionResults<(TestConfigA, TestConfigB)> =
resolve_return_results(cfg).unwrap();
let (a, b) = resolved.value;
let mk_strings =
|l: Vec<DisfavouredKey>| l.into_iter().map(|ik| ik.to_string()).collect_vec();
let ign = mk_strings(resolved.unrecognized);
let depr = mk_strings(resolved.deprecated);
assert_eq! { &a, &TestConfigA { a: "hi".into() } };
assert_eq! { &b, &TestConfigB { b: "".into(), old: true } };
assert_eq! { ign, &["wombat"] };
assert_eq! { depr, &["old"] };
let _ = TestConfigA::builder();
let _ = TestConfigB::builder();
}
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
struct TestConfigC {
#[builder(default)]
c: u32,
}
impl_standard_builder! { TestConfigC }
impl TopLevel for TestConfigC {
type Builder = TestConfigCBuilder;
}
#[test]
fn build_error() {
let test_data = r#"
# wombat is not a number.
c = "wombat"
# this _would_ be unrecognized, but for the errors.
persimmons = "sweet"
"#;
let _b = TestConfigC::builder();
let source = config::File::from_str(test_data, config::FileFormat::Toml);
let cfg = config::Config::builder()
.add_source(source)
.build()
.unwrap();
{
let res1: Result<ResolutionResults<(TestConfigA, TestConfigC)>, _> =
resolve_return_results(cfg.clone());
assert!(res1.is_err());
assert!(matches!(res1, Err(ConfigResolveError::Deserialize(_))));
}
{
let res2: Result<ResolutionResults<(TestConfigC, TestConfigA)>, _> =
resolve_return_results(cfg.clone());
assert!(res2.is_err());
assert!(matches!(res2, Err(ConfigResolveError::Deserialize(_))));
}
let mut ctx = ResolveContext {
input: cfg,
unrecognized: UnrecognizedKeys::AllKeys,
};
let _res3 = TestConfigA::resolve(&mut ctx);
assert!(matches!(&ctx.unrecognized, UnrecognizedKeys::These(k) if !k.is_empty()));
{
let res4 = TestConfigC::resolve(&mut ctx);
assert!(matches!(res4, Err(ConfigResolveError::Deserialize(_))));
}
{
assert!(matches!(&ctx.unrecognized, UnrecognizedKeys::These(k) if k.is_empty()));
}
}
}