liquid_json/
lib.rs

1//! Liquid JSON templates
2#![doc = include_str!("../README.md")]
3#![allow(unknown_lints)]
4#![deny(
5    clippy::expect_used,
6    clippy::explicit_deref_methods,
7    clippy::option_if_let_else,
8    clippy::await_holding_lock,
9    clippy::cloned_instead_of_copied,
10    clippy::explicit_into_iter_loop,
11    clippy::flat_map_option,
12    clippy::fn_params_excessive_bools,
13    clippy::implicit_clone,
14    clippy::inefficient_to_string,
15    clippy::large_types_passed_by_value,
16    clippy::manual_ok_or,
17    clippy::map_flatten,
18    clippy::map_unwrap_or,
19    clippy::must_use_candidate,
20    clippy::needless_for_each,
21    clippy::needless_pass_by_value,
22    clippy::option_option,
23    clippy::redundant_else,
24    clippy::semicolon_if_nothing_returned,
25    clippy::too_many_lines,
26    clippy::trivially_copy_pass_by_ref,
27    clippy::unnested_or_patterns,
28    clippy::future_not_send,
29    clippy::useless_let_if_seq,
30    clippy::str_to_string,
31    clippy::inherent_to_string,
32    clippy::let_and_return,
33    clippy::string_to_string,
34    clippy::try_err,
35    clippy::unused_async,
36    clippy::missing_enforced_import_renames,
37    clippy::nonstandard_macro_braces,
38    clippy::rc_mutex,
39    clippy::unwrap_or_else_default,
40    clippy::manual_split_once,
41    clippy::derivable_impls,
42    clippy::needless_option_as_deref,
43    clippy::iter_not_returning_iterator,
44    clippy::same_name_method,
45    clippy::manual_assert,
46    clippy::non_send_fields_in_send_ty,
47    clippy::equatable_if_let,
48    bad_style,
49    clashing_extern_declarations,
50    dead_code,
51    deprecated,
52    explicit_outlives_requirements,
53    improper_ctypes,
54    invalid_value,
55    missing_copy_implementations,
56    missing_debug_implementations,
57    mutable_transmutes,
58    no_mangle_generic_items,
59    non_shorthand_field_patterns,
60    overflowing_literals,
61    path_statements,
62    patterns_in_fns_without_body,
63    private_in_public,
64    trivial_bounds,
65    trivial_casts,
66    trivial_numeric_casts,
67    type_alias_bounds,
68    unconditional_recursion,
69    unreachable_pub,
70    unsafe_code,
71    unstable_features,
72    unused,
73    unused_allocation,
74    unused_comparisons,
75    unused_import_braces,
76    unused_parens,
77    unused_qualifications,
78    while_true,
79    missing_docs
80)]
81#![allow(clippy::derive_partial_eq_without_eq, clippy::box_default)]
82
83mod error;
84mod filters;
85mod liquid_json;
86#[cfg(feature = "serde")]
87mod liquid_json_value;
88mod options;
89
90use std::sync::Arc;
91
92pub use error::Error;
93use liquid::{Parser, ValueView};
94use liquid_core::{
95    model::ScalarCow,
96    runtime::{RuntimeBuilder, Variable},
97    Language, Runtime,
98};
99#[cfg(feature = "serde")]
100pub use liquid_json_value::LiquidJsonValue;
101use once_cell::sync::Lazy;
102use serde_json::Number;
103
104pub use crate::liquid_json::LiquidJson;
105
106use self::options::OptionsBuilder;
107
108static PARSER: Lazy<Arc<Parser>> = Lazy::new(|| {
109    let builder = liquid::ParserBuilder::with_stdlib()
110        .filter(filters::Each::new())
111        .filter(filters::Output)
112        .filter(filters::Base64Decode)
113        .filter(filters::Base64Encode);
114    #[cfg(feature = "serde")]
115    let builder = builder.filter(filters::Json);
116    Arc::new(builder.build().unwrap())
117});
118
119static OPTIONS: Lazy<Arc<Language>> = Lazy::new(|| {
120    let builder = OptionsBuilder::new()
121        .stdlib()
122        .filter(filters::Each::new())
123        .filter(filters::Output)
124        .filter(filters::Base64Decode)
125        .filter(filters::Base64Encode);
126    #[cfg(feature = "serde")]
127    let builder = builder.filter(filters::Json);
128    builder.build()
129});
130
131/// Utility function to render a basic string with a [serde_json::Value] instead of dealing with [liquid::Object].
132pub fn render_string(template: &str, data: &serde_json::Value) -> Result<String, Error> {
133    let template = PARSER.parse(template)?;
134    let data = to_liquid_obj(data)?;
135    Ok(template.render(&data)?)
136}
137
138fn to_liquid_obj(value: &serde_json::Value) -> Result<liquid::Object, Error> {
139    // let mut obj = liquid::Object::new();
140    match value {
141        serde_json::Value::Object(v) => v
142            .into_iter()
143            .map(|(k, v)| {
144                Ok((
145                    liquid::model::KString::from_string(k.clone()),
146                    to_liquid_value(v)?,
147                ))
148            })
149            .collect::<Result<liquid::Object, Error>>(),
150        _ => Err(Error::InvalidContext(value.clone())),
151    }
152}
153
154fn to_liquid_value(value: &serde_json::Value) -> Result<liquid::model::Value, Error> {
155    Ok(match value {
156        serde_json::Value::Null => liquid::model::Value::Nil,
157        serde_json::Value::Bool(v) => liquid::model::Value::Scalar(liquid::model::Scalar::from(*v)),
158        serde_json::Value::Number(v) => {
159            if v.is_f64() {
160                liquid::model::Value::Scalar(liquid::model::Scalar::from(v.as_f64().unwrap()))
161            } else if v.is_i64() {
162                liquid::model::Value::Scalar(liquid::model::Scalar::from(v.as_i64().unwrap()))
163            } else {
164                let num = v.as_u64().unwrap();
165                if num < u32::MAX as u64 {
166                    liquid::model::Value::Scalar(liquid::model::Scalar::new(ScalarCow::new(
167                        v.as_u64().unwrap() as u32,
168                    )))
169                } else {
170                    return Err(Error::U64);
171                }
172            }
173        }
174        serde_json::Value::String(v) => {
175            liquid::model::Value::Scalar(liquid::model::Scalar::from(v.clone()))
176        }
177        serde_json::Value::Array(v) => {
178            liquid::model::Value::Array(v.iter().map(to_liquid_value).collect::<Result<_, _>>()?)
179        }
180        serde_json::Value::Object(v) => liquid::model::Value::Object(
181            v.into_iter()
182                .map(|(k, v)| {
183                    Ok((
184                        liquid::model::KString::from_string(k.clone()),
185                        to_liquid_value(v)?,
186                    ))
187                })
188                .collect::<Result<liquid::model::Object, Error>>()?,
189        ),
190    })
191}
192
193fn to_json_value(value: liquid::model::Value) -> serde_json::Value {
194    match value {
195        liquid::model::Value::Scalar(v) => {
196            // have to match on type name because liquid::model::Scalar doesn't expose its enum.
197            let name = v.type_name();
198            match name {
199                "string" => serde_json::Value::String(v.to_kstr().to_string()),
200                "whole number" => serde_json::Value::Number(Number::from(v.to_integer().unwrap())),
201                "fractional number" => {
202                    serde_json::Value::Number(Number::from_f64(v.to_float().unwrap()).unwrap())
203                }
204                "boolean" => serde_json::Value::Bool(v.to_bool().unwrap()),
205                _ => panic!("Unknown scalar type: {}", name),
206            }
207        }
208        liquid::model::Value::Array(v) => {
209            serde_json::Value::Array(v.into_iter().map(to_json_value).collect())
210        }
211        liquid::model::Value::Object(v) => serde_json::Value::Object(
212            v.into_iter()
213                .map(|(k, v)| (k.to_string(), to_json_value(v)))
214                .collect(),
215        ),
216        liquid::model::Value::State(_v) => panic!("State not supported"),
217        liquid::model::Value::Nil => serde_json::Value::Null,
218    }
219}
220
221static SINGLE_VALUE: Lazy<regex::Regex> =
222    Lazy::new(|| regex::Regex::new(r"^\{\{\s*(\w*)\s*\}\}$").unwrap());
223
224fn render_value(
225    value: &serde_json::Value,
226    data: &liquid::Object,
227) -> Result<serde_json::Value, Error> {
228    match value {
229        serde_json::Value::String(s) => {
230            // Special case: if the entire string is a single value, return that JSON value directly.
231            if let Some(cap) = SINGLE_VALUE.captures(s) {
232                let key = cap.get(1).unwrap().as_str();
233                if let Some(val) = data.get(key) {
234                    return Ok(to_json_value(val.clone()));
235                }
236            }
237            let mut output = Vec::new();
238            let runtime = RuntimeBuilder::new().set_globals(data).build();
239
240            let elements = liquid_core::parser::parse(s, &OPTIONS)?;
241            for element in elements {
242                element.render_to(&mut output, &runtime)?;
243            }
244            let sentinel = Variable::with_literal("__output__");
245            if let Some(output) = sentinel.try_evaluate(&runtime) {
246                if let Some(value) = runtime.try_get(&output) {
247                    return Ok(to_json_value(value.to_value()));
248                }
249            }
250            let output = String::from_utf8(output).unwrap();
251            Ok(serde_json::Value::String(output))
252        }
253        serde_json::Value::Array(a) => Ok(serde_json::Value::Array(
254            a.iter()
255                .map(|v| render_value(v, data))
256                .collect::<Result<Vec<serde_json::Value>, _>>()?,
257        )),
258        serde_json::Value::Object(o) => {
259            let map = o
260                .into_iter()
261                .map(|(k, v)| Ok((k.clone(), render_value(v, data)?)))
262                .collect::<Result<serde_json::Map<String, serde_json::Value>, Error>>()?;
263            Ok(serde_json::Value::Object(map))
264        }
265        _ => Ok(value.clone()),
266    }
267}