Skip to main content

lmn_core/request_template/
renderer.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use rand::Rng;
5use serde_json::Value;
6use tracing::instrument;
7
8use crate::request_template::definition::TemplateDef;
9use crate::request_template::error::TemplateError;
10use crate::request_template::generator::GeneratorContext;
11use crate::request_template::{ENV_PLACEHOLDER_PREFIX, PlaceholderRef, parse_placeholder};
12
13// ── Segment ───────────────────────────────────────────────────────────────────
14
15/// A pre-compiled unit of a template body.
16enum Segment {
17    /// Pre-serialized JSON fragment written verbatim into the output buffer at render time.
18    Static(Arc<str>),
19    /// Placeholder name to resolve at render time.
20    Placeholder(String),
21}
22
23// ── CompiledTemplate ──────────────────────────────────────────────────────────
24
25/// A template body compiled into a flat list of `Segment`s.
26///
27/// Created once at parse time via [`CompiledTemplate::compile`]; rendered
28/// on every request via [`CompiledTemplate::render`] without revisiting the
29/// original `Value` tree.
30pub struct CompiledTemplate {
31    segments: Vec<Segment>,
32}
33
34impl CompiledTemplate {
35    /// Walks the `Value` tree depth-first and compiles it into a flat list of
36    /// `Segment`s. Done once at parse time so render time only iterates
37    /// the segment list.
38    pub fn compile(body: &Value) -> Result<Self, TemplateError> {
39        let mut segments = Vec::new();
40        compile_value(body, &mut segments)?;
41        Ok(Self { segments })
42    }
43
44    /// Iterates the compiled segments and writes static bytes and resolved
45    /// placeholder values into a `String` buffer.
46    ///
47    /// For each placeholder:
48    /// - If the name is present in `ctx.resolved` (pre-serialized `:global` or `ENV:`
49    ///   value), it is written verbatim.
50    /// - Otherwise, a fresh value is generated via `ctx.generate_by_name` and
51    ///   serialized inline.
52    pub fn render(
53        &self,
54        ctx: &GeneratorContext,
55        rng: &mut impl Rng,
56    ) -> Result<String, TemplateError> {
57        let mut buf = String::new();
58        for segment in &self.segments {
59            match segment {
60                Segment::Static(s) => buf.push_str(s),
61                Segment::Placeholder(name) => {
62                    if let Some(precomputed) = ctx.resolved.get(name) {
63                        buf.push_str(precomputed);
64                    } else {
65                        let val = ctx.generate_by_name(name, rng);
66                        buf.push_str(
67                            &serde_json::to_string(&val).map_err(TemplateError::Serialization)?,
68                        );
69                    }
70                }
71            }
72        }
73        Ok(buf)
74    }
75}
76
77fn compile_value(value: &Value, out: &mut Vec<Segment>) -> Result<(), TemplateError> {
78    match value {
79        Value::String(s) => {
80            if let Some(ph) = parse_placeholder(s) {
81                out.push(Segment::Placeholder(ph.name));
82            } else {
83                let serialized = serde_json::to_string(s).map_err(TemplateError::Serialization)?;
84                out.push(Segment::Static(Arc::from(serialized.as_str())));
85            }
86        }
87        Value::Object(map) => {
88            out.push(Segment::Static(Arc::from("{")));
89            let mut first = true;
90            for (key, val) in map {
91                if !first {
92                    out.push(Segment::Static(Arc::from(",")));
93                }
94                first = false;
95                let key_json = serde_json::to_string(key).map_err(TemplateError::Serialization)?;
96                out.push(Segment::Static(Arc::from(format!("{key_json}:").as_str())));
97                compile_value(val, out)?;
98            }
99            out.push(Segment::Static(Arc::from("}")));
100        }
101        Value::Array(arr) => {
102            out.push(Segment::Static(Arc::from("[")));
103            let mut first = true;
104            for val in arr {
105                if !first {
106                    out.push(Segment::Static(Arc::from(",")));
107                }
108                first = false;
109                compile_value(val, out)?;
110            }
111            out.push(Segment::Static(Arc::from("]")));
112        }
113        // Number, Bool, Null — serialize as-is
114        _ => {
115            let serialized = serde_json::to_string(value).map_err(TemplateError::Serialization)?;
116            out.push(Segment::Static(Arc::from(serialized.as_str())));
117        }
118    }
119    Ok(())
120}
121
122// ── PlaceholderHandler trait ──────────────────────────────────────────────────
123
124/// A strategy for pre-resolving a class of placeholders before any requests fire.
125///
126/// Implementors declare which placeholders they handle via `matches` and
127/// compute a map of `name → Arc<str>` (pre-serialized JSON) via `resolve`.
128/// The default `collect_names` and `walk` methods traverse the body tree
129/// using `matches` to find relevant placeholder names.
130pub trait PlaceholderHandler {
131    /// Returns `true` if this handler is responsible for the given placeholder.
132    fn matches(&self, ph: &PlaceholderRef) -> bool;
133
134    /// Walks `body`, collects all matching placeholder names, and resolves them
135    /// to pre-serialized JSON values. Called once at template parse time.
136    fn resolve(
137        &self,
138        body: &Value,
139        ctx: &GeneratorContext,
140    ) -> Result<HashMap<String, Arc<str>>, TemplateError>;
141
142    /// Collects all placeholder names in `body` that this handler matches.
143    /// Deduplicated and sorted for determinism.
144    fn collect_names(&self, body: &Value) -> Vec<String> {
145        let mut names = Vec::new();
146        self.walk(body, &mut names);
147        names.sort();
148        names.dedup();
149        names
150    }
151
152    /// Recursively walks `value`, pushing matching placeholder names into `names`.
153    fn walk(&self, value: &Value, names: &mut Vec<String>) {
154        match value {
155            Value::String(s) => {
156                if let Some(ph) = parse_placeholder(s)
157                    && self.matches(&ph)
158                {
159                    names.push(ph.name);
160                }
161            }
162            Value::Object(map) => map.values().for_each(|v| self.walk(v, names)),
163            Value::Array(arr) => arr.iter().for_each(|v| self.walk(v, names)),
164            _ => {}
165        }
166    }
167}
168
169// ── GlobalPlaceholderHandler ──────────────────────────────────────────────────
170
171/// Handles `:global` placeholders — non-ENV placeholders that carry the
172/// `:global` suffix. Their value is generated once at startup and reused
173/// across all requests in the run.
174#[derive(Debug)]
175pub struct GlobalPlaceholderHandler;
176
177impl PlaceholderHandler for GlobalPlaceholderHandler {
178    fn matches(&self, ph: &PlaceholderRef) -> bool {
179        ph.global && !ph.name.starts_with(ENV_PLACEHOLDER_PREFIX)
180    }
181
182    fn resolve(
183        &self,
184        body: &Value,
185        ctx: &GeneratorContext,
186    ) -> Result<HashMap<String, Arc<str>>, TemplateError> {
187        let names = self.collect_names(body);
188        let mut rng = rand::rng();
189        names
190            .into_iter()
191            .map(|n| {
192                let val = ctx.generate_by_name(&n, &mut rng);
193                let serialized =
194                    serde_json::to_string(&val).map_err(TemplateError::Serialization)?;
195                Ok((n, Arc::from(serialized.as_str())))
196            })
197            .collect()
198    }
199}
200
201// ── EnvPlaceholderHandler ─────────────────────────────────────────────────────
202
203/// Handles `ENV:` placeholders — reads named environment variables at template
204/// parse time (fail-closed: missing vars are an error).
205#[derive(Debug)]
206pub struct EnvPlaceholderHandler;
207
208impl PlaceholderHandler for EnvPlaceholderHandler {
209    fn matches(&self, ph: &PlaceholderRef) -> bool {
210        ph.name.starts_with(ENV_PLACEHOLDER_PREFIX)
211    }
212
213    fn resolve(
214        &self,
215        body: &Value,
216        _ctx: &GeneratorContext,
217    ) -> Result<HashMap<String, Arc<str>>, TemplateError> {
218        let names = self.collect_names(body);
219        resolve_env_vars(&names)
220    }
221}
222
223/// For each name like `"ENV:MY_TOKEN"`, reads the env var after the `ENV:` prefix
224/// and pre-serializes its value as a JSON string literal.
225/// Returns `Err(TemplateError::MissingEnvVar)` if any variable is not set.
226/// Returns `Err(TemplateError::InvalidEnvVarName)` if the var name portion is empty.
227fn resolve_env_vars(names: &[String]) -> Result<HashMap<String, Arc<str>>, TemplateError> {
228    let mut map = HashMap::new();
229    for name in names {
230        let var_name = &name[ENV_PLACEHOLDER_PREFIX.len()..];
231        if var_name.is_empty() {
232            return Err(TemplateError::InvalidEnvVarName(name.to_string()));
233        }
234        match std::env::var(var_name) {
235            Ok(val) => {
236                let serialized =
237                    serde_json::to_string(&val).map_err(TemplateError::Serialization)?;
238                map.insert(name.clone(), Arc::from(serialized.as_str()));
239            }
240            Err(_) => return Err(TemplateError::MissingEnvVar(var_name.to_string())),
241        }
242    }
243    Ok(map)
244}
245
246// ── resolve_string_placeholders ───────────────────────────────────────────────
247
248/// Resolves `{{placeholder_name}}` patterns in a raw string by calling the
249/// corresponding generator from `ctx`.
250///
251/// Unlike [`CompiledTemplate::render`], which operates on pre-compiled segments,
252/// this function works directly on a string — useful for resolving placeholders
253/// in header values or other non-body string fields.
254///
255/// Each placeholder is resolved to its display form (i.e. the raw string value,
256/// without surrounding JSON quotes). Unknown placeholders are replaced with an
257/// empty string.
258///
259/// Single-pass only — the generated values are NOT scanned again for
260/// `{{...}}` patterns.
261pub fn resolve_string_placeholders(
262    input: &str,
263    ctx: &GeneratorContext,
264    rng: &mut impl Rng,
265) -> String {
266    // Fast path: no `{{` in input — return a clone without any scanning.
267    if !input.contains("{{") {
268        return input.to_string();
269    }
270
271    let mut output = String::with_capacity(input.len());
272    let mut remaining = input;
273
274    while let Some(open) = remaining.find("{{") {
275        // Push everything before the opening `{{`
276        output.push_str(&remaining[..open]);
277        let after_open = &remaining[open + 2..];
278
279        match after_open.find("}}") {
280            Some(close_offset) => {
281                let placeholder_body = &after_open[..close_offset];
282                // Reuse the existing parse_placeholder logic by wrapping in `{{...}}`
283                let wrapped = format!("{{{{{placeholder_body}}}}}");
284                let resolved_value = if let Some(ph) = parse_placeholder(&wrapped) {
285                    // Check pre-resolved values first (`:global` / `ENV:`).
286                    // Pre-resolved values are JSON literals — unwrap string quotes
287                    // for display in a header value.
288                    if let Some(serialized) = ctx.resolved.get(&ph.name) {
289                        match serde_json::from_str::<Value>(serialized) {
290                            Ok(Value::String(s)) => s,
291                            Ok(other) => other.to_string(),
292                            Err(_) => serialized.to_string(),
293                        }
294                    } else {
295                        let val = ctx.generate_by_name(&ph.name, rng);
296                        match val {
297                            Value::String(s) => s,
298                            other => other.to_string(),
299                        }
300                    }
301                } else {
302                    // Empty or malformed placeholder — emit empty string
303                    String::new()
304                };
305                output.push_str(&resolved_value);
306                remaining = &after_open[close_offset + 2..];
307            }
308            None => {
309                // No closing `}}` — copy `{{` literally and continue
310                output.push_str("{{");
311                remaining = after_open;
312            }
313        }
314    }
315
316    // Append any trailing content after the last placeholder
317    output.push_str(remaining);
318    output
319}
320
321/// Validates that every `{{name}}` placeholder in the body has a corresponding
322/// definition. Returns an error naming the first unknown placeholder found.
323#[instrument(name = "lmn.template.validate_placeholders", skip(body, defs), fields(def_count = defs.len()))]
324pub fn validate_placeholders(
325    body: &Value,
326    defs: &HashMap<String, TemplateDef>,
327) -> Result<(), TemplateError> {
328    walk_strings(body, &mut |s| {
329        if let Some(ph) = parse_placeholder(s) {
330            if ph.name.starts_with(ENV_PLACEHOLDER_PREFIX) {
331                return Ok(()); // built-in, no def required
332            }
333            if !defs.contains_key(&ph.name) {
334                return Err(TemplateError::UnknownPlaceholder(ph.name));
335            }
336        }
337        Ok(())
338    })
339}
340
341fn walk_strings<F>(value: &Value, f: &mut F) -> Result<(), TemplateError>
342where
343    F: FnMut(&str) -> Result<(), TemplateError>,
344{
345    match value {
346        Value::String(s) => f(s),
347        Value::Object(map) => {
348            for v in map.values() {
349                walk_strings(v, f)?;
350            }
351            Ok(())
352        }
353        Value::Array(arr) => {
354            for v in arr {
355                walk_strings(v, f)?;
356            }
357            Ok(())
358        }
359        _ => Ok(()),
360    }
361}
362
363// ── Tests ─────────────────────────────────────────────────────────────────────
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::request_template::definition::{FloatDef, FloatStrategy, TemplateDef};
369    use crate::request_template::generator::GeneratorContext;
370
371    fn make_ctx_with_float(name: &str, value: f64) -> GeneratorContext {
372        let mut defs = HashMap::new();
373        defs.insert(
374            name.to_string(),
375            TemplateDef::Float(FloatDef {
376                strategy: FloatStrategy::Exact(value),
377                decimals: 2,
378            }),
379        );
380        GeneratorContext::new(defs)
381    }
382
383    fn make_ctx_with_choice(name: &str, choices: Vec<String>) -> GeneratorContext {
384        use crate::request_template::definition::{StringDef, StringStrategy};
385        let mut defs = HashMap::new();
386        defs.insert(
387            name.to_string(),
388            TemplateDef::String(StringDef {
389                strategy: StringStrategy::Choice(choices),
390            }),
391        );
392        GeneratorContext::new(defs)
393    }
394
395    #[test]
396    fn no_placeholder_returns_input_unchanged() {
397        let ctx = GeneratorContext::new(HashMap::new());
398        let result = resolve_string_placeholders("plain-header-value", &ctx, &mut rand::rng());
399        assert_eq!(result, "plain-header-value");
400    }
401
402    #[test]
403    fn resolves_choice_placeholder_without_quotes() {
404        let ctx = make_ctx_with_choice("user_id", vec!["alice".to_string()]);
405        let result = resolve_string_placeholders("user-{{user_id}}", &ctx, &mut rand::rng());
406        assert_eq!(result, "user-alice");
407    }
408
409    #[test]
410    fn resolves_float_placeholder() {
411        let ctx = make_ctx_with_float("amount", 9.99);
412        let result = resolve_string_placeholders("val={{amount}}", &ctx, &mut rand::rng());
413        assert_eq!(result, "val=9.99");
414    }
415
416    #[test]
417    fn resolves_multiple_placeholders_in_string() {
418        use crate::request_template::definition::{StringDef, StringStrategy};
419        let mut defs = HashMap::new();
420        defs.insert(
421            "a".to_string(),
422            TemplateDef::String(StringDef {
423                strategy: StringStrategy::Choice(vec!["foo".to_string()]),
424            }),
425        );
426        defs.insert(
427            "b".to_string(),
428            TemplateDef::String(StringDef {
429                strategy: StringStrategy::Choice(vec!["bar".to_string()]),
430            }),
431        );
432        let ctx = GeneratorContext::new(defs);
433        let result = resolve_string_placeholders("{{a}}-{{b}}", &ctx, &mut rand::rng());
434        assert_eq!(result, "foo-bar");
435    }
436
437    #[test]
438    fn unknown_placeholder_resolves_to_null_string() {
439        let ctx = GeneratorContext::new(HashMap::new());
440        let result =
441            resolve_string_placeholders("prefix-{{unknown}}-suffix", &ctx, &mut rand::rng());
442        assert_eq!(result, "prefix-null-suffix");
443    }
444
445    #[test]
446    fn unclosed_braces_preserved_literally() {
447        let ctx = GeneratorContext::new(HashMap::new());
448        let result = resolve_string_placeholders("{{unclosed", &ctx, &mut rand::rng());
449        assert_eq!(result, "{{unclosed");
450    }
451
452    #[test]
453    fn validate_placeholders_skips_env_prefixed_names() {
454        let body = Value::Object({
455            let mut m = serde_json::Map::new();
456            m.insert(
457                "token".to_string(),
458                Value::String("{{ENV:MY_VAR}}".to_string()),
459            );
460            m
461        });
462        let defs = HashMap::new();
463        assert!(validate_placeholders(&body, &defs).is_ok());
464    }
465
466    #[test]
467    fn compile_static_string_emits_json_quoted() {
468        let compiled = CompiledTemplate::compile(&serde_json::json!("hello")).unwrap();
469        assert_eq!(compiled.segments.len(), 1);
470        if let Segment::Static(s) = &compiled.segments[0] {
471            assert_eq!(s.as_ref(), "\"hello\"");
472        } else {
473            panic!("expected Static segment");
474        }
475    }
476
477    #[test]
478    fn compile_placeholder_string_emits_placeholder() {
479        let compiled = CompiledTemplate::compile(&serde_json::json!("{{val}}")).unwrap();
480        assert_eq!(compiled.segments.len(), 1);
481        if let Segment::Placeholder(name) = &compiled.segments[0] {
482            assert_eq!(name, "val");
483        } else {
484            panic!("expected Placeholder segment");
485        }
486    }
487
488    #[test]
489    fn compile_object_emits_braces_and_key() {
490        let compiled = CompiledTemplate::compile(&serde_json::json!({ "k": "v" })).unwrap();
491        // Expected: Static("{"), Static("\"k\":"), Static("\"v\""), Static("}")
492        assert!(compiled.segments.len() >= 3);
493    }
494
495    #[test]
496    fn compile_empty_object_roundtrips() {
497        let compiled = CompiledTemplate::compile(&serde_json::json!({})).unwrap();
498        let ctx = GeneratorContext::new(HashMap::new());
499        let result = compiled.render(&ctx, &mut rand::rng()).unwrap();
500        assert_eq!(result, "{}");
501    }
502
503    #[test]
504    fn compile_empty_array_roundtrips() {
505        let compiled = CompiledTemplate::compile(&serde_json::json!([])).unwrap();
506        let ctx = GeneratorContext::new(HashMap::new());
507        let result = compiled.render(&ctx, &mut rand::rng()).unwrap();
508        assert_eq!(result, "[]");
509    }
510
511    #[test]
512    fn compile_array_with_placeholder_renders_correctly() {
513        let ctx = make_ctx_with_float("val", 1.0);
514        let compiled =
515            CompiledTemplate::compile(&serde_json::json!(["static", "{{val}}"])).unwrap();
516        let result: serde_json::Value =
517            serde_json::from_str(&compiled.render(&ctx, &mut rand::rng()).unwrap()).unwrap();
518        assert_eq!(result[0], serde_json::json!("static"));
519        assert!(result[1].is_number());
520    }
521
522    #[test]
523    fn compile_special_chars_in_string_are_escaped() {
524        let compiled =
525            CompiledTemplate::compile(&serde_json::json!({"key": "hello \"world\"\nnewline"}))
526                .unwrap();
527        let ctx = GeneratorContext::new(HashMap::new());
528        let output = compiled.render(&ctx, &mut rand::rng()).unwrap();
529        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
530        assert_eq!(parsed["key"], serde_json::json!("hello \"world\"\nnewline"));
531    }
532
533    #[test]
534    fn compile_deeply_nested_renders_correctly() {
535        let ctx = make_ctx_with_float("price", 5.0);
536        let compiled =
537            CompiledTemplate::compile(&serde_json::json!({ "a": { "b": { "c": "{{price}}" } } }))
538                .unwrap();
539        let result: serde_json::Value =
540            serde_json::from_str(&compiled.render(&ctx, &mut rand::rng()).unwrap()).unwrap();
541        assert!(result["a"]["b"]["c"].is_number());
542    }
543
544    #[test]
545    fn render_substitutes_placeholder() {
546        let ctx = make_ctx_with_float("val", 42.0);
547        let compiled =
548            CompiledTemplate::compile(&serde_json::json!({ "field": "{{val}}" })).unwrap();
549        let result: serde_json::Value =
550            serde_json::from_str(&compiled.render(&ctx, &mut rand::rng()).unwrap()).unwrap();
551        assert!(result["field"].is_number());
552    }
553
554    #[test]
555    fn render_leaves_plain_string_unchanged() {
556        let ctx = GeneratorContext::new(HashMap::new());
557        let compiled = CompiledTemplate::compile(&serde_json::json!({ "field": "plain" })).unwrap();
558        let result: serde_json::Value =
559            serde_json::from_str(&compiled.render(&ctx, &mut rand::rng()).unwrap()).unwrap();
560        assert_eq!(result["field"], serde_json::json!("plain"));
561    }
562
563    #[test]
564    fn render_handles_nested_objects() {
565        let ctx = make_ctx_with_float("price", 10.0);
566        let compiled =
567            CompiledTemplate::compile(&serde_json::json!({ "order": { "price": "{{price}}" } }))
568                .unwrap();
569        let result: serde_json::Value =
570            serde_json::from_str(&compiled.render(&ctx, &mut rand::rng()).unwrap()).unwrap();
571        assert!(result["order"]["price"].is_number());
572    }
573
574    #[test]
575    fn render_uses_preresolved_value() {
576        let ctx = GeneratorContext::new(HashMap::new())
577            .with_resolved([("x".to_string(), Arc::from("99"))].into_iter().collect());
578        let compiled = CompiledTemplate::compile(&serde_json::json!({ "field": "{{x}}" })).unwrap();
579        let result: serde_json::Value =
580            serde_json::from_str(&compiled.render(&ctx, &mut rand::rng()).unwrap()).unwrap();
581        assert_eq!(result["field"], serde_json::json!(99));
582    }
583
584    #[test]
585    fn resolve_string_uses_preresolved_env_value() {
586        let ctx = GeneratorContext::new(HashMap::new()).with_resolved(
587            [("ENV:TOKEN".to_string(), Arc::from("\"mysecret\""))]
588                .into_iter()
589                .collect(),
590        );
591        let result = resolve_string_placeholders("Bearer {{ENV:TOKEN}}", &ctx, &mut rand::rng());
592        assert_eq!(result, "Bearer mysecret");
593    }
594
595    #[test]
596    fn global_handler_finds_global_placeholders() {
597        use serde_json::json;
598        let body = json!({ "a": "{{x:global}}", "b": "{{y}}", "c": "{{x:global}}" });
599        let handler = GlobalPlaceholderHandler;
600        let names = handler.collect_names(&body);
601        assert_eq!(names, vec!["x"]);
602    }
603
604    #[test]
605    fn global_handler_returns_empty_when_none() {
606        use serde_json::json;
607        let body = json!({ "a": "{{x}}", "b": "plain" });
608        let handler = GlobalPlaceholderHandler;
609        assert!(handler.collect_names(&body).is_empty());
610    }
611}