Skip to main content

lmn_core/request_template/
generator.rs

1use std::collections::HashMap;
2
3use rand::Rng;
4use serde_json::Value;
5use tracing::debug;
6
7use crate::request_template::definition::{ObjectDef, TemplateDef};
8use crate::request_template::generators::Generate;
9
10// ── Context ───────────────────────────────────────────────────────────────────
11
12pub struct GeneratorContext {
13    pub defs: HashMap<String, TemplateDef>,
14    pub once_values: HashMap<String, Value>,
15}
16
17impl GeneratorContext {
18    pub fn new(defs: HashMap<String, TemplateDef>) -> Self {
19        Self {
20            defs,
21            once_values: HashMap::new(),
22        }
23    }
24
25    pub fn with_once_values(self, once_values: HashMap<String, Value>) -> Self {
26        Self {
27            once_values,
28            ..self
29        }
30    }
31
32    /// Resolves a placeholder by name, returning a pre-computed `:once` value
33    /// if available, otherwise generating a fresh one.
34    pub fn resolve(&self, name: &str, rng: &mut impl Rng) -> Value {
35        if let Some(v) = self.once_values.get(name) {
36            return v.clone();
37        }
38        self.generate_by_name(name, rng)
39    }
40
41    pub(crate) fn generate_by_name(&self, name: &str, rng: &mut impl Rng) -> Value {
42        match self.defs.get(name) {
43            Some(def) => self.generate_def(def, rng),
44            None => {
45                debug!(placeholder = name, "unknown placeholder resolved to null");
46                Value::Null
47            }
48        }
49    }
50
51    pub fn generate_def(&self, def: &TemplateDef, rng: &mut impl Rng) -> Value {
52        match def {
53            TemplateDef::String(d) => d.generate(rng),
54            TemplateDef::Float(d) => d.generate(rng),
55            TemplateDef::Object(d) => self.generate_object(d, rng),
56        }
57    }
58
59    fn generate_object(&self, def: &ObjectDef, rng: &mut impl Rng) -> Value {
60        let map = def
61            .composition
62            .iter()
63            .map(|(field, ref_name)| (field.clone(), self.resolve(ref_name, rng)))
64            .collect();
65        Value::Object(map)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::request_template::definition::{FloatDef, FloatStrategy, ObjectDef, TemplateDef};
73
74    fn float_exact(v: f64) -> TemplateDef {
75        TemplateDef::Float(FloatDef {
76            strategy: FloatStrategy::Exact(v),
77            decimals: 0,
78        })
79    }
80
81    #[test]
82    fn generate_by_name_returns_null_for_unknown() {
83        let ctx = GeneratorContext::new(HashMap::new());
84        let val = ctx.generate_by_name("unknown", &mut rand::rng());
85        assert_eq!(val, Value::Null);
86    }
87
88    #[test]
89    fn generate_by_name_returns_value_for_known() {
90        let mut defs = HashMap::new();
91        defs.insert("price".to_string(), float_exact(10.0));
92        let ctx = GeneratorContext::new(defs);
93        let val = ctx.generate_by_name("price", &mut rand::rng());
94        assert!(val.is_number());
95    }
96
97    #[test]
98    fn generate_object_composes_fields() {
99        let mut defs = HashMap::new();
100        defs.insert("price".to_string(), float_exact(42.0));
101        let ctx = GeneratorContext::new(defs);
102        let obj = ObjectDef {
103            composition: [("amount".to_string(), "price".to_string())]
104                .into_iter()
105                .collect(),
106        };
107        let val = ctx.generate_object(&obj, &mut rand::rng());
108        assert!(val["amount"].is_number());
109    }
110}