liquid-json 0.6.1

A Liquid template engine for JSON
Documentation
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::{Display_filter, Filter, FilterReflection, ParseFilter};
use liquid_core::{Value, ValueView};
use serde::de::DeserializeOwned;

use super::invalid_input;

#[derive(Clone, ParseFilter, FilterReflection)]
#[filter(
    name = "json",
    description = "Parses a JSON string into a JSON object.",
    parsed(JsonFilter)
)]
pub(crate) struct Json;

#[derive(Default, Display_filter)]
#[name = "json"]
struct JsonFilter;

impl std::fmt::Debug for JsonFilter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("JsonFilter").finish()
    }
}

impl Filter for JsonFilter {
    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
        let input = input.to_value();

        jsonify(input)
    }
}

fn jsonify(input: Value) -> Result<Value> {
    Ok(match input {
        Value::Scalar(v) => {
            let view = v.as_view();
            match view.type_name() {
                "string" => parse_json(v.to_kstr())?,
                _ => Value::Scalar(v),
            }
        }
        Value::Array(v) => Value::Array(v.into_iter().map(jsonify).collect::<Result<_, _>>()?),
        Value::Object(v) => Value::Object(
            v.into_iter()
                .map(|(k, v)| (k, jsonify(v).unwrap()))
                .collect(),
        ),
        Value::State(_) => unreachable!(),
        Value::Nil => input,
    })
}

fn parse_json<T: DeserializeOwned>(src: impl AsRef<str>) -> Result<T> {
    serde_json::from_str(src.as_ref())
        .map_err(|e| invalid_input(format!("invalid JSON string: {}", e)))
}

#[cfg(test)]
mod tests {
    use crate::LiquidJson;
    use rstest::rstest;
    use serde_json::{json, Value};

    use anyhow::Result;

    #[rstest]
    #[case(json!({"this":"{{ myval | each: '{\"height\":{{el}}}' | json | output }}"}), json!({"myval": [5.1,4.2]}), json!({"this":[{"height": 5.1},{"height": 4.2}]}))]
    #[case(json!({"recipients" : "{{ to | each: '{ \"email\": \"{{ el }}\" }' | json | output }}"}), json!({"to": ["john@example.com", "jane@example.com"]}), json!({"recipients": [
      {
        "email": "john@example.com"
      },
      {
        "email": "jane@example.com"
      }
    ]}))]
    fn filters(
        #[case] template: Value,
        #[case] data: Value,
        #[case] expected: Value,
    ) -> Result<()> {
        let tmpl = LiquidJson::new(template);
        let actual = tmpl.render(&data)?;
        assert_eq!(actual, expected);
        Ok(())
    }
}