Skip to content

Commit

Permalink
feat(stdlib): Add new object_from_array function
Browse files Browse the repository at this point in the history
  • Loading branch information
bruceg committed Dec 3, 2024
1 parent c8af70c commit 560c945
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 0 deletions.
10 changes: 10 additions & 0 deletions benches/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions changelog.d/1163.feature.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions src/stdlib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -498,6 +500,7 @@ pub fn all() -> Vec<Box<dyn Function>> {
Box::new(Mod),
Box::new(Now),
Box::new(Object),
Box::new(ObjectFromArray),
Box::new(ParseApacheLog),
Box::new(ParseAwsAlbLog),
Box::new(ParseAwsCloudWatchLogSubscriptionMessage),
Expand Down
130 changes: 130 additions & 0 deletions src/stdlib/object_from_array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::compiler::prelude::*;

fn make_object(values: Vec<Value>) -> Resolved {
values
.into_iter()
.map(make_key_value)
.collect::<Result<_, _>>()
.map(Value::Object)
}

fn make_key_value(value: Value) -> Result<(KeyString, Value), ExpressionError> {
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())
}
}

#[derive(Clone, Debug)]
enum ConstOrExpr {
Const(Value),
Expr(Box<dyn Expression>),
}

impl ConstOrExpr {
fn new(expr: Box<dyn Expression>, 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;

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()),
}
];
}

0 comments on commit 560c945

Please sign in to comment.