From 7a3005641a0cdf20a329c2eb467669912b7bdd53 Mon Sep 17 00:00:00 2001 From: thorou Date: Thu, 25 Jul 2024 07:14:16 +0200 Subject: [PATCH] Add 'Figment::zipjoin()' and 'Figment::zipmerge()'. --- src/coalesce.rs | 121 ++++++++++++++++++++++++++++++++++++++-- src/figment.rs | 128 +++++++++++++++++++++++++++++++++++++++---- src/providers/env.rs | 8 +-- src/util.rs | 34 +++++++----- src/value/value.rs | 4 ++ 5 files changed, 259 insertions(+), 36 deletions(-) diff --git a/src/coalesce.rs b/src/coalesce.rs index b795a39..b8fe839 100644 --- a/src/coalesce.rs +++ b/src/coalesce.rs @@ -1,5 +1,5 @@ use crate::Profile; -use crate::value::{Value, Map}; +use crate::value::{Map, Value}; #[derive(Debug, Clone, Copy, PartialEq)] pub enum Order { @@ -7,6 +7,8 @@ pub enum Order { Join, Adjoin, Admerge, + Zipjoin, + Zipmerge, } pub trait Coalescible: Sized { @@ -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, } } } @@ -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, } } } @@ -49,3 +52,111 @@ impl Coalescible for Map { joined } } + +impl Coalescible for Vec { + 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)])); + } +} diff --git a/src/figment.rs b/src/figment.rs index 2d39ecd..7166851 100644 --- a/src/figment.rs +++ b/src/figment.rs @@ -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 /// @@ -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**): @@ -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. /// @@ -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 = figment.extract_inner("vec").unwrap(); + /// assert_eq!(vec, vec!["item 1", "item 2", "item 3"]); // arrays zipped up + /// + /// let vec_map: Vec> = 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(self, provider: T) -> Self { + self.provide(provider, Order::Zipjoin) + } + /// Merges `provider` into the current figment. /// See [conflict resolution](#conflict-resolution) for details. /// @@ -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 = figment.extract_inner("vec").unwrap(); + /// assert_eq!(vec, vec!["item 1", "replaced item 2", "item 3"]); // arrays zipped up + /// + /// let vec_map: Vec> = 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(self, provider: T) -> Self { + self.provide(provider, Order::Zipmerge) + } + /// Sets the profile to extract from to `profile`. /// /// # Example diff --git a/src/providers/env.rs b/src/providers/env.rs index 95188f0..1a4c538 100644 --- a/src/providers/env.rs +++ b/src/providers/env.rs @@ -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("_")) @@ -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)) diff --git a/src/util.rs b/src/util.rs index 971091c..fc36050 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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`), @@ -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::().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::() { + 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) diff --git a/src/value/value.rs b/src/value/value.rs index 2211c51..aefb1fd 100644 --- a/src/value/value.rs +++ b/src/value/value.rs @@ -426,6 +426,10 @@ impl Value { } } + pub(crate) fn is_none(&self) -> bool { + matches!(self, Value::Empty(_, Empty::None)) + } + /// Attempts to retrieve a nested value through dictionary key or array index. pub(crate) fn index(self, key: &str) -> Option { fn try_remove(mut vec: Vec, key: &str) -> Option {