vrl 0.32.0

Vector Remap Language
Documentation
use super::util::ConstOrExpr;
use crate::compiler::prelude::*;

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

fn make_object_2(keys: Vec<Value>, values: Vec<Value>) -> Resolved {
    keys.into_iter()
        .zip(values)
        .filter_map(|(key, value)| {
            make_key_string(key)
                .transpose()
                .map(|key| key.map(|key| (key, value)))
        })
        .collect::<Result<_, _>>()
        .map(Value::Object)
}

fn make_key_value(value: Value) -> ExpressionResult<Option<(KeyString, Value)>> {
    let array = value.try_array()?;
    let mut iter = array.into_iter();
    let Some(key) = iter.next() else {
        return Err("array value too short".into());
    };
    Ok(make_key_string(key)?.map(|key| (key, iter.next().unwrap_or(Value::Null))))
}

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

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

impl Function for ObjectFromArray {
    fn identifier(&self) -> &'static str {
        "object_from_array"
    }

    fn usage(&self) -> &'static str {
        indoc! {"
            Iterate over either one array of arrays or a pair of arrays and create an object out of all the key-value pairs contained in them.
            With one array of arrays, any entries with no value use `null` instead.
            Any keys that are `null` skip the  corresponding value.

            If a single parameter is given, it must contain an array of all the input arrays.
        "}
    }

    fn category(&self) -> &'static str {
        Category::Object.as_ref()
    }

    fn internal_failure_reasons(&self) -> &'static [&'static str] {
        &[
            "`values` and `keys` must be arrays.",
            "If `keys` is not present, `values` must contain only arrays.",
        ]
    }

    fn return_kind(&self) -> u16 {
        kind::OBJECT
    }

    fn return_rules(&self) -> &'static [&'static str] {
        &[
            "`object_from_array` is considered fallible in the following cases: if any of the parameters is not an array; if only the `value` parameter is present and it is not an array of arrays; or if any of the keys are not either a string or `null`.",
        ]
    }

    fn parameters(&self) -> &'static [Parameter] {
        const PARAMETERS: &[Parameter] = &[
            Parameter::required(
                "values",
                kind::ARRAY,
                "The first array of elements, or the array of input arrays if no other parameter is present.",
            ),
            Parameter::optional(
                "keys",
                kind::ARRAY,
                "The second array of elements. If not present, the first parameter must contain all the arrays.",
            ),
        ];
        PARAMETERS
    }

    fn examples(&self) -> &'static [Example] {
        &[
            example! {
                title: "Create an object from one array",
                source: r#"object_from_array([["one", 1], [null, 2], ["two", 3]])"#,
                result: Ok(r#"{ "one": 1, "two": 3 }"#),
            },
            example! {
                title: "Create an object from separate key and value arrays",
                source: r#"object_from_array([1, 2, 3], keys: ["one", null, "two"])"#,
                result: Ok(r#"{ "one": 1, "two": 3 }"#),
            },
            example! {
                title: "Create an object from a separate arrays of keys and values",
                source: r#"object_from_array(values: [1, null, true], keys: ["a", "b", "c"])"#,
                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);
        let keys = arguments
            .optional("keys")
            .map(|keys| ConstOrExpr::new(keys, state));

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

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

        skips_null_keys1 {
            args: func_args![values: value!([["foo", 1], [null, 2], ["bar", 3]])],
            want: Ok(value!({"foo": 1, "bar": 3})),
            tdef: TypeDef::object(Collection::any()),
        }

        skips_null_keys2 {
            args: func_args![values: value!([1, 2, 3]), keys: value!(["foo", null, "bar"])],
            want: Ok(value!({"foo": 1, "bar": 3})),
            tdef: TypeDef::object(Collection::any()),
        }
    ];
}