adrs_core/
template.rs

1//! Template system for generating ADR files.
2
3use crate::{Adr, Config, Error, Result};
4use minijinja::{Environment, context};
5use std::path::Path;
6
7/// Built-in template formats.
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub enum TemplateFormat {
10    /// Michael Nygard's original ADR format.
11    #[default]
12    Nygard,
13
14    /// Markdown Any Decision Records format (MADR 4.0.0).
15    Madr,
16}
17
18impl std::fmt::Display for TemplateFormat {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            Self::Nygard => write!(f, "nygard"),
22            Self::Madr => write!(f, "madr"),
23        }
24    }
25}
26
27impl std::str::FromStr for TemplateFormat {
28    type Err = Error;
29
30    fn from_str(s: &str) -> Result<Self> {
31        match s.to_lowercase().as_str() {
32            "nygard" | "default" => Ok(Self::Nygard),
33            "madr" => Ok(Self::Madr),
34            _ => Err(Error::TemplateNotFound(s.to_string())),
35        }
36    }
37}
38
39/// Template variants for different levels of detail.
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
41pub enum TemplateVariant {
42    /// Full template with all sections and guidance.
43    #[default]
44    Full,
45
46    /// Minimal template with essential sections only.
47    Minimal,
48
49    /// Bare template with just the structure.
50    Bare,
51}
52
53impl std::fmt::Display for TemplateVariant {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::Full => write!(f, "full"),
57            Self::Minimal => write!(f, "minimal"),
58            Self::Bare => write!(f, "bare"),
59        }
60    }
61}
62
63impl std::str::FromStr for TemplateVariant {
64    type Err = Error;
65
66    fn from_str(s: &str) -> Result<Self> {
67        match s.to_lowercase().as_str() {
68            "full" | "default" => Ok(Self::Full),
69            "minimal" | "min" => Ok(Self::Minimal),
70            "bare" | "empty" => Ok(Self::Bare),
71            _ => Err(Error::TemplateNotFound(format!("Unknown variant: {s}"))),
72        }
73    }
74}
75
76/// A template for generating ADRs.
77#[derive(Debug, Clone)]
78pub struct Template {
79    /// The template content.
80    content: String,
81
82    /// The template name (for error messages).
83    name: String,
84}
85
86impl Template {
87    /// Create a template from a string.
88    pub fn from_string(name: impl Into<String>, content: impl Into<String>) -> Self {
89        Self {
90            name: name.into(),
91            content: content.into(),
92        }
93    }
94
95    /// Load a template from a file.
96    pub fn from_file(path: &Path) -> Result<Self> {
97        let content = std::fs::read_to_string(path)?;
98        let name = path
99            .file_name()
100            .and_then(|n| n.to_str())
101            .unwrap_or("custom")
102            .to_string();
103        Ok(Self { name, content })
104    }
105
106    /// Get a built-in template by format (uses Full variant).
107    pub fn builtin(format: TemplateFormat) -> Self {
108        Self::builtin_with_variant(format, TemplateVariant::Full)
109    }
110
111    /// Get a built-in template by format and variant.
112    pub fn builtin_with_variant(format: TemplateFormat, variant: TemplateVariant) -> Self {
113        let (name, content) = match (format, variant) {
114            // Nygard templates
115            (TemplateFormat::Nygard, TemplateVariant::Full) => ("nygard", NYGARD_TEMPLATE),
116            (TemplateFormat::Nygard, TemplateVariant::Minimal) => {
117                ("nygard-minimal", NYGARD_MINIMAL_TEMPLATE)
118            }
119            (TemplateFormat::Nygard, TemplateVariant::Bare) => {
120                ("nygard-bare", NYGARD_BARE_TEMPLATE)
121            }
122
123            // MADR templates
124            (TemplateFormat::Madr, TemplateVariant::Full) => ("madr", MADR_TEMPLATE),
125            (TemplateFormat::Madr, TemplateVariant::Minimal) => {
126                ("madr-minimal", MADR_MINIMAL_TEMPLATE)
127            }
128            (TemplateFormat::Madr, TemplateVariant::Bare) => ("madr-bare", MADR_BARE_TEMPLATE),
129        };
130        Self::from_string(name, content)
131    }
132
133    /// Render the template with the given ADR data.
134    pub fn render(&self, adr: &Adr, config: &Config) -> Result<String> {
135        use crate::LinkKind;
136
137        let mut env = Environment::new();
138        env.add_template(&self.name, &self.content)
139            .map_err(|e| Error::TemplateError(e.to_string()))?;
140
141        let template = env
142            .get_template(&self.name)
143            .map_err(|e| Error::TemplateError(e.to_string()))?;
144
145        // Convert links to a format with display-friendly kind
146        let links: Vec<_> = adr
147            .links
148            .iter()
149            .map(|link| {
150                let kind_display = match &link.kind {
151                    LinkKind::Supersedes => "Supersedes",
152                    LinkKind::SupersededBy => "Superseded by",
153                    LinkKind::Amends => "Amends",
154                    LinkKind::AmendedBy => "Amended by",
155                    LinkKind::RelatesTo => "Relates to",
156                    LinkKind::Custom(s) => s.as_str(),
157                };
158                context! {
159                    target => link.target,
160                    kind => kind_display,
161                    description => &link.description,
162                }
163            })
164            .collect();
165
166        let output = template
167            .render(context! {
168                number => adr.number,
169                title => &adr.title,
170                date => crate::parse::format_date(adr.date),
171                status => adr.status.to_string(),
172                context => &adr.context,
173                decision => &adr.decision,
174                consequences => &adr.consequences,
175                links => links,
176                is_ng => config.is_next_gen(),
177                // MADR 4.0.0 fields
178                decision_makers => &adr.decision_makers,
179                consulted => &adr.consulted,
180                informed => &adr.informed,
181            })
182            .map_err(|e| Error::TemplateError(e.to_string()))?;
183
184        Ok(output)
185    }
186}
187
188/// Template engine for managing and rendering templates.
189#[derive(Debug)]
190pub struct TemplateEngine {
191    /// The default template format.
192    default_format: TemplateFormat,
193
194    /// The default template variant.
195    default_variant: TemplateVariant,
196
197    /// Custom template path (overrides built-in).
198    custom_template: Option<Template>,
199}
200
201impl Default for TemplateEngine {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207impl TemplateEngine {
208    /// Create a new template engine.
209    pub fn new() -> Self {
210        Self {
211            default_format: TemplateFormat::default(),
212            default_variant: TemplateVariant::default(),
213            custom_template: None,
214        }
215    }
216
217    /// Set the default template format.
218    pub fn with_format(mut self, format: TemplateFormat) -> Self {
219        self.default_format = format;
220        self
221    }
222
223    /// Set the default template variant.
224    pub fn with_variant(mut self, variant: TemplateVariant) -> Self {
225        self.default_variant = variant;
226        self
227    }
228
229    /// Set a custom template.
230    pub fn with_custom_template(mut self, template: Template) -> Self {
231        self.custom_template = Some(template);
232        self
233    }
234
235    /// Load a custom template from a file.
236    pub fn with_custom_template_file(mut self, path: &Path) -> Result<Self> {
237        self.custom_template = Some(Template::from_file(path)?);
238        Ok(self)
239    }
240
241    /// Get the template to use for rendering.
242    pub fn template(&self) -> Template {
243        self.custom_template.clone().unwrap_or_else(|| {
244            Template::builtin_with_variant(self.default_format, self.default_variant)
245        })
246    }
247
248    /// Render an ADR using the configured template.
249    pub fn render(&self, adr: &Adr, config: &Config) -> Result<String> {
250        self.template().render(adr, config)
251    }
252}
253
254/// Nygard's original ADR template (compatible mode).
255const NYGARD_TEMPLATE: &str = r#"{% if is_ng %}---
256number: {{ number }}
257title: {{ title }}
258date: {{ date }}
259status: {{ status | lower }}
260{% if links %}links:
261{% for link in links %}  - target: {{ link.target }}
262    kind: {{ link.kind | lower }}
263{% endfor %}{% endif %}---
264
265{% endif %}# {{ number }}. {{ title }}
266
267Date: {{ date }}
268
269## Status
270
271{{ status }}
272{% for link in links %}
273{{ link.kind }} [{{ link.target }}. ...]({{ "%04d" | format(link.target) }}-....md)
274{% endfor %}
275## Context
276
277{{ context if context else "What is the issue that we're seeing that is motivating this decision or change?" }}
278
279## Decision
280
281{{ decision if decision else "What is the change that we're proposing and/or doing?" }}
282
283## Consequences
284
285{{ consequences if consequences else "What becomes easier or more difficult to do because of this change?" }}
286"#;
287
288/// MADR (Markdown Any Decision Records) 4.0.0 template.
289const MADR_TEMPLATE: &str = r#"---
290status: {{ status | lower }}
291date: {{ date }}
292{% if decision_makers %}decision-makers:
293{% for dm in decision_makers %}  - {{ dm }}
294{% endfor %}{% endif %}{% if consulted %}consulted:
295{% for c in consulted %}  - {{ c }}
296{% endfor %}{% endif %}{% if informed %}informed:
297{% for i in informed %}  - {{ i }}
298{% endfor %}{% endif %}---
299
300# {{ title }}
301
302## Context and Problem Statement
303
304{{ context if context else "{Describe the context and problem statement, e.g., in free form using two to three sentences or in the form of an illustrative story. You may want to articulate the problem in form of a question and add links to collaboration boards or issue management systems.}" }}
305
306<!-- This is an optional element. Feel free to remove. -->
307## Decision Drivers
308
309* {decision driver 1, e.g., a force, facing concern, ...}
310* {decision driver 2, e.g., a force, facing concern, ...}
311* ... <!-- numbers of drivers can vary -->
312
313## Considered Options
314
315* {title of option 1}
316* {title of option 2}
317* {title of option 3}
318* ... <!-- numbers of options can vary -->
319
320## Decision Outcome
321
322{{ decision if decision else "Chosen option: \"{title of option 1}\", because {justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force {force} | ... | comes out best (see below)}." }}
323
324<!-- This is an optional element. Feel free to remove. -->
325### Consequences
326
327{{ consequences if consequences else "* Good, because {positive consequence, e.g., improvement of one or more desired qualities, ...}\n* Bad, because {negative consequence, e.g., compromising one or more desired qualities, ...}\n* ... <!-- numbers of consequences can vary -->" }}
328
329<!-- This is an optional element. Feel free to remove. -->
330### Confirmation
331
332{Describe how the implementation/compliance of the ADR can/will be confirmed. Is there any automated or manual fitness function? If so, list it and explain how it is applied. Is the chosen design and its implementation in line with the decision? E.g., a design/code review or a test with a library such as ArchUnit can help validate this. Note that although we classify this element as optional, it is included in many ADRs.}
333
334<!-- This is an optional element. Feel free to remove. -->
335## Pros and Cons of the Options
336
337### {title of option 1}
338
339<!-- This is an optional element. Feel free to remove. -->
340{example | description | pointer to more information | ...}
341
342* Good, because {argument a}
343* Good, because {argument b}
344<!-- use "neutral" if the given argument weights neither for good nor bad -->
345* Neutral, because {argument c}
346* Bad, because {argument d}
347* ... <!-- numbers of pros and cons can vary -->
348
349### {title of other option}
350
351{example | description | pointer to more information | ...}
352
353* Good, because {argument a}
354* Good, because {argument b}
355* Neutral, because {argument c}
356* Bad, because {argument d}
357* ...
358
359<!-- This is an optional element. Feel free to remove. -->
360## More Information
361
362{You might want to provide additional evidence/confidence for the decision outcome here and/or document the team agreement on the decision and/or define when/how this decision should be realized and if/when it should be re-visited. Links to other decisions and resources might appear here as well.}
363"#;
364
365/// Nygard minimal template - essential sections only.
366const NYGARD_MINIMAL_TEMPLATE: &str = r#"{% if is_ng %}---
367number: {{ number }}
368title: {{ title }}
369date: {{ date }}
370status: {{ status | lower }}
371{% if links %}links:
372{% for link in links %}  - target: {{ link.target }}
373    kind: {{ link.kind | lower }}
374{% endfor %}{% endif %}---
375
376{% endif %}# {{ number }}. {{ title }}
377
378Date: {{ date }}
379
380## Status
381
382{{ status }}
383{% for link in links %}
384{{ link.kind }} [{{ link.target }}. ...]({{ "%04d" | format(link.target) }}-....md)
385{% endfor %}
386## Context
387
388{{ context if context else "" }}
389
390## Decision
391
392{{ decision if decision else "" }}
393
394## Consequences
395
396{{ consequences if consequences else "" }}
397"#;
398
399/// Nygard bare template - just the structure, no guidance.
400const NYGARD_BARE_TEMPLATE: &str = r#"# {{ number }}. {{ title }}
401
402Date: {{ date }}
403
404## Status
405
406{{ status }}
407
408## Context
409
410
411
412## Decision
413
414
415
416## Consequences
417
418"#;
419
420/// MADR minimal template - core sections only.
421const MADR_MINIMAL_TEMPLATE: &str = r#"---
422status: {{ status | lower }}
423date: {{ date }}
424{% if decision_makers %}decision-makers:
425{% for dm in decision_makers %}  - {{ dm }}
426{% endfor %}{% endif %}{% if consulted %}consulted:
427{% for c in consulted %}  - {{ c }}
428{% endfor %}{% endif %}{% if informed %}informed:
429{% for i in informed %}  - {{ i }}
430{% endfor %}{% endif %}---
431
432# {{ title }}
433
434## Context and Problem Statement
435
436{{ context if context else "" }}
437
438## Decision Outcome
439
440{{ decision if decision else "" }}
441
442### Consequences
443
444{{ consequences if consequences else "" }}
445"#;
446
447/// MADR bare template - just the structure.
448const MADR_BARE_TEMPLATE: &str = r#"---
449status: {{ status | lower }}
450date: {{ date }}
451---
452
453# {{ title }}
454
455## Context and Problem Statement
456
457
458
459## Decision Outcome
460
461
462
463### Consequences
464
465"#;
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use crate::{AdrLink, AdrStatus, ConfigMode, LinkKind};
471    use tempfile::TempDir;
472    use test_case::test_case;
473
474    // ========== TemplateFormat Tests ==========
475
476    #[test]
477    fn test_template_format_default() {
478        assert_eq!(TemplateFormat::default(), TemplateFormat::Nygard);
479    }
480
481    #[test_case("nygard" => TemplateFormat::Nygard; "nygard")]
482    #[test_case("Nygard" => TemplateFormat::Nygard; "nygard capitalized")]
483    #[test_case("NYGARD" => TemplateFormat::Nygard; "nygard uppercase")]
484    #[test_case("default" => TemplateFormat::Nygard; "default alias")]
485    #[test_case("madr" => TemplateFormat::Madr; "madr")]
486    #[test_case("MADR" => TemplateFormat::Madr; "madr uppercase")]
487    fn test_template_format_parse(input: &str) -> TemplateFormat {
488        input.parse().unwrap()
489    }
490
491    #[test]
492    fn test_template_format_parse_unknown() {
493        let result: Result<TemplateFormat> = "unknown".parse();
494        assert!(result.is_err());
495    }
496
497    #[test]
498    fn test_template_format_display() {
499        assert_eq!(TemplateFormat::Nygard.to_string(), "nygard");
500        assert_eq!(TemplateFormat::Madr.to_string(), "madr");
501    }
502
503    // ========== TemplateVariant Tests ==========
504
505    #[test]
506    fn test_template_variant_default() {
507        assert_eq!(TemplateVariant::default(), TemplateVariant::Full);
508    }
509
510    #[test_case("full" => TemplateVariant::Full; "full")]
511    #[test_case("Full" => TemplateVariant::Full; "full capitalized")]
512    #[test_case("default" => TemplateVariant::Full; "default alias")]
513    #[test_case("minimal" => TemplateVariant::Minimal; "minimal")]
514    #[test_case("min" => TemplateVariant::Minimal; "min alias")]
515    #[test_case("bare" => TemplateVariant::Bare; "bare")]
516    #[test_case("empty" => TemplateVariant::Bare; "empty alias")]
517    fn test_template_variant_parse(input: &str) -> TemplateVariant {
518        input.parse().unwrap()
519    }
520
521    #[test]
522    fn test_template_variant_parse_unknown() {
523        let result: Result<TemplateVariant> = "unknown".parse();
524        assert!(result.is_err());
525    }
526
527    #[test]
528    fn test_template_variant_display() {
529        assert_eq!(TemplateVariant::Full.to_string(), "full");
530        assert_eq!(TemplateVariant::Minimal.to_string(), "minimal");
531        assert_eq!(TemplateVariant::Bare.to_string(), "bare");
532    }
533
534    // ========== Template Creation Tests ==========
535
536    #[test]
537    fn test_template_from_string() {
538        let template = Template::from_string("test", "# {{ title }}");
539        assert_eq!(template.name, "test");
540        assert_eq!(template.content, "# {{ title }}");
541    }
542
543    #[test]
544    fn test_template_from_file() {
545        let temp = TempDir::new().unwrap();
546        let path = temp.path().join("custom.md");
547        std::fs::write(&path, "# {{ number }}. {{ title }}").unwrap();
548
549        let template = Template::from_file(&path).unwrap();
550        assert_eq!(template.name, "custom.md");
551        assert!(template.content.contains("{{ number }}"));
552    }
553
554    #[test]
555    fn test_template_from_file_not_found() {
556        let result = Template::from_file(Path::new("/nonexistent/template.md"));
557        assert!(result.is_err());
558    }
559
560    #[test]
561    fn test_template_builtin_nygard() {
562        let template = Template::builtin(TemplateFormat::Nygard);
563        assert_eq!(template.name, "nygard");
564        assert!(template.content.contains("## Status"));
565        assert!(template.content.contains("## Context"));
566        assert!(template.content.contains("## Decision"));
567        assert!(template.content.contains("## Consequences"));
568    }
569
570    #[test]
571    fn test_template_builtin_madr() {
572        let template = Template::builtin(TemplateFormat::Madr);
573        assert_eq!(template.name, "madr");
574        assert!(template.content.contains("Context and Problem Statement"));
575        assert!(template.content.contains("Decision Drivers"));
576        assert!(template.content.contains("Considered Options"));
577        assert!(template.content.contains("Decision Outcome"));
578    }
579
580    // ========== Template Rendering - Nygard Compatible Mode ==========
581
582    #[test]
583    fn test_render_nygard_compatible() {
584        let template = Template::builtin(TemplateFormat::Nygard);
585        let mut adr = Adr::new(1, "Use Rust");
586        adr.status = AdrStatus::Accepted;
587
588        let config = Config::default();
589        let output = template.render(&adr, &config).unwrap();
590
591        assert!(output.contains("# 1. Use Rust"));
592        assert!(output.contains("## Status"));
593        assert!(output.contains("Accepted"));
594        assert!(!output.starts_with("---")); // No frontmatter in compatible mode
595    }
596
597    #[test]
598    fn test_render_nygard_all_statuses() {
599        let template = Template::builtin(TemplateFormat::Nygard);
600        let config = Config::default();
601
602        for (status, expected_text) in [
603            (AdrStatus::Proposed, "Proposed"),
604            (AdrStatus::Accepted, "Accepted"),
605            (AdrStatus::Deprecated, "Deprecated"),
606            (AdrStatus::Superseded, "Superseded"),
607            (AdrStatus::Custom("Draft".into()), "Draft"),
608        ] {
609            let mut adr = Adr::new(1, "Test");
610            adr.status = status;
611
612            let output = template.render(&adr, &config).unwrap();
613            assert!(
614                output.contains(expected_text),
615                "Output should contain '{expected_text}': {output}"
616            );
617        }
618    }
619
620    #[test]
621    fn test_render_nygard_with_content() {
622        let template = Template::builtin(TemplateFormat::Nygard);
623        let mut adr = Adr::new(1, "Use Rust");
624        adr.status = AdrStatus::Accepted;
625        adr.context = "We need a safe language.".to_string();
626        adr.decision = "We will use Rust.".to_string();
627        adr.consequences = "Better memory safety.".to_string();
628
629        let config = Config::default();
630        let output = template.render(&adr, &config).unwrap();
631
632        assert!(output.contains("We need a safe language."));
633        assert!(output.contains("We will use Rust."));
634        assert!(output.contains("Better memory safety."));
635    }
636
637    #[test]
638    fn test_render_nygard_with_links() {
639        let template = Template::builtin(TemplateFormat::Nygard);
640        let mut adr = Adr::new(2, "Use PostgreSQL");
641        adr.status = AdrStatus::Accepted;
642        adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
643
644        let config = Config::default();
645        let output = template.render(&adr, &config).unwrap();
646
647        assert!(output.contains("Supersedes"));
648        assert!(output.contains("[1. ...]"));
649        assert!(output.contains("0001-....md"));
650    }
651
652    #[test]
653    fn test_render_nygard_with_multiple_links() {
654        let template = Template::builtin(TemplateFormat::Nygard);
655        let mut adr = Adr::new(5, "Combined Decision");
656        adr.status = AdrStatus::Accepted;
657        adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
658        adr.links.push(AdrLink::new(2, LinkKind::Amends));
659        adr.links.push(AdrLink::new(3, LinkKind::SupersededBy));
660
661        let config = Config::default();
662        let output = template.render(&adr, &config).unwrap();
663
664        assert!(output.contains("Supersedes"));
665        assert!(output.contains("Amends"));
666        assert!(output.contains("Superseded by"));
667    }
668
669    // ========== Template Rendering - Nygard NextGen Mode ==========
670
671    #[test]
672    fn test_render_nygard_ng() {
673        let template = Template::builtin(TemplateFormat::Nygard);
674        let mut adr = Adr::new(1, "Use Rust");
675        adr.status = AdrStatus::Accepted;
676
677        let config = Config {
678            mode: ConfigMode::NextGen,
679            ..Default::default()
680        };
681        let output = template.render(&adr, &config).unwrap();
682
683        assert!(output.starts_with("---")); // Has frontmatter in ng mode
684        assert!(output.contains("number: 1"));
685        assert!(output.contains("title: Use Rust"));
686        assert!(output.contains("status: accepted"));
687    }
688
689    #[test]
690    fn test_render_nygard_ng_with_links() {
691        let template = Template::builtin(TemplateFormat::Nygard);
692        let mut adr = Adr::new(2, "Test");
693        adr.status = AdrStatus::Accepted;
694        adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
695
696        let config = Config {
697            mode: ConfigMode::NextGen,
698            ..Default::default()
699        };
700        let output = template.render(&adr, &config).unwrap();
701
702        assert!(output.contains("links:"));
703        assert!(output.contains("target: 1"));
704    }
705
706    // ========== Template Rendering - MADR 4.0.0 ==========
707
708    #[test]
709    fn test_render_madr_basic() {
710        let template = Template::builtin(TemplateFormat::Madr);
711        let mut adr = Adr::new(1, "Use Rust");
712        adr.status = AdrStatus::Accepted;
713
714        let config = Config::default();
715        let output = template.render(&adr, &config).unwrap();
716
717        assert!(output.starts_with("---")); // MADR always has frontmatter
718        assert!(output.contains("status: accepted"));
719        assert!(output.contains("# Use Rust"));
720        assert!(output.contains("## Context and Problem Statement"));
721        assert!(output.contains("## Decision Drivers"));
722        assert!(output.contains("## Considered Options"));
723        assert!(output.contains("## Decision Outcome"));
724        assert!(output.contains("## Pros and Cons of the Options"));
725    }
726
727    #[test]
728    fn test_render_madr_with_decision_makers() {
729        let template = Template::builtin(TemplateFormat::Madr);
730        let mut adr = Adr::new(1, "Use Rust");
731        adr.status = AdrStatus::Accepted;
732        adr.decision_makers = vec!["Alice".into(), "Bob".into()];
733
734        let config = Config::default();
735        let output = template.render(&adr, &config).unwrap();
736
737        assert!(output.contains("decision-makers:"));
738        assert!(output.contains("  - Alice"));
739        assert!(output.contains("  - Bob"));
740    }
741
742    #[test]
743    fn test_render_madr_with_consulted() {
744        let template = Template::builtin(TemplateFormat::Madr);
745        let mut adr = Adr::new(1, "Use Rust");
746        adr.status = AdrStatus::Accepted;
747        adr.consulted = vec!["Carol".into()];
748
749        let config = Config::default();
750        let output = template.render(&adr, &config).unwrap();
751
752        assert!(output.contains("consulted:"));
753        assert!(output.contains("  - Carol"));
754    }
755
756    #[test]
757    fn test_render_madr_with_informed() {
758        let template = Template::builtin(TemplateFormat::Madr);
759        let mut adr = Adr::new(1, "Use Rust");
760        adr.status = AdrStatus::Accepted;
761        adr.informed = vec!["Dave".into(), "Eve".into()];
762
763        let config = Config::default();
764        let output = template.render(&adr, &config).unwrap();
765
766        assert!(output.contains("informed:"));
767        assert!(output.contains("  - Dave"));
768        assert!(output.contains("  - Eve"));
769    }
770
771    #[test]
772    fn test_render_madr_full_frontmatter() {
773        let template = Template::builtin(TemplateFormat::Madr);
774        let mut adr = Adr::new(1, "Use MADR Format");
775        adr.status = AdrStatus::Accepted;
776        adr.decision_makers = vec!["Alice".into(), "Bob".into()];
777        adr.consulted = vec!["Carol".into()];
778        adr.informed = vec!["Dave".into()];
779
780        let config = Config::default();
781        let output = template.render(&adr, &config).unwrap();
782
783        // Check frontmatter structure
784        assert!(output.starts_with("---\nstatus: accepted\ndate:"));
785        assert!(output.contains("decision-makers:\n  - Alice\n  - Bob"));
786        assert!(output.contains("consulted:\n  - Carol"));
787        assert!(output.contains("informed:\n  - Dave"));
788        assert!(output.contains("---\n\n# Use MADR Format"));
789    }
790
791    #[test]
792    fn test_render_madr_empty_optional_fields() {
793        let template = Template::builtin(TemplateFormat::Madr);
794        let mut adr = Adr::new(1, "Simple ADR");
795        adr.status = AdrStatus::Proposed;
796
797        let config = Config::default();
798        let output = template.render(&adr, &config).unwrap();
799
800        // Empty optional fields should not appear
801        assert!(!output.contains("decision-makers:"));
802        assert!(!output.contains("consulted:"));
803        assert!(!output.contains("informed:"));
804    }
805
806    // ========== Template Variants Tests ==========
807
808    #[test]
809    fn test_nygard_minimal_template() {
810        let template =
811            Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Minimal);
812        let adr = Adr::new(1, "Minimal Test");
813        let config = Config::default();
814        let output = template.render(&adr, &config).unwrap();
815
816        // Should have basic structure but no guidance text
817        assert!(output.contains("# 1. Minimal Test"));
818        assert!(output.contains("## Status"));
819        assert!(output.contains("## Context"));
820        assert!(output.contains("## Decision"));
821        assert!(output.contains("## Consequences"));
822        // Should NOT have guidance text
823        assert!(!output.contains("What is the issue"));
824    }
825
826    #[test]
827    fn test_nygard_bare_template() {
828        let template =
829            Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Bare);
830        let adr = Adr::new(1, "Bare Test");
831        let config = Config::default();
832        let output = template.render(&adr, &config).unwrap();
833
834        // Should have basic structure
835        assert!(output.contains("# 1. Bare Test"));
836        assert!(output.contains("## Status"));
837        assert!(output.contains("## Context"));
838        // Bare template has no frontmatter
839        assert!(!output.contains("---"));
840    }
841
842    #[test]
843    fn test_madr_minimal_template() {
844        let template =
845            Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Minimal);
846        let adr = Adr::new(1, "MADR Minimal");
847        let config = Config::default();
848        let output = template.render(&adr, &config).unwrap();
849
850        // Should have frontmatter
851        assert!(output.starts_with("---"));
852        assert!(output.contains("# MADR Minimal"));
853        assert!(output.contains("## Context and Problem Statement"));
854        assert!(output.contains("## Decision Outcome"));
855        // Should NOT have full MADR sections
856        assert!(!output.contains("## Decision Drivers"));
857        assert!(!output.contains("## Considered Options"));
858    }
859
860    #[test]
861    fn test_madr_bare_template() {
862        let template = Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Bare);
863        let adr = Adr::new(1, "MADR Bare");
864        let config = Config::default();
865        let output = template.render(&adr, &config).unwrap();
866
867        // Should have minimal frontmatter
868        assert!(output.starts_with("---"));
869        assert!(output.contains("status:"));
870        assert!(output.contains("# MADR Bare"));
871        // Should NOT have decision-makers etc in frontmatter
872        assert!(!output.contains("decision-makers"));
873    }
874
875    #[test]
876    fn test_builtin_defaults_to_full() {
877        let full = Template::builtin(TemplateFormat::Nygard);
878        let explicit_full =
879            Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Full);
880
881        assert_eq!(full.name, explicit_full.name);
882        assert_eq!(full.content, explicit_full.content);
883    }
884
885    // ========== Template Engine Tests ==========
886
887    #[test]
888    fn test_template_engine_new() {
889        let engine = TemplateEngine::new();
890        assert_eq!(engine.default_format, TemplateFormat::Nygard);
891        assert_eq!(engine.default_variant, TemplateVariant::Full);
892        assert!(engine.custom_template.is_none());
893    }
894
895    #[test]
896    fn test_template_engine_default() {
897        let engine = TemplateEngine::default();
898        assert_eq!(engine.default_format, TemplateFormat::Nygard);
899        assert_eq!(engine.default_variant, TemplateVariant::Full);
900    }
901
902    #[test]
903    fn test_template_engine_with_format() {
904        let engine = TemplateEngine::new().with_format(TemplateFormat::Madr);
905        assert_eq!(engine.default_format, TemplateFormat::Madr);
906    }
907
908    #[test]
909    fn test_template_engine_with_custom_template() {
910        let custom = Template::from_string("custom", "# {{ title }}");
911        let engine = TemplateEngine::new().with_custom_template(custom);
912        assert!(engine.custom_template.is_some());
913    }
914
915    #[test]
916    fn test_template_engine_with_custom_template_file() {
917        let temp = TempDir::new().unwrap();
918        let path = temp.path().join("template.md");
919        std::fs::write(&path, "# {{ title }}").unwrap();
920
921        let engine = TemplateEngine::new()
922            .with_custom_template_file(&path)
923            .unwrap();
924        assert!(engine.custom_template.is_some());
925    }
926
927    #[test]
928    fn test_template_engine_with_custom_template_file_not_found() {
929        let result = TemplateEngine::new().with_custom_template_file(Path::new("/nonexistent.md"));
930        assert!(result.is_err());
931    }
932
933    #[test]
934    fn test_template_engine_template_builtin() {
935        let engine = TemplateEngine::new();
936        let template = engine.template();
937        assert_eq!(template.name, "nygard");
938    }
939
940    #[test]
941    fn test_template_engine_template_custom() {
942        let custom = Template::from_string("my-template", "# Custom");
943        let engine = TemplateEngine::new().with_custom_template(custom);
944        let template = engine.template();
945        assert_eq!(template.name, "my-template");
946    }
947
948    #[test]
949    fn test_template_engine_render() {
950        let engine = TemplateEngine::new();
951        let adr = Adr::new(1, "Test");
952        let config = Config::default();
953
954        let output = engine.render(&adr, &config).unwrap();
955        assert!(output.contains("# 1. Test"));
956    }
957
958    #[test]
959    fn test_template_engine_render_custom() {
960        let custom = Template::from_string("custom", "ADR {{ number }}: {{ title }}");
961        let engine = TemplateEngine::new().with_custom_template(custom);
962        let adr = Adr::new(42, "Custom ADR");
963        let config = Config::default();
964
965        let output = engine.render(&adr, &config).unwrap();
966        assert_eq!(output, "ADR 42: Custom ADR");
967    }
968
969    // ========== Custom Template Tests ==========
970
971    #[test]
972    fn test_custom_template_all_fields() {
973        let custom = Template::from_string(
974            "full",
975            r#"# {{ number }}. {{ title }}
976Date: {{ date }}
977Status: {{ status }}
978Context: {{ context }}
979Decision: {{ decision }}
980Consequences: {{ consequences }}
981Links: {% for link in links %}{{ link.kind }} {{ link.target }}{% endfor %}"#,
982        );
983
984        let mut adr = Adr::new(1, "Test");
985        adr.status = AdrStatus::Accepted;
986        adr.context = "Context text".into();
987        adr.decision = "Decision text".into();
988        adr.consequences = "Consequences text".into();
989        adr.links.push(AdrLink::new(2, LinkKind::Amends));
990
991        let config = Config::default();
992        let output = custom.render(&adr, &config).unwrap();
993
994        assert!(output.contains("# 1. Test"));
995        assert!(output.contains("Status: Accepted"));
996        assert!(output.contains("Context: Context text"));
997        assert!(output.contains("Decision: Decision text"));
998        assert!(output.contains("Consequences: Consequences text"));
999        assert!(output.contains("Amends 2"));
1000    }
1001
1002    #[test]
1003    fn test_custom_template_is_ng_flag() {
1004        let custom = Template::from_string(
1005            "ng-check",
1006            r#"{% if is_ng %}NextGen Mode{% else %}Compatible Mode{% endif %}"#,
1007        );
1008
1009        let adr = Adr::new(1, "Test");
1010
1011        let compat_config = Config::default();
1012        let output = custom.render(&adr, &compat_config).unwrap();
1013        assert_eq!(output, "Compatible Mode");
1014
1015        let ng_config = Config {
1016            mode: ConfigMode::NextGen,
1017            ..Default::default()
1018        };
1019        let output = custom.render(&adr, &ng_config).unwrap();
1020        assert_eq!(output, "NextGen Mode");
1021    }
1022
1023    #[test]
1024    fn test_custom_template_link_kinds() {
1025        let custom = Template::from_string(
1026            "links",
1027            r#"{% for link in links %}{{ link.kind }}|{% endfor %}"#,
1028        );
1029
1030        let mut adr = Adr::new(1, "Test");
1031        adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1032        adr.links.push(AdrLink::new(2, LinkKind::SupersededBy));
1033        adr.links.push(AdrLink::new(3, LinkKind::Amends));
1034        adr.links.push(AdrLink::new(4, LinkKind::AmendedBy));
1035        adr.links.push(AdrLink::new(5, LinkKind::RelatesTo));
1036        adr.links
1037            .push(AdrLink::new(6, LinkKind::Custom("Depends on".into())));
1038
1039        let config = Config::default();
1040        let output = custom.render(&adr, &config).unwrap();
1041
1042        assert!(output.contains("Supersedes|"));
1043        assert!(output.contains("Superseded by|"));
1044        assert!(output.contains("Amends|"));
1045        assert!(output.contains("Amended by|"));
1046        assert!(output.contains("Relates to|"));
1047        assert!(output.contains("Depends on|"));
1048    }
1049
1050    // ========== Error Cases ==========
1051
1052    #[test]
1053    fn test_template_invalid_syntax() {
1054        let custom = Template::from_string("invalid", "{{ unclosed");
1055        let adr = Adr::new(1, "Test");
1056        let config = Config::default();
1057
1058        let result = custom.render(&adr, &config);
1059        assert!(result.is_err());
1060    }
1061
1062    #[test]
1063    fn test_template_undefined_variable() {
1064        let custom = Template::from_string("undefined", "{{ nonexistent }}");
1065        let adr = Adr::new(1, "Test");
1066        let config = Config::default();
1067
1068        // minijinja treats undefined as empty string by default
1069        let result = custom.render(&adr, &config);
1070        assert!(result.is_ok());
1071    }
1072
1073    // ========== Large Number Formatting ==========
1074
1075    #[test]
1076    fn test_render_four_digit_number() {
1077        let template = Template::builtin(TemplateFormat::Nygard);
1078        let adr = Adr::new(9999, "Large Number");
1079        let config = Config::default();
1080
1081        let output = template.render(&adr, &config).unwrap();
1082        assert!(output.contains("# 9999. Large Number"));
1083    }
1084
1085    #[test]
1086    fn test_render_link_number_formatting() {
1087        let template = Template::builtin(TemplateFormat::Nygard);
1088        let mut adr = Adr::new(2, "Test");
1089        adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1090
1091        let config = Config::default();
1092        let output = template.render(&adr, &config).unwrap();
1093
1094        // Link should use 4-digit padding
1095        assert!(output.contains("0001-"));
1096    }
1097}