Skip to main content

earl_core/
render.rs

1use std::collections::BTreeMap;
2
3use anyhow::Result;
4use serde_json::Value;
5
6/// Trait abstracting template rendering so protocol crates can render
7/// strings and JSON values without depending on a specific engine.
8pub trait TemplateRenderer {
9    /// Render a template string with the given context, returning the raw string.
10    fn render_str(&self, template: &str, context: &Value) -> Result<String>;
11
12    /// Render a JSON value, recursively expanding any template strings.
13    fn render_value(&self, value: &Value, context: &Value) -> Result<Value>;
14}
15
16/// Render a `BTreeMap<String, Value>` of template key-value pairs into a
17/// flat list of `(String, String)` pairs, expanding arrays into multiple
18/// entries with the same key.
19pub fn render_key_value_map(
20    input: Option<&BTreeMap<String, Value>>,
21    context: &Value,
22    renderer: &dyn TemplateRenderer,
23) -> Result<Vec<(String, String)>> {
24    let mut out = Vec::new();
25    let Some(input) = input else {
26        return Ok(out);
27    };
28
29    for (key, value) in input {
30        let rendered_key = renderer.render_str(key, context)?;
31        let rendered_value = renderer.render_value(value, context)?;
32
33        match rendered_value {
34            Value::Null => {} // Absent optional params render to null; skip them so they are omitted from the request.
35            Value::Array(values) => {
36                for value in values {
37                    let s = value_to_string(value)?;
38                    if !s.is_empty() {
39                        out.push((rendered_key.clone(), s));
40                    }
41                }
42            }
43            other => {
44                let s = value_to_string(other)?;
45                // Empty strings are treated as absent in query/header maps — same
46                // policy as null, since `default = ""` was the old workaround for
47                // optional params that are now handled via null-skipping.
48                if !s.is_empty() {
49                    out.push((rendered_key, s));
50                }
51            }
52        }
53    }
54
55    Ok(out)
56}
57
58/// Convert a `serde_json::Value` into its string representation.
59pub fn value_to_string(value: Value) -> Result<String> {
60    let out = match value {
61        Value::Null => String::new(),
62        Value::Bool(v) => v.to_string(),
63        Value::Number(v) => v.to_string(),
64        Value::String(v) => v,
65        Value::Array(_) | Value::Object(_) => serde_json::to_string(&value)?,
66    };
67    Ok(out)
68}