diff --git a/docs/builtins.md b/docs/builtins.md index dddf124b9..e5f1fd81b 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -52,6 +52,7 @@ In future, each builtin will be associated with a feature (many builtins could b | Builtin | Feature | |-----------------------------------------------------------------------------------------------------------|---------| | [array.concat](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayconcat) | _ | + | [array.flatten](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayflatten) | _ | | [array.reverse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayreverse) | _ | | [array.slice](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayslice) | _ | diff --git a/src/builtins/arrays.rs b/src/builtins/arrays.rs index 3245f2c87..a3ca58538 100644 --- a/src/builtins/arrays.rs +++ b/src/builtins/arrays.rs @@ -4,15 +4,17 @@ use crate::ast::{Expr, Ref}; use crate::builtins; -use crate::builtins::utils::{ensure_args_count, ensure_array, ensure_numeric}; +use crate::builtins::utils::{enforce_limit, ensure_args_count, ensure_array, ensure_numeric}; use crate::lexer::Span; use crate::Rc; use crate::Value; +use crate::Vec; use anyhow::Result; pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) { m.insert("array.concat", (concat, 2)); + m.insert("array.flatten", (flatten, 1)); m.insert("array.reverse", (reverse, 1)); m.insert("array.slice", (slice, 3)); } @@ -68,3 +70,28 @@ fn slice(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re let slice = array.as_slice().get(start..stop).unwrap_or_default(); Ok(Value::from(slice.to_vec())) } + +fn flatten(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "array.flatten"; + ensure_args_count(span, name, params, args, 1)?; + let array = ensure_array(name, ¶ms[0], args[0].clone())?; + let mut flattened = Vec::new(); + + for value in array.iter() { + // `pattern_type_mismatch` requires the explicit `&`/`ref` here, which in + // turn triggers `needless_borrowed_reference`; the two lints conflict for + // this shape, so silence the latter (see template_functions_collection.rs). + #[allow(clippy::needless_borrowed_reference)] + if let &Value::Array(ref nested) = value { + for nested_value in nested.iter() { + flattened.push(nested_value.clone()); + enforce_limit()?; + } + } else { + flattened.push(value.clone()); + enforce_limit()?; + } + } + + Ok(Value::from_array(flattened)) +} diff --git a/tests/interpreter/cases/builtins/arrays/flatten.yaml b/tests/interpreter/cases/builtins/arrays/flatten.yaml new file mode 100644 index 000000000..c22cdb102 --- /dev/null +++ b/tests/interpreter/cases/builtins/arrays/flatten.yaml @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: shallow + data: {} + modules: + - | + package test + x = array.flatten([[1, 2], [3, 4]]) + query: data.test.x + want_result: [1, 2, 3, 4] + + - note: mixed-depth + data: {} + modules: + - | + package test + x = array.flatten([[1, [2, 3]], 4, [5]]) + query: data.test.x + want_result: [1, [2, 3], 4, 5] + + - note: empty + data: {} + modules: + - | + package test + x = array.flatten([]) + query: data.test.x + want_result: [] + + - note: all-scalars + data: {} + modules: + - | + package test + x = array.flatten([1, 2, 3]) + query: data.test.x + want_result: [1, 2, 3] + + - note: undefined-element-propagates + data: {} + modules: + - | + package test + r { false } + x = array.flatten([r]) + query: data.test.x + no_result: true + + - note: wrong-type + data: {} + modules: + - | + package test + x = array.flatten("not-an-array") + query: data.test.x + error: "`array.flatten` expects array argument." + + - note: too-many-args + data: {} + modules: + - | + package test + x = array.flatten([1], [2]) + query: data.test.x + error: "`array.flatten` expects 1 argument" diff --git a/tests/rvm/rego/cases/array_flatten.yaml b/tests/rvm/rego/cases/array_flatten.yaml new file mode 100644 index 000000000..636e4a9fe --- /dev/null +++ b/tests/rvm/rego/cases/array_flatten.yaml @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# RVM coverage for `array.flatten`, mirroring +# tests/interpreter/cases/builtins/arrays/flatten.yaml so the interpreter and +# RVM execution paths stay in parity for this builtin. + +cases: + - note: shallow + data: {} + modules: + - | + package test + x = array.flatten([[1, 2], [3, 4]]) + query: data.test.x + want_result: [1, 2, 3, 4] + + - note: mixed-depth + data: {} + modules: + - | + package test + x = array.flatten([[1, [2, 3]], 4, [5]]) + query: data.test.x + want_result: [1, [2, 3], 4, 5] + + - note: empty + data: {} + modules: + - | + package test + x = array.flatten([]) + query: data.test.x + want_result: [] + + - note: all-scalars + data: {} + modules: + - | + package test + x = array.flatten([1, 2, 3]) + query: data.test.x + want_result: [1, 2, 3] + + - note: undefined-element-propagates + data: {} + modules: + - | + package test + r if { false } + x = array.flatten([r]) + query: data.test.x + want_result: "#undefined" + + - note: wrong-type + data: {} + modules: + - | + package test + x = array.flatten("not-an-array") + query: data.test.x + want_result: "#undefined" + # RVM defaults `strict_builtin_errors` to false (the interpreter defaults + # to true), so the type-check `bail!` raised by `ensure_array` is + # swallowed to Undefined here rather than surfacing as an error. This is + # a pre-existing, general divergence in default settings that applies to + # every builtin's error path, not something specific to `array.flatten`. + allow_interpreter_incorrect_behavior: true + + - note: too-many-args + data: {} + modules: + - | + package test + x = array.flatten([1], [2], [3]) + query: data.test.x + want_result: "#undefined" + # Same non-strict-builtin-errors divergence as `wrong-type` above: the + # `ensure_args_count` arity-check `bail!` is swallowed to Undefined under + # RVM's default settings instead of surfacing as an error. (Two args are + # deliberately avoided here because `array.flatten(a, b)` with exactly + # one extra argument is valid Rego out-param call syntax equivalent to + # `b := array.flatten(a)`; three args unambiguously exceeds that.) + allow_interpreter_incorrect_behavior: true