Skip to content

Commit

Permalink
Add 'Figment::zipjoin()' and 'Figment::zipmerge()'.
Browse files Browse the repository at this point in the history
  • Loading branch information
thorio committed Aug 31, 2024
1 parent b6f8d3d commit 7a30056
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 36 deletions.
121 changes: 116 additions & 5 deletions src/coalesce.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::Profile;
use crate::value::{Value, Map};
use crate::value::{Map, Value};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Order {
Merge,
Join,
Adjoin,
Admerge,
Zipjoin,
Zipmerge,
}

pub trait Coalescible: Sized {
Expand All @@ -17,8 +19,8 @@ pub trait Coalescible: Sized {
impl Coalescible for Profile {
fn coalesce(self, other: Self, order: Order) -> Self {
match order {
Order::Join | Order::Adjoin => self,
Order::Merge | Order::Admerge => other,
Order::Join | Order::Adjoin | Order::Zipjoin => self,
Order::Merge | Order::Admerge | Order::Zipmerge => other,
}
}
}
Expand All @@ -27,9 +29,10 @@ impl Coalescible for Value {
fn coalesce(self, other: Self, o: Order) -> Self {
use {Value::Dict as D, Value::Array as A, Order::*};
match (self, other, o) {
(D(t, a), D(_, b), Join | Adjoin) | (D(_, a), D(t, b), Merge | Admerge) => D(t, a.coalesce(b, o)),
(D(t, a), D(_, b), Join | Adjoin | Zipjoin) | (D(_, a), D(t, b), Merge | Admerge | Zipmerge) => D(t, a.coalesce(b, o)),
(A(t, mut a), A(_, b), Adjoin | Admerge) => A(t, { a.extend(b); a }),
(v, _, Join | Adjoin) | (_, v, Merge | Admerge) => v,
(A(t, a), A(_, b), Zipjoin | Zipmerge) => A(t, a.coalesce(b, o)),
(v, _, Join | Adjoin | Zipjoin) | (_, v, Merge | Admerge | Zipmerge) => v,
}
}
}
Expand All @@ -49,3 +52,111 @@ impl<K: Eq + std::hash::Hash + Ord, V: Coalescible> Coalescible for Map<K, V> {
joined
}
}

impl Coalescible for Vec<Value> {
fn coalesce(self, other: Self, order: Order) -> Self {
let mut zipped = Vec::new();
let mut other = other.into_iter();

// Coalesces self[0] with other[0], self[1] with other[1] and so on.
for a_val in self.into_iter() {
match other.next() {
// Special cases: either a or b has an empty value, in which
// case we always choose the non-empty one regardless of order.
// If both are empty we just push either of the empties.
Some(b_val) if a_val.is_none() => zipped.push(b_val),
Some(b_val) if b_val.is_none() => zipped.push(a_val),

Some(b_val) => zipped.push(a_val.coalesce(b_val, order)),
None => zipped.push(a_val),
};
}

// `b` contains more items than `a`; append them all.
zipped.extend(other);
zipped
}
}

#[cfg(test)]
mod tests {
use crate::value::Empty;
use crate::{map, value::Value};
use crate::coalesce::{Coalescible, Order};

#[test]
pub fn coalesce_values() {
fn a() -> Value { Value::from("a") }
fn b() -> Value { Value::from("b") }

fn expect(order: Order, result: Value) { assert_eq!(a().coalesce(b(), order), result) }

expect(Order::Merge, b());
expect(Order::Admerge, b());
expect(Order::Zipmerge, b());
expect(Order::Join, a());
expect(Order::Adjoin, a());
expect(Order::Zipjoin, a());
}

#[test]
pub fn coalesce_dicts() {
fn a() -> Value { Value::from(map!(
"a" => map!("one" => 1, "two" => 2),
"b" => map!("ten" => 10, "twenty" => 20),
)) }
fn b() -> Value { Value::from(map!(
"a" => map!("one" => 2, "three" => 3),
"b" => map!("ten" => 20, "thirty" => 30),
)) }
fn result_join() -> Value { Value::from(map!(
"a" => map!("one" => 1, "two" => 2, "three" => 3),
"b" => map!("ten" => 10, "twenty" => 20, "thirty" => 30),
)) }
fn result_merge() -> Value { Value::from(map!(
"a" => map!("one" => 2, "two" => 2, "three" => 3),
"b" => map!("ten" => 20, "twenty" => 20, "thirty" => 30),
)) }

fn expect(order: Order, result: Value) { assert_eq!(a().coalesce(b(), order), result) }

expect(Order::Merge, result_merge());
expect(Order::Admerge, result_merge());
expect(Order::Zipmerge, result_merge());
expect(Order::Join, result_join());
expect(Order::Adjoin, result_join());
expect(Order::Zipjoin, result_join());
}

#[test]
pub fn coalesce_arrays() {
fn a() -> Value { Value::from(vec![1, 2]) }
fn b() -> Value { Value::from(vec![2, 3, 4]) }

fn expect(order: Order, result: Value) { assert_eq!(a().coalesce(b(), order), result) }

expect(Order::Merge, Value::from(vec![2, 3, 4]));
expect(Order::Admerge, Value::from(vec![1, 2, 2, 3, 4]));
expect(Order::Zipmerge, Value::from(vec![2, 3, 4]));
expect(Order::Join, Value::from(vec![1, 2]));
expect(Order::Adjoin, Value::from(vec![1, 2, 2, 3, 4]));
expect(Order::Zipjoin, Value::from(vec![1, 2, 4]));
}

#[test]
pub fn coalesce_arrays_empty() {
fn e() -> Value { Value::from(Empty::None) }
fn v(i: i32) -> Value { Value::from(i) }
fn a() -> Value { Value::from(vec![v(50), e(), v(4)]) }
fn b() -> Value { Value::from(vec![e(), v(2), v(6), e(), v(20)]) }

fn expect(order: Order, result: Value) { assert_eq!(a().coalesce(b(), order), result) }

expect(Order::Merge, Value::from(vec![e(), v(2), v(6), e(), v(20)]));
expect(Order::Admerge, Value::from(vec![v(50), e(), v(4), e(), v(2), v(6), e(), v(20)]));
expect(Order::Zipmerge, Value::from(vec![v(50), v(2), v(6), e(), v(20)]));
expect(Order::Join, Value::from(vec![v(50), e(), v(4)]));
expect(Order::Adjoin, Value::from(vec![v(50), e(), v(4), e(), v(2), v(6), e(), v(20)]));
expect(Order::Zipjoin, Value::from(vec![v(50), v(2), v(4), e(), v(20)]));
}
}
128 changes: 117 additions & 11 deletions src/figment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,25 @@ use crate::coalesce::{Coalescible, Order};
/// ## Conflict Resolution
///
/// Conflicts arising from two providers providing values for the same key are
/// resolved via one of four strategies: [`join`], [`adjoin`], [`merge`], and
/// [`admerge`]. In general, `join` and `adjoin` prefer existing values while
/// `merge` and `admerge` prefer later values. The `ad-` strategies additionally
/// concatenate conflicting arrays whereas the non-`ad-` strategies treat arrays
/// as non-composite values.
/// resolved via one of six strategies: [`join`], [`adjoin`], [`zipjoin`],
/// [`merge`], [`admerge`], and [`zipmerge`]. In general, the `-join` strategies
/// prefer existing values while the `-merge` strategies prefer later values.
/// The `ad-` strategies additionally concatenate arrays, the `zip-` strategies
/// combine both of the first items, both of the second items and so on, whereas
/// the unprefixed strategies treat arrays as non-composite values.
///
/// The table below summarizes these strategies and their behavior, with the
/// column label referring to the type of the value pointed to by the
/// conflicting keys:
///
/// | Strategy | Dictionaries | Arrays | All Others |
/// |-------------|----------------|---------------|---------------|
/// | [`join`] | Union, Recurse | Keep Existing | Keep Existing |
/// | [`adjoin`] | Union, Recurse | Concatenate | Keep Existing |
/// | [`merge`] | Union, Recurse | Use Incoming | Use Incoming |
/// | [`admerge`] | Union, Recurse | Concatenate | Use Incoming |
/// | Strategy | Dictionaries | Arrays | All Others |
/// |--------------|----------------|---------------|---------------|
/// | [`join`] | Union, Recurse | Keep Existing | Keep Existing |
/// | [`adjoin`] | Union, Recurse | Concatenate | Keep Existing |
/// | [`zipjoin`] | Union, Recurse | Zip by index | Keep Existing |
/// | [`merge`] | Union, Recurse | Use Incoming | Use Incoming |
/// | [`admerge`] | Union, Recurse | Concatenate | Use Incoming |
/// | [`zipmerge`] | Union, Recurse | Zip by index | Use Incoming |
///
/// ### Description
///
Expand All @@ -50,6 +53,7 @@ use crate::coalesce::{Coalescible, Order};
/// * `join` uses the existing value
/// * `merge` uses the incoming value
/// * `adjoin` and `admerge` concatenate the arrays
/// * `zipjoin` and `zipmerge` combine array items with equal index
///
/// If both keys point to a **non-composite** (`String`, `Num`, etc.) or values
/// of different kinds (i.e, **array** and **num**):
Expand All @@ -59,8 +63,10 @@ use crate::coalesce::{Coalescible, Order};
///
/// [`join`]: Figment::join()
/// [`adjoin`]: Figment::adjoin()
/// [`zipjoin`]: Figment::zipjoin()
/// [`merge`]: Figment::merge()
/// [`admerge`]: Figment::admerge()
/// [`zipmerge`]: Figment::zipmerge()
///
/// For examples, refer to each strategy's documentation.
///
Expand Down Expand Up @@ -251,6 +257,56 @@ impl Figment {
self.provide(provider, Order::Adjoin)
}

/// Joins `provider` into the current figment while zipping up vectors.
/// See [conflict resolution](#conflict-resolution) for details.
///
/// ```rust
/// use figment::Figment;
/// use figment::util::map;
/// use figment::value::Map;
///
/// let figment = Figment::new()
/// .join(("string", "original"))
/// .join(("vec", vec!["item 1", "item 2"]))
/// .join(("vec_map", vec![
/// map!["inner_value" => "inner original", "old_inner_value" => "old"],
/// map!["other_value" => "other"]
/// ]));
///
/// let new_figment = Figment::new()
/// .join(("string", "replaced"))
/// .join(("vec", vec![None, Some("replaced item 2"), Some("item 3")]))
/// .join(("vec_map", vec![
/// map!["inner_value" => "inner replaced", "new_inner_value" => "new"],
/// ]))
/// .join(("new", "value"));
///
/// let figment = figment.zipjoin(new_figment); // **zipjoin**
///
/// let string: String = figment.extract_inner("string").unwrap();
/// assert_eq!(string, "original"); // existing value retained
///
/// let vec: Vec<String> = figment.extract_inner("vec").unwrap();
/// assert_eq!(vec, vec!["item 1", "item 2", "item 3"]); // arrays zipped up
///
/// let vec_map: Vec<Map<String, String>> = figment.extract_inner("vec_map").unwrap();
/// assert_eq!(vec_map, vec![
/// map![ // union of both maps
/// "inner_value".into() => "inner original".into(), // existing value retained
/// "old_inner_value".into() => "old".into(), // existing value retained
/// "new_inner_value".into() => "new".into() // new key added
/// ],
/// map!["other_value".into() => "other".into()] // new array item added
/// ]);
///
/// let new: String = figment.extract_inner("new").unwrap();
/// assert_eq!(new, "value"); // new key added
/// ```
#[track_caller]
pub fn zipjoin<T: Provider>(self, provider: T) -> Self {
self.provide(provider, Order::Zipjoin)
}

/// Merges `provider` into the current figment.
/// See [conflict resolution](#conflict-resolution) for details.
///
Expand Down Expand Up @@ -333,6 +389,56 @@ impl Figment {
self.provide(provider, Order::Admerge)
}

/// Merges `provider` into the current figment while zipping up vectors.
/// See [conflict resolution](#conflict-resolution) for details.
///
/// ```rust
/// use figment::Figment;
/// use figment::util::map;
/// use figment::value::Map;
///
/// let figment = Figment::new()
/// .join(("string", "original"))
/// .join(("vec", vec!["item 1", "item 2"]))
/// .join(("vec_map", vec![
/// map!["inner_value" => "inner original", "old_inner_value" => "old"],
/// map!["other_value" => "other"]
/// ]));
///
/// let new_figment = Figment::new()
/// .join(("string", "replaced"))
/// .join(("vec", vec![None, Some("replaced item 2"), Some("item 3")]))
/// .join(("vec_map", vec![
/// map!["inner_value" => "inner replaced", "new_inner_value" => "new"],
/// ]))
/// .join(("new", "value"));
///
/// let figment = figment.zipmerge(new_figment); // **zipmerge**
///
/// let string: String = figment.extract_inner("string").unwrap();
/// assert_eq!(string, "replaced"); // incoming value replaced existing
///
/// let vec: Vec<String> = figment.extract_inner("vec").unwrap();
/// assert_eq!(vec, vec!["item 1", "replaced item 2", "item 3"]); // arrays zipped up
///
/// let vec_map: Vec<Map<String, String>> = figment.extract_inner("vec_map").unwrap();
/// assert_eq!(vec_map, vec![
/// map![ // union of both maps
/// "inner_value".into() => "inner replaced".into(), // incoming value replaced existing
/// "old_inner_value".into() => "old".into(), // existing value retained
/// "new_inner_value".into() => "new".into() // new key added
/// ],
/// map!["other_value".into() => "other".into()] // new array item added
/// ]);
///
/// let new: String = figment.extract_inner("new").unwrap();
/// assert_eq!(new, "value"); // new key added
/// ```
#[track_caller]
pub fn zipmerge<T: Provider>(self, provider: T) -> Self {
self.provide(provider, Order::Zipmerge)
}

/// Sets the profile to extract from to `profile`.
///
/// # Example
Expand Down
8 changes: 3 additions & 5 deletions src/providers/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,9 @@ impl Env {
/// jail.set_env("APP_FOO_KEY", 20);
/// jail.set_env("APP_MAP_ONE", "1.0");
/// jail.set_env("APP_MAP_TWO", "dos");
///
/// // Note that array order currently depends on definition order
/// jail.set_env("APP_ARRAY_0", "4");
/// jail.set_env("APP_ARRAY_2", "5");
/// jail.set_env("APP_ARRAY_1", "6");
/// jail.set_env("APP_ARRAY_2", "6");
/// jail.set_env("APP_ARRAY_1", "5");
///
/// let config: Config = Figment::new()
/// .merge(Env::prefixed("APP_").split("_"))
Expand Down Expand Up @@ -628,7 +626,7 @@ impl Provider for Env {
.into_dict()
.expect("key is non-empty: must have dict");

dict = dict.coalesce(nested_dict, Order::Admerge);
dict = dict.coalesce(nested_dict, Order::Zipmerge);
}

Ok(self.profile.collect(dict))
Expand Down
34 changes: 19 additions & 15 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ pub mod vec_tuple_map {
}
}

use crate::value::{Value, Dict};
use crate::value::{Dict, Empty, Value};

/// Given a key path `key` of the form `a.b.c`, creates nested dictionaries for
/// for every path component delimited by `.` in the path string (3 in `a.b.c`),
Expand Down Expand Up @@ -270,21 +270,25 @@ use crate::value::{Value, Dict};
/// ```
pub fn nest(key: &str, value: Value) -> Value {
fn value_from(mut keys: std::str::Split<'_, char>, value: Value) -> Value {
match keys.next() {
Some(k) if k.parse::<usize>().is_ok() => {
// TODO
// even if we do honor the index, it will get lost when coalescing.
// seems to me that nesting arrays will only be truly useful when
// coalescing two array items by their index is possible.
Value::from(vec![value_from(keys, value)])
}
Some(k) if !k.is_empty() => {
let mut dict = Dict::new();
dict.insert(k.into(), value_from(keys, value));
dict.into()
}
Some(_) | None => value
let Some(key) = keys.next() else {
return value;
};

if let Ok(index) = key.parse::<usize>() {
let mut vec = vec![Value::from(Empty::None); index + 1];
vec[index] = value_from(keys, value);

return Value::from(vec);
}

if !key.is_empty() {
let mut dict = Dict::new();
dict.insert(key.into(), value_from(keys, value));

return dict.into()
}

value
}

value_from(key.split('.'), value)
Expand Down
Loading

0 comments on commit 7a30056

Please sign in to comment.