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 #[instrument(name = "lmn.template.generate_one", skip(self))]
106 pub fn generate_one(&self) -> Result<String, TemplateError> {
107 let mut rng = rand::rng();
108 self.compiled.render(&self.context, &mut rng)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use std::io::Write;
116
117 fn write_temp(content: &str) -> tempfile::NamedTempFile {
118 let mut f = tempfile::NamedTempFile::new().unwrap();
119 f.write_all(content.as_bytes()).unwrap();
120 f
121 }
122
123 #[test]
124 fn parse_fails_on_missing_file() {
125 assert!(Template::parse(Path::new("nonexistent.json")).is_err());
126 }
127
128 #[test]
129 fn parse_fails_on_invalid_json() {
130 let f = write_temp("not json");
131 assert!(Template::parse(f.path()).is_err());
132 }
133
134 #[test]
135 fn parse_fails_on_unknown_placeholder() {
136 let f = write_temp(r#"{"field": "{{undefined}}"}"#);
137 assert!(Template::parse(f.path()).is_err());
138 }
139
140 #[test]
141 fn parse_fails_on_circular_reference() {
142 let f = write_temp(
143 r#"{
144 "field": "{{a}}",
145 "_lmn_metadata_templates": {
146 "a": { "type": "object", "composition": { "x": "{{b}}" } },
147 "b": { "type": "object", "composition": { "y": "{{a}}" } }
148 }
149 }"#,
150 );
151 assert!(Template::parse(f.path()).is_err());
152 }
153
154 #[test]
155 fn parse_succeeds_with_no_placeholders() {
156 let f = write_temp(r#"{"field": "static"}"#);
157 assert!(Template::parse(f.path()).is_ok());
158 }
159
160 #[test]
161 fn generate_one_returns_valid_json() {
162 let f = write_temp(r#"{"field": "static", "value": 42}"#);
163 let template = Template::parse(f.path()).unwrap();
164 let result = template.generate_one().unwrap();
165 let parsed: serde_json::Value =
166 serde_json::from_str(&result).expect("generate_one must return valid JSON");
167 assert_eq!(
168 parsed["field"],
169 serde_json::Value::String("static".to_string())
170 );
171 assert_eq!(parsed["value"], serde_json::json!(42));
172 }
173
174 #[test]
175 fn generate_one_is_independent_per_call() {
176 let f = write_temp(r#"{"field": "static"}"#);
177 let template = Template::parse(f.path()).unwrap();
178 let a = template.generate_one().unwrap();
179 let b = template.generate_one().unwrap();
180 assert!(serde_json::from_str::<serde_json::Value>(&a).is_ok());
181 assert!(serde_json::from_str::<serde_json::Value>(&b).is_ok());
182 }
183
184 #[test]
185 fn parse_env_placeholder_resolved_from_env() {
186 unsafe { std::env::set_var("LUMEN_TEST_TOKEN", "secret123") };
187 let f = write_temp(r#"{"token": "{{ENV:LUMEN_TEST_TOKEN}}"}"#);
188 let template = Template::parse(f.path()).unwrap();
189 let result = template.generate_one().unwrap();
190 assert!(
191 result.contains("secret123"),
192 "expected 'secret123' in output, got: {result}"
193 );
194 }
195
196 #[test]
197 fn parse_env_placeholder_missing_var_is_error() {
198 unsafe { std::env::remove_var("LUMEN_NONEXISTENT_12345") };
199 let f = write_temp(r#"{"token": "{{ENV:LUMEN_NONEXISTENT_12345}}"}"#);
200 let result = Template::parse(f.path());
201 assert!(
202 result.is_err(),
203 "expected parse to fail for missing env var"
204 );
205 assert!(
206 matches!(result.err(), Some(TemplateError::MissingEnvVar(_))),
207 "expected MissingEnvVar error variant"
208 );
209 }
210
211 #[test]
212 fn parse_env_placeholder_no_def_required() {
213 unsafe { std::env::set_var("LUMEN_TEST_TOKEN", "anyvalue") };
214 let f =
215 write_temp(r#"{"token": "{{ENV:LUMEN_TEST_TOKEN}}", "_lmn_metadata_templates": {}}"#);
216 assert!(Template::parse(f.path()).is_ok());
217 }
218
219 #[test]
220 fn parse_env_placeholder_with_global_suffix_resolves_correctly() {
221 unsafe { std::env::set_var("LUMEN_TEST_GLOBAL_TOKEN", "global_secret_value") };
225 let f = write_temp(r#"{"token": "{{ENV:LUMEN_TEST_GLOBAL_TOKEN:global}}"}"#);
226 let template = Template::parse(f.path()).unwrap();
227 let result = template.generate_one().unwrap();
228 assert!(
229 result.contains("global_secret_value"),
230 "expected 'global_secret_value' in output, got: {result}"
231 );
232 }
233
234 #[test]
235 fn parse_env_placeholder_empty_var_name_is_error() {
236 let f = write_temp(r#"{"token": "{{ENV:}}"}"#);
238 let result = Template::parse(f.path());
239 assert!(
240 result.is_err(),
241 "expected parse to fail for empty ENV var name"
242 );
243 assert!(
244 matches!(result.err(), Some(TemplateError::InvalidEnvVarName(_))),
245 "expected InvalidEnvVarName error variant"
246 );
247 }
248}