use std::collections::BTreeMap;
use crate::error::Origin;
use crate::value::Value;
pub(crate) type Table = BTreeMap<String, Value>;
pub(crate) type Resolved = (Table, BTreeMap<String, Origin>);
pub(crate) type PerLayer = (&'static str, Table, BTreeMap<String, Origin>);
pub(crate) struct Contribution {
pub(crate) layer: &'static str,
pub(crate) origin: Origin,
pub(crate) values: Table,
}
impl Contribution {
pub(crate) fn new(layer: &'static str, origin: Origin, values: Table) -> Self {
Self {
layer,
origin,
values,
}
}
}
#[derive(Default)]
pub(crate) struct Collected {
pub(crate) layers: Vec<Contribution>,
pub(crate) siblings: BTreeMap<String, Vec<Contribution>>,
}
impl Collected {
pub(crate) fn document(
&mut self,
layer: &'static str,
origin: &Origin,
section: Option<Table>,
siblings: BTreeMap<String, Table>,
) {
if let Some(values) = section {
self.layers
.push(Contribution::new(layer, origin.clone(), values));
}
for (name, values) in siblings {
self.siblings
.entry(name)
.or_default()
.push(Contribution::new(layer, origin.clone(), values));
}
}
pub(crate) fn layer(&mut self, layer: &'static str, origin: Origin, values: Table) {
self.layers.push(Contribution::new(layer, origin, values));
}
pub(crate) fn take_layers(&mut self) -> Vec<Contribution> {
std::mem::take(&mut self.layers)
}
pub(crate) fn by_layer(
&self,
engine: &dyn crate::engine::Engine,
) -> Result<Vec<PerLayer>, crate::Error> {
let mut grouped: Vec<(&'static str, Vec<Contribution>)> = Vec::new();
for contribution in &self.layers {
let restated = Contribution::new(
contribution.layer,
contribution.origin.clone(),
contribution.values.clone(),
);
match grouped.last_mut() {
Some((name, group)) if *name == contribution.layer => group.push(restated),
_ => grouped.push((contribution.layer, vec![restated])),
}
}
grouped
.into_iter()
.map(|(name, group)| {
let (values, origins) = compose(engine, group)?;
Ok((name, values, origins))
})
.collect()
}
pub(crate) fn sibling(
&self,
engine: &dyn crate::engine::Engine,
name: &str,
) -> Result<Option<Resolved>, crate::Error> {
let Some(contributions) = self.siblings.get(name) else {
return Ok(None);
};
compose(
engine,
contributions
.iter()
.map(|c| Contribution::new(c.layer, c.origin.clone(), c.values.clone()))
.collect(),
)
.map(Some)
}
}
pub(crate) fn compose(
engine: &dyn crate::engine::Engine,
contributions: Vec<Contribution>,
) -> Result<Resolved, crate::Error> {
compose_with(engine, contributions, Fold::ShortCircuitOne)
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Fold {
ShortCircuitOne,
Always,
}
pub(crate) fn compose_with(
engine: &dyn crate::engine::Engine,
contributions: Vec<Contribution>,
fold: Fold,
) -> Result<Resolved, crate::Error> {
let (origins, trees): (Vec<Origin>, Vec<Value>) = contributions
.into_iter()
.map(|contribution| (contribution.origin, Value::Table(contribution.values)))
.unzip();
if trees.len() == 1 && fold == Fold::ShortCircuitOne {
let Some(Value::Table(values)) = trees.into_iter().next() else {
return Err(not_a_table());
};
let mut provenance = BTreeMap::new();
for (key, value) in &values {
let mut path = vec![key.clone()];
record(value, &origins[0], &mut path, &mut provenance);
}
return Ok((values, provenance));
}
let layers: Vec<crate::engine::Layer<'_>> = trees
.iter()
.enumerate()
.map(|(tag, values)| crate::engine::Layer { tag, values })
.collect();
let folded = engine.fold(&layers)?;
let Value::Table(values) = folded.values else {
return Err(not_a_table());
};
let mut provenance: BTreeMap<String, Origin> = folded
.tags
.into_iter()
.filter_map(|(path, tag)| origins.get(tag).map(|origin| (path, origin.clone())))
.collect();
backfill(&values, &trees, &origins, &mut provenance);
Ok((values, provenance))
}
fn backfill(
values: &Table,
trees: &[Value],
origins: &[Origin],
provenance: &mut BTreeMap<String, Origin>,
) {
let missing: std::collections::BTreeSet<String> = crate::value::leaf_paths_of(values)
.into_iter()
.filter(|leaf| !provenance.contains_key(leaf))
.collect();
if missing.is_empty() {
return;
}
for (index, tree) in trees.iter().enumerate() {
let Some(origin) = origins.get(index) else {
continue;
};
let Value::Table(table) = tree else {
continue;
};
for supplied in crate::value::leaf_paths_of(table) {
if missing.contains(&supplied) {
provenance.insert(supplied, origin.clone());
}
}
}
}
pub(crate) fn at<'a>(tree: &'a Table, path: &str) -> Option<&'a Value> {
let mut segments = path.split('.');
let mut current = tree.get(segments.next()?)?;
for segment in segments {
let Value::Table(nested) = current else {
return None;
};
current = nested.get(segment)?;
}
Some(current)
}
pub(crate) fn supplied_beyond(
provenance: &BTreeMap<String, Origin>,
path: &str,
ignoring: &Origin,
) -> bool {
let below = format!("{path}.");
provenance
.iter()
.any(|(known, origin)| (known == path || known.starts_with(&below)) && origin != ignoring)
}
pub(crate) fn assign(
tree: &mut Table,
provenance: &mut BTreeMap<String, Origin>,
path: &str,
value: Value,
origin: &Origin,
) {
let mut walked: Vec<String> = path.split('.').map(str::to_owned).collect();
forget(&walked, provenance);
record(&value, origin, &mut walked, provenance);
crate::layer::insert_path(tree, path, value);
}
fn not_a_table() -> crate::Error {
crate::Error::new(
crate::ErrorKind::Backend,
"the resolution engine answered with something that is not a table",
)
}
fn record(
value: &Value,
origin: &Origin,
path: &mut Vec<String>,
provenance: &mut BTreeMap<String, Origin>,
) {
match value {
Value::Table(table) if !table.is_empty() => {
for (key, nested) in table {
path.push(key.clone());
record(nested, origin, path, provenance);
path.pop();
}
}
_ => {
provenance.insert(path.join("."), origin.clone());
}
}
}
fn forget(path: &[String], provenance: &mut BTreeMap<String, Origin>) {
if path.is_empty() {
provenance.clear();
return;
}
let here = path.join(".");
let below = format!("{here}.");
provenance.retain(|known, _| *known != here && !known.starts_with(&below));
}
#[cfg(test)]
mod tests {
use super::{compose, compose_with, Contribution};
use crate::error::Origin;
use crate::value::Value;
use proptest::prelude::*;
fn table(pairs: &[(&str, Value)]) -> super::Table {
pairs
.iter()
.map(|(key, value)| ((*key).to_owned(), value.clone()))
.collect()
}
fn nested(pairs: &[(&str, Value)]) -> Value {
Value::Table(table(pairs))
}
fn origin(name: &str) -> Origin {
Origin::Env(name.to_owned())
}
fn compose_in_test(contributions: Vec<Contribution>) -> super::Resolved {
compose(crate::engine::default(), contributions).expect("the layers fold")
}
fn trees() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
(0i64..8).prop_map(|number| Value::Integer(i128::from(number))),
"[a-c]{1,3}".prop_map(Value::String),
prop::collection::vec((0i64..4).prop_map(|n| Value::Integer(i128::from(n))), 0..3)
.prop_map(Value::Array),
];
leaf.prop_recursive(3, 12, 3, |inner| {
prop::collection::btree_map("[a-c]", inner, 0..3).prop_map(Value::Table)
})
}
fn layers() -> impl Strategy<Value = Vec<(String, super::Table)>> {
prop::collection::vec(
(
"layer[0-9]",
prop::collection::btree_map("[a-c]", trees(), 0..3),
),
1..5,
)
}
proptest! {
#[test]
fn one_layer_answers_the_same_with_the_shortcut_and_with_an_engine(
values in trees()
) {
let table = match values {
Value::Table(table) => table,
other => super::Table::from([("a".to_owned(), other)]),
};
let short = compose_in_test(vec![Contribution::new(
"test",
origin("only"),
table.clone(),
)]);
for engine in crate::engine::all() {
let folded = compose_with(
engine,
vec![Contribution::new("test", origin("only"), table.clone())],
super::Fold::Always,
)
.expect("the layer folds");
prop_assert_eq!(
&short.0, &folded.0,
"the shortcut and {} disagree on the tree", engine.name()
);
prop_assert_eq!(
&short.1, &folded.1,
"the shortcut and {} disagree on who won", engine.name()
);
}
}
#[test]
fn every_engine_folds_the_same_layers_the_same_way(layers in layers()) {
let mut answers = Vec::new();
for engine in crate::engine::all() {
let contributions: Vec<_> = layers
.iter()
.map(|(layer, values)| {
Contribution::new("test", origin(layer), values.clone())
})
.collect();
answers.push((
engine.name(),
compose_with(engine, contributions, super::Fold::Always)
.expect("the layers fold"),
));
}
let (reference_name, reference) = &answers[0];
for (name, answer) in &answers[1..] {
prop_assert_eq!(
&answer.0, &reference.0,
"{} and {} disagree on the tree", reference_name, name
);
prop_assert_eq!(
&answer.1, &reference.1,
"{} and {} disagree on who won", reference_name, name
);
}
}
}
}