lmn_core/request_template/
renderer.rs1use 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
13enum Segment {
17 Static(Arc<str>),
19 Placeholder(String),
21}
22
23pub struct CompiledTemplate {
31 segments: Vec<Segment>,
32}
33
34impl CompiledTemplate {
35 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 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 _ => {
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
122pub trait PlaceholderHandler {
131 fn matches(&self, ph: &PlaceholderRef) -> bool;
133
134 fn resolve(
137 &self,
138 body: &Value,
139 ctx: &GeneratorContext,
140 ) -> Result<HashMap<String, Arc<str>>, TemplateError>;
141
142 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 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#[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#[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
223fn 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
246pub fn resolve_string_placeholders(
262 input: &str,
263 ctx: &GeneratorContext,
264 rng: &mut impl Rng,
265) -> String {
266 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 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 let wrapped = format!("{{{{{placeholder_body}}}}}");
284 let resolved_value = if let Some(ph) = parse_placeholder(&wrapped) {
285 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 String::new()
304 };
305 output.push_str(&resolved_value);
306 remaining = &after_open[close_offset + 2..];
307 }
308 None => {
309 output.push_str("{{");
311 remaining = after_open;
312 }
313 }
314 }
315
316 output.push_str(remaining);
318 output
319}
320
321#[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(()); }
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#[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 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}