Skip to content

Commit

Permalink
enhancement(stdlib): Add support for keys parameter to `object_from…
Browse files Browse the repository at this point in the history
…_array` (#1176)

* enhancement(stdlib): Add support for `keys` parameter to `object_from_array`

* Add underscores to internal function names
  • Loading branch information
bruceg authored Dec 5, 2024
1 parent 92c8aef commit 4bad088
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 19 deletions.
8 changes: 8 additions & 0 deletions benches/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,14 @@ bench_function! {
args: func_args![values: value!([["zero",null], ["one",true], ["two","foo"], ["three",3]])],
want: Ok(value!({"zero":null, "one":true, "two":"foo", "three":3})),
}

values_and_keys {
args: func_args![
keys: value!(["zero", "one", "two", "three"]),
values: value!([null, true, "foo", 3]),
],
want: Ok(value!({"zero":null, "one":true, "two":"foo", "three":3})),
}
}

bench_function! {
Expand Down
78 changes: 59 additions & 19 deletions src/stdlib/object_from_array.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
use super::util::ConstOrExpr;
use crate::compiler::prelude::*;

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

fn make_object_2(keys: Vec<Value>, values: Vec<Value>) -> Resolved {
keys.into_iter()
.zip(values)
.map(|(key, value)| Ok((make_key_string(key)?, value)))
.collect::<Result<_, _>>()
.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 Some(key) = iter.next() else {
return Err("array value too short".into());
};
let value = iter.next().unwrap_or(Value::Null);
Ok((key, value))
Ok((make_key_string(key)?, iter.next().unwrap_or(Value::Null)))
})
}

fn make_key_string(key: Value) -> ExpressionResult<KeyString> {
match key {
Value::Bytes(key) => Ok(String::from_utf8_lossy(&key).into()),
_ => Err("object keys must be strings".into()),
}
}

#[derive(Clone, Copy, Debug)]
pub struct ObjectFromArray;

Expand All @@ -31,19 +43,33 @@ impl Function for ObjectFromArray {
}

fn parameters(&self) -> &'static [Parameter] {
&[Parameter {
keyword: "values",
kind: kind::ARRAY,
required: true,
}]
&[
Parameter {
keyword: "values",
kind: kind::ARRAY,
required: true,
},
Parameter {
keyword: "keys",
kind: kind::ARRAY,
required: false,
},
]
}

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}"#),
}]
&[
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}"#),
},
Example {
title: "create an object from a separate arrays of keys and values",
source: r#"object_from_array(keys: ["a", "b", "c"], values: [1, null, true])"#,
result: Ok(r#"{"a": 1, "b": null, "c": true}"#),
},
]
}

fn compile(
Expand All @@ -53,19 +79,27 @@ impl Function for ObjectFromArray {
arguments: ArgumentList,
) -> Compiled {
let values = ConstOrExpr::new(arguments.required("values"), state);
let keys = arguments
.optional("keys")
.map(|keys| ConstOrExpr::new(keys, state));

Ok(OFAFn { values }.as_expr())
Ok(OFAFn { keys, values }.as_expr())
}
}

#[derive(Clone, Debug)]
struct OFAFn {
keys: Option<ConstOrExpr>,
values: ConstOrExpr,
}

impl FunctionExpression for OFAFn {
fn resolve(&self, ctx: &mut Context) -> Resolved {
make_object(self.values.resolve(ctx)?.try_array()?)
let values = self.values.resolve(ctx)?.try_array()?;
match &self.keys {
None => make_object_1(values),
Some(keys) => make_object_2(keys.resolve(ctx)?.try_array()?, values),
}
}

fn type_def(&self, _state: &TypeState) -> TypeDef {
Expand All @@ -88,6 +122,12 @@ mod tests {
tdef: TypeDef::object(Collection::any()),
}

uses_keys_parameter {
args: func_args![keys: value!(["foo", "bar"]), values: value!([1, 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})),
Expand Down

0 comments on commit 4bad088

Please sign in to comment.