lmn_core/request_template/
mod.rs1pub mod definition;
2pub mod error;
3pub mod generator;
4mod generators;
5pub mod renderer;
6mod validators;
7
8use std::path::Path;
9
10use serde_json::Value;
11use tracing::instrument;
12
13pub use error::TemplateError;
14use generator::GeneratorContext;
15use renderer::{
16 CompiledTemplate, EnvPlaceholderHandler, GlobalPlaceholderHandler, PlaceholderHandler,
17};
18
19const METADATA_KEY: &str = "_lmn_metadata_templates";
20pub(crate) const ENV_PLACEHOLDER_PREFIX: &str = "ENV:";
21
22pub struct PlaceholderRef {
25 pub name: String,
26 pub global: bool,
27}
28
29pub fn parse_placeholder(s: &str) -> Option<PlaceholderRef> {
32 let inner = s.trim().strip_prefix("{{")?.strip_suffix("}}")?;
33 if inner.is_empty() {
34 return None;
35 }
36 let (name, global) = match inner.strip_suffix(":global") {
37 Some(n) => (n, true),
38 None => (inner, false),
39 };
40 if name.is_empty() {
41 return None;
42 }
43 Some(PlaceholderRef {
44 name: name.to_string(),
45 global,
46 })
47}
48
49pub struct Template {
52 compiled: CompiledTemplate,
53 context: GeneratorContext,
54}
55
56impl Template {
57 #[instrument(name = "lmn.template.parse", fields(path = %path.display()))]
60 pub fn parse(path: &Path) -> Result<Self, TemplateError> {
61 let content = std::fs::read_to_string(path)?;
62 let mut root: serde_json::Map<String, Value> = serde_json::from_str(&content)?;
63
64 let metadata = root
66 .remove(METADATA_KEY)
67 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
68
69 let raw_defs: std::collections::HashMap<String, definition::RawTemplateDef> =
70 serde_json::from_value(metadata)?;
71 let defs = definition::validate_all(raw_defs)?;
72
73 let body = Value::Object(root);
74
75 renderer::validate_placeholders(&body, &defs)?;
77
78 definition::check_circular_refs(&defs)?;
80
81 let ctx = GeneratorContext::new(defs);
82
83 let global_resolved = GlobalPlaceholderHandler.resolve(&body, &ctx)?;
85
86 let env_resolved = EnvPlaceholderHandler.resolve(&body, &ctx)?;
88
89 let mut all_resolved = global_resolved;
91 all_resolved.extend(env_resolved);
92
93 let compiled = CompiledTemplate::compile(&body)?;
94 Ok(Template {
97 compiled,
98 context: ctx.with_resolved(all_resolved),
99 })
100 }
101
102 pub fn generate_one(&self) -> Result<String, TemplateError> {
106 let mut rng = rand::rng();
107 self.compiled.render(&self.context, &mut rng)
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use std::io::Write;
115
116 fn write_temp(content: &str) -> tempfile::NamedTempFile {
117 let mut f = tempfile::NamedTempFile::new().unwrap();
118 f.write_all(content.as_bytes()).unwrap();
119 f
120 }
121
122 #[test]
123 fn parse_fails_on_missing_file() {
124 assert!(Template::parse(Path::new("nonexistent.json")).is_err());
125 }
126
127 #[test]
128 fn parse_fails_on_invalid_json() {
129 let f = write_temp("not json");
130 assert!(Template::parse(f.path()).is_err());
131 }
132
133 #[test]
134 fn parse_fails_on_unknown_placeholder() {
135 let f = write_temp(r#"{"field": "{{undefined}}"}"#);
136 assert!(Template::parse(f.path()).is_err());
137 }
138
139 #[test]
140 fn parse_fails_on_circular_reference() {
141 let f = write_temp(
142 r#"{
143 "field": "{{a}}",
144 "_lmn_metadata_templates": {
145 "a": { "type": "object", "composition": { "x": "{{b}}" } },
146 "b": { "type": "object", "composition": { "y": "{{a}}" } }
147 }
148 }"#,
149 );
150 assert!(Template::parse(f.path()).is_err());
151 }
152
153 #[test]
154 fn parse_succeeds_with_no_placeholders() {
155 let f = write_temp(r#"{"field": "static"}"#);
156 assert!(Template::parse(f.path()).is_ok());
157 }
158
159 #[test]
160 fn generate_one_returns_valid_json() {
161 let f = write_temp(r#"{"field": "static", "value": 42}"#);
162 let template = Template::parse(f.path()).unwrap();
163 let result = template.generate_one().unwrap();
164 let parsed: serde_json::Value =
165 serde_json::from_str(&result).expect("generate_one must return valid JSON");
166 assert_eq!(
167 parsed["field"],
168 serde_json::Value::String("static".to_string())
169 );
170 assert_eq!(parsed["value"], serde_json::json!(42));
171 }
172
173 #[test]
174 fn generate_one_is_independent_per_call() {
175 let f = write_temp(r#"{"field": "static"}"#);
176 let template = Template::parse(f.path()).unwrap();
177 let a = template.generate_one().unwrap();
178 let b = template.generate_one().unwrap();
179 assert!(serde_json::from_str::<serde_json::Value>(&a).is_ok());
180 assert!(serde_json::from_str::<serde_json::Value>(&b).is_ok());
181 }
182
183 #[test]
184 fn parse_env_placeholder_resolved_from_env() {
185 unsafe { std::env::set_var("LUMEN_TEST_RESOLVE_TOKEN", "secret123") };
186 let f = write_temp(r#"{"token": "{{ENV:LUMEN_TEST_RESOLVE_TOKEN}}"}"#);
187 let template = Template::parse(f.path()).unwrap();
188 let result = template.generate_one().unwrap();
189 assert!(
190 result.contains("secret123"),
191 "expected 'secret123' in output, got: {result}"
192 );
193 }
194
195 #[test]
196 fn parse_env_placeholder_missing_var_is_error() {
197 unsafe { std::env::remove_var("LUMEN_NONEXISTENT_12345") };
198 let f = write_temp(r#"{"token": "{{ENV:LUMEN_NONEXISTENT_12345}}"}"#);
199 let result = Template::parse(f.path());
200 assert!(
201 result.is_err(),
202 "expected parse to fail for missing env var"
203 );
204 assert!(
205 matches!(result.err(), Some(TemplateError::MissingEnvVar(_))),
206 "expected MissingEnvVar error variant"
207 );
208 }
209
210 #[test]
211 fn parse_env_placeholder_no_def_required() {
212 unsafe { std::env::set_var("LUMEN_TEST_TOKEN", "anyvalue") };
213 let f =
214 write_temp(r#"{"token": "{{ENV:LUMEN_TEST_TOKEN}}", "_lmn_metadata_templates": {}}"#);
215 assert!(Template::parse(f.path()).is_ok());
216 }
217
218 #[test]
219 fn parse_env_placeholder_with_global_suffix_resolves_correctly() {
220 unsafe { std::env::set_var("LUMEN_TEST_GLOBAL_TOKEN", "global_secret_value") };
224 let f = write_temp(r#"{"token": "{{ENV:LUMEN_TEST_GLOBAL_TOKEN:global}}"}"#);
225 let template = Template::parse(f.path()).unwrap();
226 let result = template.generate_one().unwrap();
227 assert!(
228 result.contains("global_secret_value"),
229 "expected 'global_secret_value' in output, got: {result}"
230 );
231 }
232
233 #[test]
234 fn parse_env_placeholder_empty_var_name_is_error() {
235 let f = write_temp(r#"{"token": "{{ENV:}}"}"#);
237 let result = Template::parse(f.path());
238 assert!(
239 result.is_err(),
240 "expected parse to fail for empty ENV var name"
241 );
242 assert!(
243 matches!(result.err(), Some(TemplateError::InvalidEnvVarName(_))),
244 "expected InvalidEnvVarName error variant"
245 );
246 }
247}