git_adr/core/
templates.rs1use crate::Error;
7use std::collections::HashMap;
8use tera::{Context, Tera};
9
10pub const TEMPLATE_NYGARD: &str = r#"# {{ title }}
12
13## Status
14
15{{ status }}
16
17## Context
18
19{{ context | default(value="What is the issue that we're seeing that is motivating this decision or change?") }}
20
21## Decision
22
23{{ decision | default(value="What is the change that we're proposing and/or doing?") }}
24
25## Consequences
26
27{{ consequences | default(value="What becomes easier or more difficult to do because of this change?") }}
28"#;
29
30pub const TEMPLATE_MADR: &str = r#"# {{ title }}
32
33## Status
34
35{{ status }}
36
37{% if deciders %}
38Deciders: {{ deciders | join(sep=", ") }}
39{% endif %}
40{% if date %}
41Date: {{ date }}
42{% endif %}
43
44## Context and Problem Statement
45
46{{ context | default(value="Describe the context and problem statement...") }}
47
48## Decision Drivers
49
50{% for driver in decision_drivers | default(value=[]) %}
51* {{ driver }}
52{% else %}
53* Driver 1
54* Driver 2
55{% endfor %}
56
57## Considered Options
58
59{% for option in options | default(value=[]) %}
60* {{ option }}
61{% else %}
62* Option 1
63* Option 2
64{% endfor %}
65
66## Decision Outcome
67
68Chosen option: "{{ chosen_option | default(value="Option X") }}"
69
70### Consequences
71
72#### Good
73
74{% for item in good_consequences | default(value=[]) %}
75* {{ item }}
76{% else %}
77* Good consequence 1
78{% endfor %}
79
80#### Bad
81
82{% for item in bad_consequences | default(value=[]) %}
83* {{ item }}
84{% else %}
85* Bad consequence 1
86{% endfor %}
87
88## More Information
89
90{{ more_info | default(value="Additional information, links, references...") }}
91"#;
92
93pub const TEMPLATE_Y_STATEMENT: &str = r#"# {{ title }}
95
96## Status
97
98{{ status }}
99
100## Decision
101
102In the context of {{ context | default(value="<use case/user story>") }},
103facing {{ facing | default(value="<concern>") }},
104we decided for {{ decision | default(value="<option>") }}
105and against {{ against | default(value="<other options>") }},
106to achieve {{ achieve | default(value="<quality>") }},
107accepting {{ accepting | default(value="<downside>") }}.
108"#;
109
110pub const TEMPLATE_ALEXANDRIAN: &str = r#"# {{ title }}
112
113## Status
114
115{{ status }}
116
117## Prologue
118
119{{ prologue | default(value="Summary and context for this ADR") }}
120
121## Problem Statement
122
123{{ problem | default(value="The problem or question being addressed") }}
124
125## Forces
126
127{% for force in forces | default(value=[]) %}
128* {{ force }}
129{% else %}
130* Force 1: Description
131* Force 2: Description
132{% endfor %}
133
134## Solution
135
136{{ solution | default(value="The chosen solution") }}
137
138## Consequences
139
140{{ consequences | default(value="Resulting context after applying the solution") }}
141
142## Related Patterns
143
144{% for pattern in related | default(value=[]) %}
145* {{ pattern }}
146{% endfor %}
147"#;
148
149pub const TEMPLATE_BUSINESS_CASE: &str = r#"# {{ title }}
151
152## Status
153
154{{ status }}
155
156## Executive Summary
157
158{{ executive_summary | default(value="Brief overview of the decision and its impact") }}
159
160## Background
161
162{{ background | default(value="Context and history leading to this decision") }}
163
164## Problem Statement
165
166{{ problem | default(value="The business problem being addressed") }}
167
168## Proposed Solution
169
170{{ solution | default(value="Recommended approach") }}
171
172## Alternatives Considered
173
174{% for alt in alternatives | default(value=[]) %}
175### {{ alt.name }}
176{{ alt.description }}
177{% else %}
178### Alternative 1
179Description of alternative approach
180{% endfor %}
181
182## Cost-Benefit Analysis
183
184### Costs
185{% for cost in costs | default(value=[]) %}
186* {{ cost }}
187{% else %}
188* Implementation cost
189* Maintenance cost
190{% endfor %}
191
192### Benefits
193{% for benefit in benefits | default(value=[]) %}
194* {{ benefit }}
195{% else %}
196* Benefit 1
197* Benefit 2
198{% endfor %}
199
200## Risk Assessment
201
202{% for risk in risks | default(value=[]) %}
203* {{ risk }}
204{% else %}
205* Risk 1: Mitigation strategy
206{% endfor %}
207
208## Implementation Plan
209
210{{ implementation | default(value="High-level implementation approach") }}
211
212## Success Metrics
213
214{% for metric in metrics | default(value=[]) %}
215* {{ metric }}
216{% else %}
217* Metric 1
218{% endfor %}
219"#;
220
221#[derive(Debug)]
223pub struct TemplateEngine {
224 tera: Tera,
225}
226
227impl Default for TemplateEngine {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233impl TemplateEngine {
234 #[must_use]
236 pub fn new() -> Self {
237 let mut tera = Tera::default();
238
239 let _ = tera.add_raw_template("nygard", TEMPLATE_NYGARD);
241 let _ = tera.add_raw_template("madr", TEMPLATE_MADR);
242 let _ = tera.add_raw_template("y-statement", TEMPLATE_Y_STATEMENT);
243 let _ = tera.add_raw_template("alexandrian", TEMPLATE_ALEXANDRIAN);
244 let _ = tera.add_raw_template("business-case", TEMPLATE_BUSINESS_CASE);
245
246 Self { tera }
247 }
248
249 pub fn add_template(&mut self, name: &str, content: &str) -> Result<(), Error> {
255 self.tera
256 .add_raw_template(name, content)
257 .map_err(|e| Error::TemplateError {
258 message: format!("Failed to add template '{name}': {e}"),
259 })
260 }
261
262 pub fn render(
268 &self,
269 template: &str,
270 context: &HashMap<String, String>,
271 ) -> Result<String, Error> {
272 let mut tera_context = Context::new();
273 for (key, value) in context {
274 tera_context.insert(key, value);
275 }
276
277 self.tera
278 .render(template, &tera_context)
279 .map_err(|e| Error::TemplateError {
280 message: format!("Failed to render template '{template}': {e}"),
281 })
282 }
283
284 #[must_use]
286 pub fn list_templates(&self) -> Vec<String> {
287 self.tera.get_template_names().map(String::from).collect()
288 }
289
290 #[must_use]
292 pub fn has_template(&self, name: &str) -> bool {
293 self.tera.get_template_names().any(|n| n == name)
294 }
295
296 pub fn get_template(&self, name: &str) -> Result<String, Error> {
302 match name {
304 "nygard" => Ok(TEMPLATE_NYGARD.to_string()),
305 "madr" => Ok(TEMPLATE_MADR.to_string()),
306 "y-statement" => Ok(TEMPLATE_Y_STATEMENT.to_string()),
307 "alexandrian" => Ok(TEMPLATE_ALEXANDRIAN.to_string()),
308 "business-case" => Ok(TEMPLATE_BUSINESS_CASE.to_string()),
309 _ => Err(Error::TemplateNotFound {
310 name: name.to_string(),
311 }),
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_template_engine_new() {
322 let engine = TemplateEngine::new();
323 assert!(engine.has_template("nygard"));
324 assert!(engine.has_template("madr"));
325 assert!(!engine.has_template("nonexistent"));
326 }
327
328 #[test]
329 fn test_template_engine_default() {
330 let engine = TemplateEngine::default();
331 assert!(engine.has_template("nygard"));
332 }
333
334 #[test]
335 fn test_render_nygard() {
336 let engine = TemplateEngine::new();
337 let mut context = HashMap::new();
338 context.insert("title".to_string(), "Test ADR".to_string());
339 context.insert("status".to_string(), "Proposed".to_string());
340
341 let result = engine
342 .render("nygard", &context)
343 .expect("Template should render successfully");
344 assert!(result.contains("# Test ADR"));
345 assert!(result.contains("## Status"));
346 assert!(result.contains("Proposed"));
347 }
348
349 #[test]
350 fn test_render_madr() {
351 let engine = TemplateEngine::new();
352 let mut context = HashMap::new();
353 context.insert("title".to_string(), "MADR Test".to_string());
354 context.insert("status".to_string(), "accepted".to_string());
355
356 let result = engine.render("madr", &context).expect("Should render");
357 assert!(result.contains("# MADR Test"));
358 assert!(result.contains("Context and Problem Statement"));
359 }
360
361 #[test]
362 fn test_render_y_statement() {
363 let engine = TemplateEngine::new();
364 let mut context = HashMap::new();
365 context.insert("title".to_string(), "Y Statement Test".to_string());
366 context.insert("status".to_string(), "proposed".to_string());
367
368 let result = engine
369 .render("y-statement", &context)
370 .expect("Should render");
371 assert!(result.contains("# Y Statement Test"));
372 assert!(result.contains("In the context of"));
373 }
374
375 #[test]
376 fn test_render_alexandrian() {
377 let engine = TemplateEngine::new();
378 let mut context = HashMap::new();
379 context.insert("title".to_string(), "Alexandrian Test".to_string());
380 context.insert("status".to_string(), "proposed".to_string());
381
382 let result = engine
383 .render("alexandrian", &context)
384 .expect("Should render");
385 assert!(result.contains("# Alexandrian Test"));
386 assert!(result.contains("Prologue"));
387 assert!(result.contains("Forces"));
388 }
389
390 #[test]
391 fn test_render_business_case() {
392 let engine = TemplateEngine::new();
393 let mut context = HashMap::new();
394 context.insert("title".to_string(), "Business Case Test".to_string());
395 context.insert("status".to_string(), "proposed".to_string());
396
397 let result = engine
398 .render("business-case", &context)
399 .expect("Should render");
400 assert!(result.contains("# Business Case Test"));
401 assert!(result.contains("Executive Summary"));
402 assert!(result.contains("Cost-Benefit Analysis"));
403 }
404
405 #[test]
406 fn test_list_templates() {
407 let engine = TemplateEngine::new();
408 let templates = engine.list_templates();
409 assert!(templates.contains(&"nygard".to_string()));
410 assert!(templates.contains(&"madr".to_string()));
411 assert!(templates.contains(&"y-statement".to_string()));
412 assert!(templates.contains(&"alexandrian".to_string()));
413 assert!(templates.contains(&"business-case".to_string()));
414 }
415
416 #[test]
417 fn test_add_custom_template() {
418 let mut engine = TemplateEngine::new();
419 let custom = "# {{ title }}\n\nCustom template content";
420 engine
421 .add_template("custom", custom)
422 .expect("Should add template");
423 assert!(engine.has_template("custom"));
424 }
425
426 #[test]
427 fn test_get_template() {
428 let engine = TemplateEngine::new();
429 let nygard = engine.get_template("nygard").expect("Should get template");
430 assert!(nygard.contains("## Context"));
431 }
432
433 #[test]
434 fn test_get_template_not_found() {
435 let engine = TemplateEngine::new();
436 let result = engine.get_template("nonexistent");
437 assert!(result.is_err());
438 }
439
440 #[test]
441 fn test_render_nonexistent_template() {
442 let engine = TemplateEngine::new();
443 let context = HashMap::new();
444 let result = engine.render("nonexistent", &context);
445 assert!(result.is_err());
446 }
447
448 #[test]
449 fn test_add_template_invalid() {
450 let mut engine = TemplateEngine::new();
451 let invalid = "{% for item in items %}{{ item }}{% endwith %}";
453 let result = engine.add_template("invalid", invalid);
454 assert!(result.is_err());
455 }
456}