diff --git a/src/mapping/ast.rs b/src/mapping/ast.rs index 820d24d..9670e22 100644 --- a/src/mapping/ast.rs +++ b/src/mapping/ast.rs @@ -106,6 +106,8 @@ pub enum Statement { target: Path, span: Span, }, + /// `where ` — filter array elements by condition + Where { condition: Expr, span: Span }, } /// A parsed mapping program: a list of statements. diff --git a/src/mapping/eval.rs b/src/mapping/eval.rs index 2515bf5..1742446 100644 --- a/src/mapping/eval.rs +++ b/src/mapping/eval.rs @@ -26,6 +26,7 @@ fn eval_statement(stmt: &Statement, value: &Value) -> error::Result { } => eval_cast(value, path, target_type), Statement::Flatten { path, prefix, .. } => eval_flatten(value, path, prefix.as_deref()), Statement::Nest { paths, target, .. } => eval_nest(value, paths, target), + Statement::Where { condition, .. } => eval_where(value, condition), } } @@ -259,6 +260,35 @@ fn eval_nest(value: &Value, paths: &[Path], target: &Path) -> error::Result error::Result { + match value { + Value::Array(arr) => { + let mut filtered = Vec::new(); + for item in arr { + let result = eval_expr(condition, item)?; + if is_truthy(&result) { + filtered.push(item.clone()); + } + } + Ok(Value::Array(filtered)) + } + _ => { + // For non-arrays, apply as a boolean gate: if condition is true, + // return value unchanged; otherwise return null + let result = eval_expr(condition, value)?; + if is_truthy(&result) { + Ok(value.clone()) + } else { + Ok(Value::Null) + } + } + } +} + // --------------------------------------------------------------------------- // Expression evaluation // --------------------------------------------------------------------------- diff --git a/src/mapping/parser.rs b/src/mapping/parser.rs index c07637a..efa545d 100644 --- a/src/mapping/parser.rs +++ b/src/mapping/parser.rs @@ -99,6 +99,7 @@ impl Parser { TokenKind::Cast => self.parse_cast(), TokenKind::Flatten => self.parse_flatten(), TokenKind::Nest => self.parse_nest(), + TokenKind::Where => self.parse_where(), _ => { let suggestion = suggest_keyword(&token.kind); let msg = if let Some(s) = suggestion { @@ -274,6 +275,15 @@ impl Parser { }) } + fn parse_where(&mut self) -> error::Result { + let start = self.advance().unwrap(); // consume 'where' + let condition = self.parse_expr()?; + Ok(Statement::Where { + condition, + span: start.span, + }) + } + fn parse_path_list(&mut self) -> error::Result> { let mut paths = vec![self.parse_path()?]; while let Some(TokenKind::Comma) = self.peek_kind() { diff --git a/tests/where_filter.rs b/tests/where_filter.rs new file mode 100644 index 0000000..7075be1 --- /dev/null +++ b/tests/where_filter.rs @@ -0,0 +1,296 @@ +//! Integration tests for issue #21: where filtering. + +use indexmap::IndexMap; +use morph::mapping::{eval, parser}; +use morph::value::Value; + +fn run(mapping: &str, input: &Value) -> Value { + let program = parser::parse_str(mapping).unwrap(); + eval::eval(&program, input).unwrap() +} + +fn make_map(pairs: &[(&str, Value)]) -> Value { + let mut m = IndexMap::new(); + for (k, v) in pairs { + m.insert((*k).to_string(), v.clone()); + } + Value::Map(m) +} + +fn person(name: &str, age: i64, active: bool) -> Value { + make_map(&[ + ("name", Value::String(name.into())), + ("age", Value::Int(age)), + ("active", Value::Bool(active)), + ]) +} + +fn people() -> Value { + Value::Array(vec![ + person("Alice", 25, true), + person("Bob", 17, true), + person("Charlie", 30, false), + person("Diana", 15, true), + ]) +} + +// --------------------------------------------------------------------------- +// where .age > 18 +// --------------------------------------------------------------------------- + +#[test] +fn where_age_greater_than() { + let result = run("where .age > 18", &people()); + match &result { + Value::Array(arr) => { + assert_eq!(arr.len(), 2); + assert_eq!( + arr[0].get_path(".name"), + Some(&Value::String("Alice".into())) + ); + assert_eq!( + arr[1].get_path(".name"), + Some(&Value::String("Charlie".into())) + ); + } + other => panic!("expected array, got: {other:?}"), + } +} + +// --------------------------------------------------------------------------- +// where .status == "active" +// --------------------------------------------------------------------------- + +#[test] +fn where_string_comparison() { + let data = Value::Array(vec![ + make_map(&[ + ("name", Value::String("Alice".into())), + ("status", Value::String("active".into())), + ]), + make_map(&[ + ("name", Value::String("Bob".into())), + ("status", Value::String("inactive".into())), + ]), + make_map(&[ + ("name", Value::String("Charlie".into())), + ("status", Value::String("active".into())), + ]), + ]); + + let result = run(r#"where .status == "active""#, &data); + match &result { + Value::Array(arr) => { + assert_eq!(arr.len(), 2); + assert_eq!( + arr[0].get_path(".name"), + Some(&Value::String("Alice".into())) + ); + assert_eq!( + arr[1].get_path(".name"), + Some(&Value::String("Charlie".into())) + ); + } + other => panic!("expected array, got: {other:?}"), + } +} + +// --------------------------------------------------------------------------- +// where .name != null +// --------------------------------------------------------------------------- + +#[test] +fn where_null_check() { + let data = Value::Array(vec![ + make_map(&[ + ("name", Value::String("Alice".into())), + ("age", Value::Int(25)), + ]), + make_map(&[("name", Value::Null), ("age", Value::Int(30))]), + make_map(&[("age", Value::Int(20))]), + ]); + + let result = run("where .name != null", &data); + match &result { + Value::Array(arr) => { + assert_eq!(arr.len(), 1); + assert_eq!( + arr[0].get_path(".name"), + Some(&Value::String("Alice".into())) + ); + } + other => panic!("expected array, got: {other:?}"), + } +} + +// --------------------------------------------------------------------------- +// where .score >= 50 and .verified == true +// --------------------------------------------------------------------------- + +#[test] +fn where_compound_and_condition() { + let data = Value::Array(vec![ + make_map(&[ + ("name", Value::String("Alice".into())), + ("score", Value::Int(80)), + ("verified", Value::Bool(true)), + ]), + make_map(&[ + ("name", Value::String("Bob".into())), + ("score", Value::Int(40)), + ("verified", Value::Bool(true)), + ]), + make_map(&[ + ("name", Value::String("Charlie".into())), + ("score", Value::Int(90)), + ("verified", Value::Bool(false)), + ]), + make_map(&[ + ("name", Value::String("Diana".into())), + ("score", Value::Int(60)), + ("verified", Value::Bool(true)), + ]), + ]); + + let result = run("where .score >= 50 and .verified == true", &data); + match &result { + Value::Array(arr) => { + assert_eq!(arr.len(), 2); + assert_eq!( + arr[0].get_path(".name"), + Some(&Value::String("Alice".into())) + ); + assert_eq!( + arr[1].get_path(".name"), + Some(&Value::String("Diana".into())) + ); + } + other => panic!("expected array, got: {other:?}"), + } +} + +// --------------------------------------------------------------------------- +// where .role == "admin" or .role == "super" +// --------------------------------------------------------------------------- + +#[test] +fn where_or_condition() { + let data = Value::Array(vec![ + make_map(&[ + ("name", Value::String("Alice".into())), + ("role", Value::String("admin".into())), + ]), + make_map(&[ + ("name", Value::String("Bob".into())), + ("role", Value::String("user".into())), + ]), + make_map(&[ + ("name", Value::String("Charlie".into())), + ("role", Value::String("super".into())), + ]), + ]); + + let result = run(r#"where .role == "admin" or .role == "super""#, &data); + match &result { + Value::Array(arr) => { + assert_eq!(arr.len(), 2); + assert_eq!( + arr[0].get_path(".name"), + Some(&Value::String("Alice".into())) + ); + assert_eq!( + arr[1].get_path(".name"), + Some(&Value::String("Charlie".into())) + ); + } + other => panic!("expected array, got: {other:?}"), + } +} + +// --------------------------------------------------------------------------- +// where not .deleted +// --------------------------------------------------------------------------- + +#[test] +fn where_negation() { + let data = Value::Array(vec![ + make_map(&[ + ("name", Value::String("Alice".into())), + ("deleted", Value::Bool(false)), + ]), + make_map(&[ + ("name", Value::String("Bob".into())), + ("deleted", Value::Bool(true)), + ]), + make_map(&[ + ("name", Value::String("Charlie".into())), + ("deleted", Value::Bool(false)), + ]), + ]); + + let result = run("where not .deleted", &data); + match &result { + Value::Array(arr) => { + assert_eq!(arr.len(), 2); + assert_eq!( + arr[0].get_path(".name"), + Some(&Value::String("Alice".into())) + ); + assert_eq!( + arr[1].get_path(".name"), + Some(&Value::String("Charlie".into())) + ); + } + other => panic!("expected array, got: {other:?}"), + } +} + +// --------------------------------------------------------------------------- +// where on non-array → applies as boolean gate +// --------------------------------------------------------------------------- + +#[test] +fn where_on_non_array_true() { + let data = make_map(&[ + ("name", Value::String("Alice".into())), + ("active", Value::Bool(true)), + ]); + + let result = run("where .active == true", &data); + assert_eq!(result, data); +} + +#[test] +fn where_on_non_array_false() { + let data = make_map(&[ + ("name", Value::String("Alice".into())), + ("active", Value::Bool(false)), + ]); + + let result = run("where .active == true", &data); + assert_eq!(result, Value::Null); +} + +// --------------------------------------------------------------------------- +// Empty result after filter → empty array +// --------------------------------------------------------------------------- + +#[test] +fn where_empty_result() { + let data = Value::Array(vec![person("Alice", 25, true), person("Bob", 17, true)]); + + let result = run("where .age > 100", &data); + assert_eq!(result, Value::Array(vec![])); +} + +// --------------------------------------------------------------------------- +// All pass → unchanged array +// --------------------------------------------------------------------------- + +#[test] +fn where_all_pass() { + let data = Value::Array(vec![person("Alice", 25, true), person("Bob", 30, true)]); + + let result = run("where .age > 10", &data); + assert_eq!(result, data); +}