lmn_core/request_template/
mod.rs1pub mod definition;
2pub mod error;
3pub mod generator;
4mod generators;
5pub mod renderer;
6mod validators;
7
8use std::collections::HashMap;
9use std::path::Path;
10
11use serde_json::Value;
12use tracing::instrument;
13
14pub use error::TemplateError;
15use generator::GeneratorContext;
16
17const METADATA_KEY: &str = "_lmn_metadata_templates";
18pub(crate) const ENV_PLACEHOLDER_PREFIX: &str = "ENV:";
19
20pub struct PlaceholderRef {
23 pub name: String,
24 pub once: bool,
25}
26
27pub fn parse_placeholder(s: &str) -> Option<PlaceholderRef> {
30 let inner = s.trim().strip_prefix("{{")?.strip_suffix("}}")?;
31 if inner.is_empty() {
32 return None;
33 }
34 let (name, once) = match inner.strip_suffix(":once") {
35 Some(n) => (n, true),
36 None => (inner, false),
37 };
38 if name.is_empty() {
39 return None;
40 }
41 Some(PlaceholderRef {
42 name: name.to_string(),
43 once,
44 })
45}
46
47fn collect_env_placeholder_names(body: &Value) -> Vec<String> {
52 let mut names = Vec::new();
53 collect_env(body, &mut names);
54 names.sort();
55 names.dedup();
56 names
57}
58
59fn collect_env(value: &Value, names: &mut Vec<String>) {
60 match value {
61 Value::String(s) => {
62 if let Some(ph) = parse_placeholder(s)
63 && ph.name.starts_with(ENV_PLACEHOLDER_PREFIX)
64 {
65 names.push(ph.name);
66 }
67 }
68 Value::Object(map) => map.values().for_each(|v| collect_env(v, names)),
69 Value::Array(arr) => arr.iter().for_each(|v| collect_env(v, names)),
70 _ => {}
71 }
72}
73
74fn resolve_env_vars(names: &[String]) -> Result<HashMap<String, Value>, TemplateError> {
78 let mut map = HashMap::new();
79 for name in names {
80 let var_name = &name[ENV_PLACEHOLDER_PREFIX.len()..];
81 if var_name.is_empty() {
82 return Err(TemplateError::InvalidEnvVarName(name.to_string()));
83 }
84 match std::env::var(var_name) {
85 Ok(val) => {
86 map.insert(name.clone(), Value::String(val));
87 }
88 Err(_) => return Err(TemplateError::MissingEnvVar(var_name.to_string())),
89 }
90 }
91 Ok(map)
92}
93
94pub struct Template {
97 body: Value,
98 context: GeneratorContext,
99}
100
101impl Template {
102 #[instrument(name = "lmn.template.parse", fields(path = %path.display()))]
105 pub fn parse(path: &Path) -> Result<Self, TemplateError> {
106 let content = std::fs::read_to_string(path)?;
107 let mut root: serde_json::Map<String, Value> = serde_json::from_str(&content)?;
108
109 let metadata = root
111 .remove(METADATA_KEY)
112 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
113
114 let raw_defs: HashMap<String, definition::RawTemplateDef> =
115 serde_json::from_value(metadata)?;
116 let defs = definition::validate_all(raw_defs)?;
117
118 let body = Value::Object(root);
119
120 renderer::validate_placeholders(&body, &defs)?;
122
123 definition::check_circular_refs(&defs)?;
125
126 let ctx = GeneratorContext::new(defs);
128 let mut rng = rand::rng();
129
130 let once_values: HashMap<String, Value> = renderer::collect_once_placeholder_names(&body)
131 .into_iter()
132 .map(|name| {
133 let val = ctx.generate_by_name(&name, &mut rng);
134 (name, val)
135 })
136 .collect();
137
138 let env_names = collect_env_placeholder_names(&body);
140 let env_values = resolve_env_vars(&env_names)?;
141
142 let mut all_once_values = once_values;
144 all_once_values.extend(env_values);
145
146 Ok(Template {
147 body,
148 context: ctx.with_once_values(all_once_values),
149 })
150 }
151
152 #[instrument(name = "lmn.template.render", skip(self), fields(n))]
155 pub fn pre_generate(&self, n: usize) -> Vec<String> {
156 let mut rng = rand::rng();
157 (0..n)
158 .map(|_| {
159 let rendered = renderer::render(&self.body, &self.context, &mut rng);
160 serde_json::to_string(&rendered).expect("rendered Value is always valid JSON")
161 })
162 .collect()
163 }
164
165 pub fn generate_one(&self) -> String {
169 let mut rng = rand::rng();
170 let rendered = renderer::render(&self.body, &self.context, &mut rng);
171 serde_json::to_string(&rendered).expect("rendered Value is always valid JSON")
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use std::io::Write;
179
180 fn write_temp(content: &str) -> tempfile::NamedTempFile {
181 let mut f = tempfile::NamedTempFile::new().unwrap();
182 f.write_all(content.as_bytes()).unwrap();
183 f
184 }
185
186 #[test]
187 fn parse_fails_on_missing_file() {
188 assert!(Template::parse(Path::new("nonexistent.json")).is_err());
189 }
190
191 #[test]
192 fn parse_fails_on_invalid_json() {
193 let f = write_temp("not json");
194 assert!(Template::parse(f.path()).is_err());
195 }
196
197 #[test]
198 fn parse_fails_on_unknown_placeholder() {
199 let f = write_temp(r#"{"field": "{{undefined}}"}"#);
200 assert!(Template::parse(f.path()).is_err());
201 }
202
203 #[test]
204 fn parse_fails_on_circular_reference() {
205 let f = write_temp(
206 r#"{
207 "field": "{{a}}",
208 "_lmn_metadata_templates": {
209 "a": { "type": "object", "composition": { "x": "{{b}}" } },
210 "b": { "type": "object", "composition": { "y": "{{a}}" } }
211 }
212 }"#,
213 );
214 assert!(Template::parse(f.path()).is_err());
215 }
216
217 #[test]
218 fn parse_succeeds_with_no_placeholders() {
219 let f = write_temp(r#"{"field": "static"}"#);
220 assert!(Template::parse(f.path()).is_ok());
221 }
222
223 #[test]
225 fn generate_one_returns_valid_json() {
226 let f = write_temp(r#"{"field": "static", "value": 42}"#);
227 let template = Template::parse(f.path()).unwrap();
228 let result = template.generate_one();
229 let parsed: serde_json::Value =
231 serde_json::from_str(&result).expect("generate_one must return valid JSON");
232 assert_eq!(
233 parsed["field"],
234 serde_json::Value::String("static".to_string())
235 );
236 assert_eq!(parsed["value"], serde_json::json!(42));
237 }
238
239 #[test]
240 fn generate_one_is_independent_per_call() {
241 let f = write_temp(r#"{"field": "static"}"#);
243 let template = Template::parse(f.path()).unwrap();
244 let a = template.generate_one();
245 let b = template.generate_one();
246 assert!(serde_json::from_str::<serde_json::Value>(&a).is_ok());
247 assert!(serde_json::from_str::<serde_json::Value>(&b).is_ok());
248 }
249
250 #[test]
251 fn parse_env_placeholder_resolved_from_env() {
252 unsafe { std::env::set_var("LUMEN_TEST_TOKEN", "secret123") };
253 let f = write_temp(r#"{"token": "{{ENV:LUMEN_TEST_TOKEN}}"}"#);
254 let template = Template::parse(f.path()).unwrap();
255 let result = template.generate_one();
256 assert!(
257 result.contains("secret123"),
258 "expected 'secret123' in output, got: {result}"
259 );
260 }
261
262 #[test]
263 fn parse_env_placeholder_missing_var_is_error() {
264 unsafe { std::env::remove_var("LUMEN_NONEXISTENT_12345") };
266 let f = write_temp(r#"{"token": "{{ENV:LUMEN_NONEXISTENT_12345}}"}"#);
267 let result = Template::parse(f.path());
268 assert!(
269 result.is_err(),
270 "expected parse to fail for missing env var"
271 );
272 assert!(
273 matches!(result.err(), Some(TemplateError::MissingEnvVar(_))),
274 "expected MissingEnvVar error variant"
275 );
276 }
277
278 #[test]
279 fn parse_env_placeholder_no_def_required() {
280 unsafe { std::env::set_var("LUMEN_TEST_TOKEN", "anyvalue") };
282 let f =
283 write_temp(r#"{"token": "{{ENV:LUMEN_TEST_TOKEN}}", "_lmn_metadata_templates": {}}"#);
284 assert!(Template::parse(f.path()).is_ok());
285 }
286
287 #[test]
288 fn parse_env_placeholder_with_once_suffix_resolves_correctly() {
289 unsafe { std::env::set_var("LUMEN_TEST_ONCE_TOKEN", "once_secret_value") };
294 let f = write_temp(r#"{"token": "{{ENV:LUMEN_TEST_ONCE_TOKEN:once}}"}"#);
295 let template = Template::parse(f.path()).unwrap();
296 let result = template.generate_one();
297 assert!(
298 result.contains("once_secret_value"),
299 "expected 'once_secret_value' in output, got: {result}"
300 );
301 }
302
303 #[test]
304 fn parse_env_placeholder_empty_var_name_is_error() {
305 let f = write_temp(r#"{"token": "{{ENV:}}"}"#);
307 let result = Template::parse(f.path());
308 assert!(
309 result.is_err(),
310 "expected parse to fail for empty ENV var name"
311 );
312 assert!(
313 matches!(result.err(), Some(TemplateError::InvalidEnvVarName(_))),
314 "expected InvalidEnvVarName error variant"
315 );
316 }
317}