diff --git a/core/expression/src/functions/internal.rs b/core/expression/src/functions/internal.rs index 7b0c1719..6acb1d29 100644 --- a/core/expression/src/functions/internal.rs +++ b/core/expression/src/functions/internal.rs @@ -11,6 +11,8 @@ pub enum InternalFunction { Len, Contains, Flatten, + Merge, + MergeDeep, // String Upper, @@ -85,6 +87,25 @@ impl From<&InternalFunction> for Rc { signature: FunctionSignature::single(VT::Any.array(), VT::Any.array()), }), + IF::Merge => Rc::new(CompositeFunction { + implementation: Rc::new(imp::merge), + signatures: vec![ + FunctionSignature::single(VT::Any.array(), VT::Any.array()), + FunctionSignature::single( + VT::Object(Default::default()).array(), + VT::Object(Default::default()), + ), + ], + }), + + IF::MergeDeep => Rc::new(StaticFunction { + implementation: Rc::new(imp::merge_deep), + signature: FunctionSignature::single( + VT::Object(Default::default()).array(), + VT::Object(Default::default()), + ), + }), + IF::Upper => Rc::new(StaticFunction { implementation: Rc::new(imp::upper), signature: FunctionSignature::single(VT::String, VT::String), @@ -307,6 +328,7 @@ pub(crate) mod imp { use crate::vm::date::DynamicVariableExt; use crate::vm::VmDate; use crate::{Variable as V, Variable}; + use ahash::HashMapExt; use anyhow::{anyhow, Context}; use chrono_tz::Tz; #[cfg(not(feature = "regex-lite"))] @@ -451,6 +473,117 @@ pub(crate) mod imp { Ok(V::from_array(flat_arr)) } + pub fn merge(args: Arguments) -> anyhow::Result { + let a = args.array(0)?; + let arr = a.borrow(); + + let Some(first) = arr.first() else { + return Ok(V::empty_object()); + }; + + let capacity = arr + .iter() + .map(|item| match item { + V::Object(obj) => obj.borrow().len(), + V::Array(arr) => arr.borrow().len(), + _ => 0, + }) + .sum(); + + match first { + V::Array(_) | V::Null => { + let mut merged = Vec::with_capacity(capacity); + + for item in arr.iter() { + match item { + V::Array(inner) => { + let inner = inner.borrow(); + merged.extend(inner.iter().cloned()); + } + V::Null => {} + _ => return Err(anyhow!("Expected array of arrays")), + } + } + + Ok(V::from_array(merged)) + } + V::Object(_) => { + let mut merged: ahash::HashMap, V> = + ahash::HashMap::with_capacity(capacity); + for item in arr.iter() { + match item { + V::Object(obj) => { + let obj = obj.borrow(); + for (key, value) in obj.iter() { + merged.insert(key.clone(), value.clone()); + } + } + V::Null => {} + _ => return Err(anyhow!("Expected array of objects")), + } + } + + Ok(V::from_object(merged)) + } + other => Err(anyhow!( + "merge expects an array of arrays or objects, got {}", + other.type_name() + )), + } + } + + pub fn merge_deep(args: Arguments) -> anyhow::Result { + let a = args.array(0)?; + let arr = a.borrow(); + + let mut result = V::empty_object(); + for item in arr.iter() { + match item { + V::Object(_) => { + result = deep_merge_variables(&result, item); + } + V::Null => {} + _ => return Err(anyhow!("Expected array of objects")), + } + } + + Ok(result) + } + + fn deep_merge_variables(base: &V, patch: &V) -> V { + match (base, patch) { + (V::Object(a), V::Object(b)) => { + let a = a.borrow(); + let b = b.borrow(); + let mut merged: ahash::HashMap, V> = + ahash::HashMap::with_capacity(a.len() + b.len()); + + for (key, value) in a.iter() { + merged.insert(key.clone(), value.clone()); + } + + for (key, value) in b.iter() { + let entry = merged + .get(key) + .map(|existing| deep_merge_variables(existing, value)) + .unwrap_or_else(|| value.clone()); + merged.insert(key.clone(), entry); + } + + V::from_object(merged) + } + (V::Array(a), V::Array(b)) => { + let a = a.borrow(); + let b = b.borrow(); + let mut merged = Vec::with_capacity(a.len() + b.len()); + merged.extend(a.iter().cloned()); + merged.extend(b.iter().cloned()); + V::from_array(merged) + } + (_, patch) => patch.clone(), + } + } + pub fn abs(args: Arguments) -> anyhow::Result { let a = args.number(0)?; Ok(V::Number(a.abs())) diff --git a/core/expression/src/parser/unary.rs b/core/expression/src/parser/unary.rs index cdbb2056..b1b7b642 100644 --- a/core/expression/src/parser/unary.rs +++ b/core/expression/src/parser/unary.rs @@ -364,6 +364,8 @@ impl From<&Node<'_>> for UnaryNodeBehaviour { InternalFunction::Number => CompareWithReference(Equal), InternalFunction::Bool => CompareWithReference(Equal), InternalFunction::Flatten => CompareWithReference(In), + InternalFunction::Merge => CompareWithReference(In), + InternalFunction::MergeDeep => CompareWithReference(In), InternalFunction::Extract => CompareWithReference(In), InternalFunction::Contains => AsBoolean, InternalFunction::StartsWith => AsBoolean, diff --git a/core/expression/tests/data/standard.csv b/core/expression/tests/data/standard.csv index 409755a2..a82ab076 100644 --- a/core/expression/tests/data/standard.csv +++ b/core/expression/tests/data/standard.csv @@ -159,6 +159,18 @@ none([1, 2, 3, 4, 5], # > 5);; true some([1, 2, 3, 4, 5], # > 3);; true flatMap([[1, 2], [3, 4], [5, 6]], #);; [1, 2, 3, 4, 5, 6] keys([10, 11, 12]);;[0, 1, 2] +merge([[1, 2], [3, 4], [5]]);;[1, 2, 3, 4, 5] +merge([[1, 2], [], [3]]);;[1, 2, 3] +merge([]);;{} +merge([{"a": 1}, {"b": 2}, {"c": 3}]);;{"a": 1, "b": 2, "c": 3} +merge([{"a": 1, "b": 2}, {"b": 3, "c": 4}]);;{"a": 1, "b": 3, "c": 4} +mergeDeep([{"a": 1}, {"b": 2}]);;{"a": 1, "b": 2} +mergeDeep([{"a": 1, "b": 2}, {"b": 3, "c": 4}]);;{"a": 1, "b": 3, "c": 4} +mergeDeep([{"a": {"x": 1}}, {"a": {"y": 2}}]);;{"a": {"x": 1, "y": 2}} +mergeDeep([{"a": [1, 2]}, {"a": [3, 4]}]);;{"a": [1, 2, 3, 4]} +mergeDeep([{"a": {"b": {"c": 1}}}, {"a": {"b": {"d": 2}}}]);;{"a": {"b": {"c": 1, "d": 2}}} +mergeDeep([{"a": 1}, {"a": 2}]);;{"a": 2} +mergeDeep([]);;{} # Dates date("2023-09-18T12:00:00Z");; 1695038400 diff --git a/core/expression/tests/isolate.rs b/core/expression/tests/isolate.rs index 82a029e8..1bb79915 100644 --- a/core/expression/tests/isolate.rs +++ b/core/expression/tests/isolate.rs @@ -569,6 +569,34 @@ fn isolate_standard_test() { expr: r#"flatMap(nestedArray, #)"#, result: json!([1, 2, 3, 4, 5, 6]), }, + TestCase { + expr: r#"merge(nestedArray)"#, + result: json!([1, 2, 3, 4, 5, 6]), + }, + TestCase { + expr: r#"merge([[1, 2], [3, 4], [5]])"#, + result: json!([1, 2, 3, 4, 5]), + }, + ]), + }, + TestEnv { + env: json!({ + "objects": [{"a": 1}, {"b": 2}, {"c": 3}], + "overlapping": [{"a": 1, "b": 2}, {"b": 3, "c": 4}], + }), + cases: Vec::from([ + TestCase { + expr: r#"merge(objects)"#, + result: json!({"a": 1, "b": 2, "c": 3}), + }, + TestCase { + expr: r#"merge(overlapping)"#, + result: json!({"a": 1, "b": 3, "c": 4}), + }, + TestCase { + expr: r#"merge([])"#, + result: json!({}), + }, ]), }, TestEnv {