use super::FilterFunction;
use crate::functions::metadata::{ArgumentMetadata, FunctionMetadata, SyntaxVariants};
use minijinja::value::Kwargs;
use minijinja::{Error, ErrorKind, Value};
use serde_json::Map;
const OBJECT_ARG: ArgumentMetadata = ArgumentMetadata {
name: "object",
arg_type: "object",
required: true,
default: None,
description: "The object to process",
};
fn value_to_json(value: &Value, fn_name: &str) -> Result<serde_json::Value, Error> {
serde_json::to_value(value).map_err(|e| {
Error::new(
ErrorKind::InvalidOperation,
format!("{}: failed to convert value: {}", fn_name, e),
)
})
}
fn ensure_object(
json_value: serde_json::Value,
fn_name: &str,
) -> Result<Map<String, serde_json::Value>, Error> {
match json_value {
serde_json::Value::Object(map) => Ok(map),
_ => Err(Error::new(
ErrorKind::InvalidOperation,
format!("{} requires an object, not an array or primitive", fn_name),
)),
}
}
pub struct ObjectKeys;
impl ObjectKeys {
fn compute(json_value: serde_json::Value) -> Result<Value, Error> {
let map = ensure_object(json_value, "object_keys")?;
let keys: Vec<String> = map.keys().cloned().collect();
Ok(Value::from_serialize(&keys))
}
}
impl FilterFunction for ObjectKeys {
const NAME: &'static str = "object_keys";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "object_keys",
category: "object",
description: "Get object keys as an array",
arguments: &[OBJECT_ARG],
return_type: "array",
examples: &[
"{{ object_keys(object=config) }}",
"{{ config | object_keys }}",
],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let object: Value = kwargs.get("object")?;
let json_value = value_to_json(&object, "object_keys")?;
Self::compute(json_value)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
let json_value = value_to_json(value, "object_keys")?;
Self::compute(json_value)
}
}
pub struct ObjectValues;
impl ObjectValues {
fn compute(json_value: serde_json::Value) -> Result<Value, Error> {
let map = ensure_object(json_value, "object_values")?;
let values: Vec<&serde_json::Value> = map.values().collect();
Ok(Value::from_serialize(&values))
}
}
impl FilterFunction for ObjectValues {
const NAME: &'static str = "object_values";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "object_values",
category: "object",
description: "Get object values as an array",
arguments: &[OBJECT_ARG],
return_type: "array",
examples: &[
"{{ object_values(object=config) }}",
"{{ config | object_values }}",
],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let object: Value = kwargs.get("object")?;
let json_value = value_to_json(&object, "object_values")?;
Self::compute(json_value)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
let json_value = value_to_json(value, "object_values")?;
Self::compute(json_value)
}
}
pub struct ObjectFlatten;
impl ObjectFlatten {
fn compute(json_value: serde_json::Value, delimiter: &str) -> Result<Value, Error> {
if !json_value.is_object() {
return Err(Error::new(
ErrorKind::InvalidOperation,
"object_flatten requires an object",
));
}
let mut result = Map::new();
Self::flatten_recursive(&json_value, "", delimiter, &mut result);
Ok(Value::from_serialize(serde_json::Value::Object(result)))
}
fn flatten_recursive(
value: &serde_json::Value,
prefix: &str,
delimiter: &str,
result: &mut Map<String, serde_json::Value>,
) {
match value {
serde_json::Value::Object(map) => {
for (key, val) in map {
let new_key = if prefix.is_empty() {
key.clone()
} else {
format!("{}{}{}", prefix, delimiter, key)
};
Self::flatten_recursive(val, &new_key, delimiter, result);
}
}
serde_json::Value::Array(arr) => {
for (index, val) in arr.iter().enumerate() {
let new_key = if prefix.is_empty() {
index.to_string()
} else {
format!("{}{}{}", prefix, delimiter, index)
};
Self::flatten_recursive(val, &new_key, delimiter, result);
}
}
_ => {
result.insert(prefix.to_string(), value.clone());
}
}
}
}
impl FilterFunction for ObjectFlatten {
const NAME: &'static str = "object_flatten";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "object_flatten",
category: "object",
description: "Flatten nested object to dot notation",
arguments: &[
OBJECT_ARG,
ArgumentMetadata {
name: "delimiter",
arg_type: "string",
required: false,
default: Some("."),
description: "Delimiter to use between keys",
},
],
return_type: "object",
examples: &[
"{{ object_flatten(object=nested) }}",
"{{ nested | object_flatten(delimiter=\"_\") }}",
],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let object: Value = kwargs.get("object")?;
let delimiter: Option<String> = kwargs.get("delimiter")?;
let delimiter = delimiter.unwrap_or_else(|| ".".to_string());
let json_value = value_to_json(&object, "object_flatten")?;
Self::compute(json_value, &delimiter)
}
fn call_as_filter(value: &Value, kwargs: Kwargs) -> Result<Value, Error> {
let delimiter: Option<String> = kwargs.get("delimiter")?;
let delimiter = delimiter.unwrap_or_else(|| ".".to_string());
let json_value = value_to_json(value, "object_flatten")?;
Self::compute(json_value, &delimiter)
}
}