From 9473334fc15b06de68aa31588183c9d3d0028c86 Mon Sep 17 00:00:00 2001 From: Sheshnath Yadav Date: Sat, 14 Mar 2026 22:33:56 -0400 Subject: [PATCH 1/2] feat: add filter() macro for array filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a built-in `filter(array, predicate)` macro that returns only the elements of an array for which the predicate expression is truthy. Must be a macro (not a function) so the predicate is re-evaluated with each element as the context node (`.`), matching the same pattern used by the existing `for` expression and `fallback` macro. Behaviour follows existing JSLT conventions: - null input → null (matches `for` expression) - non-array input → JsltException (matches `for` expression) - empty array → [] - falsy/null predicate result → element excluded Includes dedicated test files (filter-tests.json, filter-error-tests.json) registered in QueryTest and QueryErrorTest. --- .../spt/data/jslt/impl/BuiltinFunctions.java | 28 +++++++++++++++ .../spt/data/jslt/QueryErrorTest.java | 1 + .../schibsted/spt/data/jslt/QueryTest.java | 1 + .../test/resources/filter-error-tests.json | 11 ++++++ core/src/test/resources/filter-tests.json | 36 +++++++++++++++++++ 5 files changed, 77 insertions(+) create mode 100644 core/src/test/resources/filter-error-tests.json create mode 100644 core/src/test/resources/filter-tests.json diff --git a/core/src/main/java/com/schibsted/spt/data/jslt/impl/BuiltinFunctions.java b/core/src/main/java/com/schibsted/spt/data/jslt/impl/BuiltinFunctions.java index b903cbe9..f984ee97 100644 --- a/core/src/main/java/com/schibsted/spt/data/jslt/impl/BuiltinFunctions.java +++ b/core/src/main/java/com/schibsted/spt/data/jslt/impl/BuiltinFunctions.java @@ -138,6 +138,7 @@ public class BuiltinFunctions { public static Map macros = new HashMap(); static { macros.put("fallback", new BuiltinFunctions.Fallback()); + macros.put("filter", new BuiltinFunctions.Filter()); } private static abstract class AbstractMacro extends AbstractCallable implements Macro { @@ -599,6 +600,33 @@ public JsonNode call(Scope scope, JsonNode input, } } + // ===== FILTER + + public static class Filter extends AbstractMacro { + + public Filter() { + super("filter", 2, 2); + } + + public JsonNode call(Scope scope, JsonNode input, + ExpressionNode[] parameters) { + JsonNode array = parameters[0].apply(scope, input); + if (array.isNull()) + return NullNode.instance; + if (!array.isArray()) + throw new JsltException("filter() argument is not an array: " + array); + + ArrayNode result = NodeUtils.mapper.createArrayNode(); + for (int ix = 0; ix < array.size(); ix++) { + JsonNode element = array.get(ix); + JsonNode test = parameters[1].apply(scope, element); + if (NodeUtils.isTrue(test)) + result.add(element); + } + return result; + } + } + // ===== IS-OBJECT public static class IsObject extends AbstractFunction { diff --git a/core/src/test/java/com/schibsted/spt/data/jslt/QueryErrorTest.java b/core/src/test/java/com/schibsted/spt/data/jslt/QueryErrorTest.java index 078044ff..1fdfbb51 100644 --- a/core/src/test/java/com/schibsted/spt/data/jslt/QueryErrorTest.java +++ b/core/src/test/java/com/schibsted/spt/data/jslt/QueryErrorTest.java @@ -60,6 +60,7 @@ public static Collection data() { List strings = new ArrayList(); strings.addAll(loadTests("query-error-tests.json")); strings.addAll(loadTests("function-error-tests.json")); + strings.addAll(loadTests("filter-error-tests.json")); strings.addAll(loadTests("function-declaration-tests.yaml")); return strings; } diff --git a/core/src/test/java/com/schibsted/spt/data/jslt/QueryTest.java b/core/src/test/java/com/schibsted/spt/data/jslt/QueryTest.java index 221a8037..23f24779 100644 --- a/core/src/test/java/com/schibsted/spt/data/jslt/QueryTest.java +++ b/core/src/test/java/com/schibsted/spt/data/jslt/QueryTest.java @@ -67,6 +67,7 @@ public static Collection data() { strings.addAll(loadTests("query-tests.yaml")); strings.addAll(loadTests("function-tests.json")); strings.addAll(loadTests("experimental-tests.json")); + strings.addAll(loadTests("filter-tests.json")); strings.addAll(loadTests("function-declaration-tests.yaml")); return strings; } diff --git a/core/src/test/resources/filter-error-tests.json b/core/src/test/resources/filter-error-tests.json new file mode 100644 index 00000000..7249693f --- /dev/null +++ b/core/src/test/resources/filter-error-tests.json @@ -0,0 +1,11 @@ +{ + "description" : "Error tests for the filter() macro.", + "tests" : +[ + { + "query" : "filter(42, .active)", + "input" : "{}", + "error" : "filter() argument is not an array" + } +] +} diff --git a/core/src/test/resources/filter-tests.json b/core/src/test/resources/filter-tests.json new file mode 100644 index 00000000..b625b309 --- /dev/null +++ b/core/src/test/resources/filter-tests.json @@ -0,0 +1,36 @@ +{ + "description" : "Tests for the filter() macro.", + "tests" : +[ + { + "query" : "filter([{\"a\":1,\"active\":true},{\"a\":2,\"active\":false}], .active)", + "input" : "{}", + "output" : "[{\"a\":1,\"active\":true}]" + }, + { + "query" : "filter([1, 2, 3, 4, 5], . > 3)", + "input" : "{}", + "output" : "[4, 5]" + }, + { + "query" : "filter(null, .active)", + "input" : "{}", + "output" : "null" + }, + { + "query" : "filter([], .active)", + "input" : "{}", + "output" : "[]" + }, + { + "query" : "[for (filter([{\"id\":1,\"ok\":true},{\"id\":2,\"ok\":false}], .ok)) .id]", + "input" : "{}", + "output" : "[1]" + }, + { + "query" : "size(filter([{\"active\":true},{\"active\":false},{\"active\":true}], .active))", + "input" : "{}", + "output" : "2" + } +] +} From a45e2c5e8029b7fa8f9e7a649f560a7ae6ba1e36 Mon Sep 17 00:00:00 2001 From: Sheshnath Yadav Date: Sat, 14 Mar 2026 22:42:36 -0400 Subject: [PATCH 2/2] docs: document filter() macro in functions.md --- functions.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/functions.md b/functions.md index 3065ce01..c34f4415 100644 --- a/functions.md +++ b/functions.md @@ -58,6 +58,24 @@ if (not(is-array(.things))) error("'things' is not an array") ``` +### _filter(array, expr) -> array_ + +Returns a new array containing only the elements of _array_ for which +_expr_ evaluates to a truthy value. _expr_ is evaluated with each +element as the context node (`.`). + +If _array_ is `null` the result is `null`. If _array_ is empty the +result is `[]`. + +Examples: + +``` +filter([1, 2, 3, 4, 5], . > 3) => [4, 5] +filter(.items, .active) => [{...}, ...] +size(filter(.items, .active)) => 2 +[for (filter(.items, .active)) .id] => [...] +``` + ### _fallback(arg1, arg2, ...) -> value_ Returns the first argument that has a value. That is, the first