Skip to main content

lmn_core/request_template/
generator.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use rand::Rng;
5use serde_json::Value;
6use tracing::debug;
7
8use crate::request_template::definition::{ObjectDef, TemplateDef};
9use crate::request_template::generators::Generate;
10
11// ── Context ───────────────────────────────────────────────────────────────────
12
13pub struct GeneratorContext {
14    pub defs: HashMap<String, TemplateDef>,
15    /// Pre-serialized JSON strings for `:global` and `ENV:` placeholders.
16    /// Values are stored as JSON literals (e.g. `"\"hello\""` for a string).
17    pub resolved: HashMap<String, Arc<str>>,
18}
19
20impl GeneratorContext {
21    pub fn new(defs: HashMap<String, TemplateDef>) -> Self {
22        Self {
23            defs,
24            resolved: HashMap::new(),
25        }
26    }
27
28    pub(crate) fn with_resolved(mut self, resolved: HashMap<String, Arc<str>>) -> Self {
29        self.resolved.extend(resolved);
30        self
31    }
32
33    pub(crate) fn generate_by_name(&self, name: &str, rng: &mut impl Rng) -> Value {
34        match self.defs.get(name) {
35            Some(def) => self.generate_def(def, rng),
36            None => {
37                debug!(placeholder = name, "unknown placeholder resolved to null");
38                Value::Null
39            }
40        }
41    }
42
43    pub fn generate_def(&self, def: &TemplateDef, rng: &mut impl Rng) -> Value {
44        match def {
45            TemplateDef::String(d) => d.generate(rng),
46            TemplateDef::Float(d) => d.generate(rng),
47            TemplateDef::Object(d) => self.generate_object(d, rng),
48        }
49    }
50
51    fn generate_object(&self, def: &ObjectDef, rng: &mut impl Rng) -> Value {
52        let map = def
53            .composition
54            .iter()
55            .map(|(field, ref_name)| (field.clone(), self.generate_by_name(ref_name, rng)))
56            .collect();
57        Value::Object(map)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::request_template::definition::{FloatDef, FloatStrategy, ObjectDef, TemplateDef};
65
66    fn float_exact(v: f64) -> TemplateDef {
67        TemplateDef::Float(FloatDef {
68            strategy: FloatStrategy::Exact(v),
69            decimals: 0,
70        })
71    }
72
73    #[test]
74    fn generate_by_name_returns_null_for_unknown() {
75        let ctx = GeneratorContext::new(HashMap::new());
76        let val = ctx.generate_by_name("unknown", &mut rand::rng());
77        assert_eq!(val, Value::Null);
78    }
79
80    #[test]
81    fn generate_by_name_returns_value_for_known() {
82        let mut defs = HashMap::new();
83        defs.insert("price".to_string(), float_exact(10.0));
84        let ctx = GeneratorContext::new(defs);
85        let val = ctx.generate_by_name("price", &mut rand::rng());
86        assert!(val.is_number());
87    }
88
89    #[test]
90    fn generate_object_composes_fields() {
91        let mut defs = HashMap::new();
92        defs.insert("price".to_string(), float_exact(42.0));
93        let ctx = GeneratorContext::new(defs);
94        let obj = ObjectDef {
95            composition: [("amount".to_string(), "price".to_string())]
96                .into_iter()
97                .collect(),
98        };
99        let val = ctx.generate_object(&obj, &mut rand::rng());
100        assert!(val["amount"].is_number());
101    }
102
103    #[test]
104    fn generate_object_unknown_field_returns_null() {
105        let ctx = GeneratorContext::new(HashMap::new());
106        let obj = ObjectDef {
107            composition: [("field".to_string(), "unknown".to_string())]
108                .into_iter()
109                .collect(),
110        };
111        let val = ctx.generate_object(&obj, &mut rand::rng());
112        assert_eq!(val["field"], Value::Null);
113    }
114
115    #[test]
116    fn with_resolved_merges_entries() {
117        let ctx = GeneratorContext::new(HashMap::new()).with_resolved(
118            [("key".to_string(), Arc::from("\"value\""))]
119                .into_iter()
120                .collect(),
121        );
122        assert!(ctx.resolved.contains_key("key"));
123    }
124}