diff --git a/benches/stdlib.rs b/benches/stdlib.rs index b505562df..67f56a9f4 100644 --- a/benches/stdlib.rs +++ b/benches/stdlib.rs @@ -94,6 +94,7 @@ criterion_group!( // TODO: value is dynamic so we cannot assert equality //now, object, + object_from_array, parse_apache_log, parse_aws_alb_log, parse_aws_cloudwatch_log_subscription_message, @@ -1348,6 +1349,15 @@ bench_function! { } } +bench_function! { + object_from_array => vrl::stdlib::ObjectFromArray; + + default { + args: func_args![values: value!([["zero",null], ["one",true], ["two","foo"], ["three",3]])], + want: Ok(value!({"zero":null, "one":true, "two":"foo", "three":3})), + } +} + bench_function! { parse_aws_alb_log => vrl::stdlib::ParseAwsAlbLog; diff --git a/changelog.d/1164.feature.md b/changelog.d/1164.feature.md new file mode 100644 index 000000000..226432955 --- /dev/null +++ b/changelog.d/1164.feature.md @@ -0,0 +1,2 @@ +Added new `object_from_array` function to create an object from an array of +value pairs such as what `zip` can produce. diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index 74f1c66bf..583bd7719 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -132,6 +132,7 @@ cfg_if::cfg_if! { mod mod_func; mod now; mod object; + mod object_from_array; mod parse_apache_log; mod parse_aws_alb_log; mod parse_aws_cloudwatch_log_subscription_message; @@ -313,6 +314,7 @@ cfg_if::cfg_if! { pub use mod_func::Mod; pub use now::Now; pub use object::Object; + pub use object_from_array::ObjectFromArray; pub use parse_apache_log::ParseApacheLog; pub use parse_aws_alb_log::ParseAwsAlbLog; pub use parse_aws_cloudwatch_log_subscription_message::ParseAwsCloudWatchLogSubscriptionMessage; @@ -498,6 +500,7 @@ pub fn all() -> Vec> { Box::new(Mod), Box::new(Now), Box::new(Object), + Box::new(ObjectFromArray), Box::new(ParseApacheLog), Box::new(ParseAwsAlbLog), Box::new(ParseAwsCloudWatchLogSubscriptionMessage), diff --git a/src/stdlib/object_from_array.rs b/src/stdlib/object_from_array.rs new file mode 100644 index 000000000..4d5628edf --- /dev/null +++ b/src/stdlib/object_from_array.rs @@ -0,0 +1,109 @@ +use super::util::ConstOrExpr; +use crate::compiler::prelude::*; + +fn make_object(values: Vec) -> Resolved { + values + .into_iter() + .map(make_key_value) + .collect::>() + .map(Value::Object) +} + +fn make_key_value(value: Value) -> ExpressionResult<(KeyString, Value)> { + value.try_array().map_err(Into::into).and_then(|array| { + let mut iter = array.into_iter(); + let key: KeyString = match iter.next() { + None => return Err("array value too short".into()), + Some(Value::Bytes(key)) => String::from_utf8_lossy(&key).into(), + Some(_) => return Err("object keys must be strings".into()), + }; + let value = iter.next().unwrap_or(Value::Null); + Ok((key, value)) + }) +} + +#[derive(Clone, Copy, Debug)] +pub struct ObjectFromArray; + +impl Function for ObjectFromArray { + fn identifier(&self) -> &'static str { + "object_from_array" + } + + fn parameters(&self) -> &'static [Parameter] { + &[Parameter { + keyword: "values", + kind: kind::ARRAY, + required: true, + }] + } + + fn examples(&self) -> &'static [Example] { + &[Example { + title: "create an object from an array of keys/value pairs", + source: r#"object_from_array([["a", 1], ["b"], ["c", true, 3, 4]])"#, + result: Ok(r#"{"a": 1, "b": null, "c": true}"#), + }] + } + + fn compile( + &self, + state: &TypeState, + _ctx: &mut FunctionCompileContext, + arguments: ArgumentList, + ) -> Compiled { + let values = ConstOrExpr::new(arguments.required("values"), state); + + Ok(OFAFn { values }.as_expr()) + } +} + +#[derive(Clone, Debug)] +struct OFAFn { + values: ConstOrExpr, +} + +impl FunctionExpression for OFAFn { + fn resolve(&self, ctx: &mut Context) -> Resolved { + make_object(self.values.resolve(ctx)?.try_array()?) + } + + fn type_def(&self, _state: &TypeState) -> TypeDef { + TypeDef::object(Collection::any()) + } +} + +#[cfg(test)] +mod tests { + use crate::value; + + use super::*; + + test_function![ + object_from_array => ObjectFromArray; + + makes_object_simple { + args: func_args![values: value!([["foo", 1], ["bar", 2]])], + want: Ok(value!({"foo": 1, "bar": 2})), + tdef: TypeDef::object(Collection::any()), + } + + handles_missing_values { + args: func_args![values: value!([["foo", 1], ["bar"]])], + want: Ok(value!({"foo": 1, "bar": null})), + tdef: TypeDef::object(Collection::any()), + } + + drops_extra_values { + args: func_args![values: value!([["foo", 1, 2, 3, 4]])], + want: Ok(value!({"foo": 1})), + tdef: TypeDef::object(Collection::any()), + } + + errors_on_missing_keys { + args: func_args![values: value!([["foo", 1], []])], + want: Err("array value too short"), + tdef: TypeDef::object(Collection::any()), + } + ]; +} diff --git a/src/stdlib/util.rs b/src/stdlib/util.rs index 8cdfc6c6e..aee710755 100644 --- a/src/stdlib/util.rs +++ b/src/stdlib/util.rs @@ -1,4 +1,5 @@ -use crate::value::{KeyString, ObjectMap}; +use crate::compiler::{Context, Expression, Resolved, TypeState}; +use crate::value::{KeyString, ObjectMap, Value}; /// Rounds the given number to the given precision. /// Takes a function parameter so the exact rounding function (ceil, floor or round) @@ -115,3 +116,29 @@ impl std::str::FromStr for Base64Charset { } } } + +#[derive(Clone, Debug)] +pub(super) enum ConstOrExpr { + Const(Value), + Expr(Box), +} + +impl ConstOrExpr { + pub(super) fn new(expr: Box, state: &TypeState) -> Self { + match expr.resolve_constant(state) { + Some(cnst) => Self::Const(cnst), + None => Self::Expr(expr), + } + } + + pub(super) fn optional(expr: Option>, state: &TypeState) -> Option { + expr.map(|expr| Self::new(expr, state)) + } + + pub(super) fn resolve(&self, ctx: &mut Context) -> Resolved { + match self { + Self::Const(value) => Ok(value.clone()), + Self::Expr(expr) => expr.resolve(ctx), + } + } +} diff --git a/src/stdlib/zip.rs b/src/stdlib/zip.rs index 8b2de86dd..ec640324a 100644 --- a/src/stdlib/zip.rs +++ b/src/stdlib/zip.rs @@ -1,3 +1,4 @@ +use super::util::ConstOrExpr; use crate::compiler::prelude::*; fn zip2(value0: Value, value1: Value) -> Resolved { @@ -75,9 +76,7 @@ impl Function for Zip { arguments: ArgumentList, ) -> Compiled { let array_0 = ConstOrExpr::new(arguments.required("array_0"), state); - let array_1 = arguments - .optional("array_1") - .map(|a| ConstOrExpr::new(a, state)); + let array_1 = ConstOrExpr::optional(arguments.optional("array_1"), state); Ok(ZipFn { array_0, array_1 }.as_expr()) } @@ -103,28 +102,6 @@ impl FunctionExpression for ZipFn { } } -#[derive(Clone, Debug)] -enum ConstOrExpr { - Const(Value), - Expr(Box), -} - -impl ConstOrExpr { - fn new(expr: Box, state: &TypeState) -> Self { - match expr.resolve_constant(state) { - Some(cnst) => Self::Const(cnst), - None => Self::Expr(expr), - } - } - - fn resolve(&self, ctx: &mut Context) -> Resolved { - match self { - Self::Const(value) => Ok(value.clone()), - Self::Expr(expr) => expr.resolve(ctx), - } - } -} - #[cfg(test)] mod tests { use crate::value;