1use crate::{Adr, Config, Error, Result};
4use minijinja::{Environment, context};
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub enum TemplateFormat {
10 #[default]
12 Nygard,
13
14 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
47pub enum TemplateVariant {
48 #[default]
50 Full,
51
52 Minimal,
54
55 Bare,
57
58 BareMinimal,
60}
61
62impl std::fmt::Display for TemplateVariant {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::Full => write!(f, "full"),
66 Self::Minimal => write!(f, "minimal"),
67 Self::Bare => write!(f, "bare"),
68 Self::BareMinimal => write!(f, "bare-minimal"),
69 }
70 }
71}
72
73impl std::str::FromStr for TemplateVariant {
74 type Err = Error;
75
76 fn from_str(s: &str) -> Result<Self> {
77 match s.to_lowercase().replace('_', "-").as_str() {
78 "full" | "default" => Ok(Self::Full),
79 "minimal" | "min" => Ok(Self::Minimal),
80 "bare" => Ok(Self::Bare),
81 "bare-minimal" | "bareminimal" | "empty" => Ok(Self::BareMinimal),
82 _ => Err(Error::TemplateNotFound(format!("Unknown variant: {s}"))),
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct Template {
90 content: String,
92
93 name: String,
95}
96
97impl Template {
98 pub fn from_string(name: impl Into<String>, content: impl Into<String>) -> Self {
100 Self {
101 name: name.into(),
102 content: content.into(),
103 }
104 }
105
106 pub fn content(&self) -> &str {
108 &self.content
109 }
110
111 pub fn name(&self) -> &str {
113 &self.name
114 }
115
116 pub fn from_file(path: &Path) -> Result<Self> {
118 let content = std::fs::read_to_string(path)?;
119 let name = path
120 .file_name()
121 .and_then(|n| n.to_str())
122 .unwrap_or("custom")
123 .to_string();
124 Ok(Self { name, content })
125 }
126
127 pub fn builtin(format: TemplateFormat) -> Self {
129 Self::builtin_with_variant(format, TemplateVariant::Full)
130 }
131
132 pub fn builtin_with_variant(format: TemplateFormat, variant: TemplateVariant) -> Self {
134 let (name, content) = match (format, variant) {
135 (TemplateFormat::Nygard, TemplateVariant::Full) => ("nygard", NYGARD_TEMPLATE),
137 (TemplateFormat::Nygard, TemplateVariant::Minimal) => {
138 ("nygard-minimal", NYGARD_MINIMAL_TEMPLATE)
139 }
140 (TemplateFormat::Nygard, TemplateVariant::Bare) => {
141 ("nygard-bare", NYGARD_BARE_TEMPLATE)
142 }
143 (TemplateFormat::Nygard, TemplateVariant::BareMinimal) => {
144 ("nygard-bare-minimal", NYGARD_BARE_MINIMAL_TEMPLATE)
145 }
146
147 (TemplateFormat::Madr, TemplateVariant::Full) => ("madr", MADR_TEMPLATE),
149 (TemplateFormat::Madr, TemplateVariant::Minimal) => {
150 ("madr-minimal", MADR_MINIMAL_TEMPLATE)
151 }
152 (TemplateFormat::Madr, TemplateVariant::Bare) => ("madr-bare", MADR_BARE_TEMPLATE),
153 (TemplateFormat::Madr, TemplateVariant::BareMinimal) => {
154 ("madr-bare-minimal", MADR_BARE_MINIMAL_TEMPLATE)
155 }
156 };
157 Self::from_string(name, content)
158 }
159
160 pub fn render(&self, adr: &Adr, config: &Config) -> Result<String> {
162 use crate::LinkKind;
163
164 let mut env = Environment::new();
165 env.add_template(&self.name, &self.content)
166 .map_err(|e| Error::TemplateError(e.to_string()))?;
167
168 let template = env
169 .get_template(&self.name)
170 .map_err(|e| Error::TemplateError(e.to_string()))?;
171
172 let links: Vec<_> = adr
174 .links
175 .iter()
176 .map(|link| {
177 let kind_display = match &link.kind {
178 LinkKind::Supersedes => "Supersedes",
179 LinkKind::SupersededBy => "Superseded by",
180 LinkKind::Amends => "Amends",
181 LinkKind::AmendedBy => "Amended by",
182 LinkKind::RelatesTo => "Relates to",
183 LinkKind::Custom(s) => s.as_str(),
184 };
185 context! {
186 target => link.target,
187 kind => kind_display,
188 description => &link.description,
189 }
190 })
191 .collect();
192
193 let output = template
194 .render(context! {
195 number => adr.number,
196 title => &adr.title,
197 date => crate::parse::format_date(adr.date),
198 status => adr.status.to_string(),
199 context => &adr.context,
200 decision => &adr.decision,
201 consequences => &adr.consequences,
202 links => links,
203 tags => &adr.tags,
204 is_ng => config.is_next_gen(),
205 decision_makers => &adr.decision_makers,
207 consulted => &adr.consulted,
208 informed => &adr.informed,
209 })
210 .map_err(|e| Error::TemplateError(e.to_string()))?;
211
212 Ok(output)
213 }
214}
215
216#[derive(Debug)]
218pub struct TemplateEngine {
219 default_format: TemplateFormat,
221
222 default_variant: TemplateVariant,
224
225 custom_template: Option<Template>,
227}
228
229impl Default for TemplateEngine {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235impl TemplateEngine {
236 pub fn new() -> Self {
238 Self {
239 default_format: TemplateFormat::default(),
240 default_variant: TemplateVariant::default(),
241 custom_template: None,
242 }
243 }
244
245 pub fn with_format(mut self, format: TemplateFormat) -> Self {
247 self.default_format = format;
248 self
249 }
250
251 pub fn with_variant(mut self, variant: TemplateVariant) -> Self {
253 self.default_variant = variant;
254 self
255 }
256
257 pub fn with_custom_template(mut self, template: Template) -> Self {
259 self.custom_template = Some(template);
260 self
261 }
262
263 pub fn with_custom_template_file(mut self, path: &Path) -> Result<Self> {
265 self.custom_template = Some(Template::from_file(path)?);
266 Ok(self)
267 }
268
269 pub fn template(&self) -> Template {
271 self.custom_template.clone().unwrap_or_else(|| {
272 Template::builtin_with_variant(self.default_format, self.default_variant)
273 })
274 }
275
276 pub fn render(&self, adr: &Adr, config: &Config) -> Result<String> {
278 self.template().render(adr, config)
279 }
280}
281
282const NYGARD_TEMPLATE: &str = r#"{% if is_ng %}---
284number: {{ number }}
285title: {{ title }}
286date: {{ date }}
287status: {{ status | lower }}
288{% if links %}links:
289{% for link in links %} - target: {{ link.target }}
290 kind: {{ link.kind | lower }}
291{% endfor %}{% endif %}{% if tags %}tags:
292{% for tag in tags %} - {{ tag }}
293{% endfor %}{% endif %}---
294
295{% endif %}# {{ number }}. {{ title }}
296
297Date: {{ date }}
298
299## Status
300
301{{ status }}
302{% for link in links %}
303{{ link.kind }} [{{ link.target }}. ...]({{ "%04d" | format(link.target) }}-....md)
304{% endfor %}
305## Context
306
307{{ context if context else "What is the issue that we're seeing that is motivating this decision or change?" }}
308
309## Decision
310
311{{ decision if decision else "What is the change that we're proposing and/or doing?" }}
312
313## Consequences
314
315{{ consequences if consequences else "What becomes easier or more difficult to do because of this change?" }}
316"#;
317
318const MADR_TEMPLATE: &str = r#"---
320number: {{ number }}
321title: {{ title }}
322status: {{ status | lower }}
323date: {{ date }}
324{% if decision_makers %}decision-makers:
325{% for dm in decision_makers %} - {{ dm }}
326{% endfor %}{% endif %}{% if consulted %}consulted:
327{% for c in consulted %} - {{ c }}
328{% endfor %}{% endif %}{% if informed %}informed:
329{% for i in informed %} - {{ i }}
330{% endfor %}{% endif %}{% if tags %}tags:
331{% for tag in tags %} - {{ tag }}
332{% endfor %}{% endif %}---
333
334# {{ title }}
335
336## Context and Problem Statement
337
338{{ 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.}" }}
339
340<!-- This is an optional element. Feel free to remove. -->
341## Decision Drivers
342
343* {decision driver 1, e.g., a force, facing concern, ...}
344* {decision driver 2, e.g., a force, facing concern, ...}
345* ... <!-- numbers of drivers can vary -->
346
347## Considered Options
348
349* {title of option 1}
350* {title of option 2}
351* {title of option 3}
352* ... <!-- numbers of options can vary -->
353
354## Decision Outcome
355
356{{ 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)}." }}
357
358<!-- This is an optional element. Feel free to remove. -->
359### Consequences
360
361{{ 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 -->" }}
362
363<!-- This is an optional element. Feel free to remove. -->
364### Confirmation
365
366{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.}
367
368<!-- This is an optional element. Feel free to remove. -->
369## Pros and Cons of the Options
370
371### {title of option 1}
372
373<!-- This is an optional element. Feel free to remove. -->
374{example | description | pointer to more information | ...}
375
376* Good, because {argument a}
377* Good, because {argument b}
378<!-- use "neutral" if the given argument weights neither for good nor bad -->
379* Neutral, because {argument c}
380* Bad, because {argument d}
381* ... <!-- numbers of pros and cons can vary -->
382
383### {title of other option}
384
385{example | description | pointer to more information | ...}
386
387* Good, because {argument a}
388* Good, because {argument b}
389* Neutral, because {argument c}
390* Bad, because {argument d}
391* ...
392
393<!-- This is an optional element. Feel free to remove. -->
394## More Information
395
396{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.}
397"#;
398
399const NYGARD_MINIMAL_TEMPLATE: &str = r#"{% if is_ng %}---
401number: {{ number }}
402title: {{ title }}
403date: {{ date }}
404status: {{ status | lower }}
405{% if links %}links:
406{% for link in links %} - target: {{ link.target }}
407 kind: {{ link.kind | lower }}
408{% endfor %}{% endif %}{% if tags %}tags:
409{% for tag in tags %} - {{ tag }}
410{% endfor %}{% endif %}---
411
412{% endif %}# {{ number }}. {{ title }}
413
414Date: {{ date }}
415
416## Status
417
418{{ status }}
419{% for link in links %}
420{{ link.kind }} [{{ link.target }}. ...]({{ "%04d" | format(link.target) }}-....md)
421{% endfor %}
422## Context
423
424{{ context if context else "" }}
425
426## Decision
427
428{{ decision if decision else "" }}
429
430## Consequences
431
432{{ consequences if consequences else "" }}
433"#;
434
435const NYGARD_BARE_TEMPLATE: &str = r#"# {{ number }}. {{ title }}
437
438Date: {{ date }}
439
440## Status
441
442{{ status }}
443
444## Context
445
446
447
448## Decision
449
450
451
452## Consequences
453
454"#;
455
456const NYGARD_BARE_MINIMAL_TEMPLATE: &str = r#"# {{ number }}. {{ title }}
458
459## Status
460
461{{ status }}
462
463## Context
464
465
466
467## Decision
468
469
470
471## Consequences
472
473"#;
474
475const MADR_MINIMAL_TEMPLATE: &str = r#"# {{ title }}
478
479## Context and Problem Statement
480
481{{ 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.}" }}
482
483## Considered Options
484
485* {title of option 1}
486* {title of option 2}
487* {title of option 3}
488* ... <!-- numbers of options can vary -->
489
490## Decision Outcome
491
492{{ 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)}." }}
493
494<!-- This is an optional element. Feel free to remove. -->
495### Consequences
496
497{{ 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 -->" }}
498"#;
499
500const MADR_BARE_TEMPLATE: &str = r#"---
503number: {{ number }}
504title: {{ title }}
505status: {{ status | lower }}
506date: {{ date }}
507decision-makers:
508consulted:
509informed:
510{% if tags %}tags:
511{% for tag in tags %} - {{ tag }}
512{% endfor %}{% else %}tags:
513{% endif %}---
514
515# {{ title }}
516
517## Context and Problem Statement
518
519
520
521## Decision Drivers
522
523* <!-- decision driver -->
524
525## Considered Options
526
527* <!-- option -->
528
529## Decision Outcome
530
531Chosen option: "", because
532
533### Consequences
534
535* Good, because
536* Bad, because
537
538### Confirmation
539
540
541
542## Pros and Cons of the Options
543
544### <!-- title of option -->
545
546* Good, because
547* Neutral, because
548* Bad, because
549
550## More Information
551
552"#;
553
554const MADR_BARE_MINIMAL_TEMPLATE: &str = r#"# {{ title }}
557
558## Context and Problem Statement
559
560
561
562## Considered Options
563
564
565
566## Decision Outcome
567
568
569
570### Consequences
571
572"#;
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use crate::{AdrLink, AdrStatus, ConfigMode, LinkKind};
578 use tempfile::TempDir;
579 use test_case::test_case;
580
581 #[test]
584 fn test_template_format_default() {
585 assert_eq!(TemplateFormat::default(), TemplateFormat::Nygard);
586 }
587
588 #[test_case("nygard" => TemplateFormat::Nygard; "nygard")]
589 #[test_case("Nygard" => TemplateFormat::Nygard; "nygard capitalized")]
590 #[test_case("NYGARD" => TemplateFormat::Nygard; "nygard uppercase")]
591 #[test_case("default" => TemplateFormat::Nygard; "default alias")]
592 #[test_case("madr" => TemplateFormat::Madr; "madr")]
593 #[test_case("MADR" => TemplateFormat::Madr; "madr uppercase")]
594 fn test_template_format_parse(input: &str) -> TemplateFormat {
595 input.parse().unwrap()
596 }
597
598 #[test]
599 fn test_template_format_parse_unknown() {
600 let result: Result<TemplateFormat> = "unknown".parse();
601 assert!(result.is_err());
602 }
603
604 #[test]
605 fn test_template_format_display() {
606 assert_eq!(TemplateFormat::Nygard.to_string(), "nygard");
607 assert_eq!(TemplateFormat::Madr.to_string(), "madr");
608 }
609
610 #[test]
613 fn test_template_variant_default() {
614 assert_eq!(TemplateVariant::default(), TemplateVariant::Full);
615 }
616
617 #[test_case("full" => TemplateVariant::Full; "full")]
618 #[test_case("Full" => TemplateVariant::Full; "full capitalized")]
619 #[test_case("default" => TemplateVariant::Full; "default alias")]
620 #[test_case("minimal" => TemplateVariant::Minimal; "minimal")]
621 #[test_case("min" => TemplateVariant::Minimal; "min alias")]
622 #[test_case("bare" => TemplateVariant::Bare; "bare")]
623 #[test_case("bare-minimal" => TemplateVariant::BareMinimal; "bare-minimal")]
624 #[test_case("bareminimal" => TemplateVariant::BareMinimal; "bareminimal")]
625 #[test_case("empty" => TemplateVariant::BareMinimal; "empty alias")]
626 fn test_template_variant_parse(input: &str) -> TemplateVariant {
627 input.parse().unwrap()
628 }
629
630 #[test]
631 fn test_template_variant_parse_unknown() {
632 let result: Result<TemplateVariant> = "unknown".parse();
633 assert!(result.is_err());
634 }
635
636 #[test]
637 fn test_template_variant_display() {
638 assert_eq!(TemplateVariant::Full.to_string(), "full");
639 assert_eq!(TemplateVariant::Minimal.to_string(), "minimal");
640 assert_eq!(TemplateVariant::Bare.to_string(), "bare");
641 assert_eq!(TemplateVariant::BareMinimal.to_string(), "bare-minimal");
642 }
643
644 #[test]
647 fn test_template_from_string() {
648 let template = Template::from_string("test", "# {{ title }}");
649 assert_eq!(template.name, "test");
650 assert_eq!(template.content, "# {{ title }}");
651 }
652
653 #[test]
654 fn test_template_from_file() {
655 let temp = TempDir::new().unwrap();
656 let path = temp.path().join("custom.md");
657 std::fs::write(&path, "# {{ number }}. {{ title }}").unwrap();
658
659 let template = Template::from_file(&path).unwrap();
660 assert_eq!(template.name, "custom.md");
661 assert!(template.content.contains("{{ number }}"));
662 }
663
664 #[test]
665 fn test_template_from_file_not_found() {
666 let result = Template::from_file(Path::new("/nonexistent/template.md"));
667 assert!(result.is_err());
668 }
669
670 #[test]
671 fn test_template_builtin_nygard() {
672 let template = Template::builtin(TemplateFormat::Nygard);
673 assert_eq!(template.name, "nygard");
674 assert!(template.content.contains("## Status"));
675 assert!(template.content.contains("## Context"));
676 assert!(template.content.contains("## Decision"));
677 assert!(template.content.contains("## Consequences"));
678 }
679
680 #[test]
681 fn test_template_builtin_madr() {
682 let template = Template::builtin(TemplateFormat::Madr);
683 assert_eq!(template.name, "madr");
684 assert!(template.content.contains("Context and Problem Statement"));
685 assert!(template.content.contains("Decision Drivers"));
686 assert!(template.content.contains("Considered Options"));
687 assert!(template.content.contains("Decision Outcome"));
688 }
689
690 #[test]
693 fn test_render_nygard_compatible() {
694 let template = Template::builtin(TemplateFormat::Nygard);
695 let mut adr = Adr::new(1, "Use Rust");
696 adr.status = AdrStatus::Accepted;
697
698 let config = Config::default();
699 let output = template.render(&adr, &config).unwrap();
700
701 assert!(output.contains("# 1. Use Rust"));
702 assert!(output.contains("## Status"));
703 assert!(output.contains("Accepted"));
704 assert!(!output.starts_with("---")); }
706
707 #[test]
708 fn test_render_nygard_all_statuses() {
709 let template = Template::builtin(TemplateFormat::Nygard);
710 let config = Config::default();
711
712 for (status, expected_text) in [
713 (AdrStatus::Proposed, "Proposed"),
714 (AdrStatus::Accepted, "Accepted"),
715 (AdrStatus::Deprecated, "Deprecated"),
716 (AdrStatus::Superseded, "Superseded"),
717 (AdrStatus::Custom("Draft".into()), "Draft"),
718 ] {
719 let mut adr = Adr::new(1, "Test");
720 adr.status = status;
721
722 let output = template.render(&adr, &config).unwrap();
723 assert!(
724 output.contains(expected_text),
725 "Output should contain '{expected_text}': {output}"
726 );
727 }
728 }
729
730 #[test]
731 fn test_render_nygard_with_content() {
732 let template = Template::builtin(TemplateFormat::Nygard);
733 let mut adr = Adr::new(1, "Use Rust");
734 adr.status = AdrStatus::Accepted;
735 adr.context = "We need a safe language.".to_string();
736 adr.decision = "We will use Rust.".to_string();
737 adr.consequences = "Better memory safety.".to_string();
738
739 let config = Config::default();
740 let output = template.render(&adr, &config).unwrap();
741
742 assert!(output.contains("We need a safe language."));
743 assert!(output.contains("We will use Rust."));
744 assert!(output.contains("Better memory safety."));
745 }
746
747 #[test]
748 fn test_render_nygard_with_links() {
749 let template = Template::builtin(TemplateFormat::Nygard);
750 let mut adr = Adr::new(2, "Use PostgreSQL");
751 adr.status = AdrStatus::Accepted;
752 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
753
754 let config = Config::default();
755 let output = template.render(&adr, &config).unwrap();
756
757 assert!(output.contains("Supersedes"));
758 assert!(output.contains("[1. ...]"));
759 assert!(output.contains("0001-....md"));
760 }
761
762 #[test]
763 fn test_render_nygard_with_multiple_links() {
764 let template = Template::builtin(TemplateFormat::Nygard);
765 let mut adr = Adr::new(5, "Combined Decision");
766 adr.status = AdrStatus::Accepted;
767 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
768 adr.links.push(AdrLink::new(2, LinkKind::Amends));
769 adr.links.push(AdrLink::new(3, LinkKind::SupersededBy));
770
771 let config = Config::default();
772 let output = template.render(&adr, &config).unwrap();
773
774 assert!(output.contains("Supersedes"));
775 assert!(output.contains("Amends"));
776 assert!(output.contains("Superseded by"));
777 }
778
779 #[test]
782 fn test_render_nygard_ng() {
783 let template = Template::builtin(TemplateFormat::Nygard);
784 let mut adr = Adr::new(1, "Use Rust");
785 adr.status = AdrStatus::Accepted;
786
787 let config = Config {
788 mode: ConfigMode::NextGen,
789 ..Default::default()
790 };
791 let output = template.render(&adr, &config).unwrap();
792
793 assert!(output.starts_with("---")); assert!(output.contains("number: 1"));
795 assert!(output.contains("title: Use Rust"));
796 assert!(output.contains("status: accepted"));
797 }
798
799 #[test]
800 fn test_render_nygard_ng_with_links() {
801 let template = Template::builtin(TemplateFormat::Nygard);
802 let mut adr = Adr::new(2, "Test");
803 adr.status = AdrStatus::Accepted;
804 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
805
806 let config = Config {
807 mode: ConfigMode::NextGen,
808 ..Default::default()
809 };
810 let output = template.render(&adr, &config).unwrap();
811
812 assert!(output.contains("links:"));
813 assert!(output.contains("target: 1"));
814 }
815
816 #[test]
819 fn test_render_madr_basic() {
820 let template = Template::builtin(TemplateFormat::Madr);
821 let mut adr = Adr::new(1, "Use Rust");
822 adr.status = AdrStatus::Accepted;
823
824 let config = Config::default();
825 let output = template.render(&adr, &config).unwrap();
826
827 assert!(output.starts_with("---")); assert!(output.contains("status: accepted"));
829 assert!(output.contains("# Use Rust"));
830 assert!(output.contains("## Context and Problem Statement"));
831 assert!(output.contains("## Decision Drivers"));
832 assert!(output.contains("## Considered Options"));
833 assert!(output.contains("## Decision Outcome"));
834 assert!(output.contains("## Pros and Cons of the Options"));
835 }
836
837 #[test]
838 fn test_render_madr_with_decision_makers() {
839 let template = Template::builtin(TemplateFormat::Madr);
840 let mut adr = Adr::new(1, "Use Rust");
841 adr.status = AdrStatus::Accepted;
842 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
843
844 let config = Config::default();
845 let output = template.render(&adr, &config).unwrap();
846
847 assert!(output.contains("decision-makers:"));
848 assert!(output.contains(" - Alice"));
849 assert!(output.contains(" - Bob"));
850 }
851
852 #[test]
853 fn test_render_madr_with_consulted() {
854 let template = Template::builtin(TemplateFormat::Madr);
855 let mut adr = Adr::new(1, "Use Rust");
856 adr.status = AdrStatus::Accepted;
857 adr.consulted = vec!["Carol".into()];
858
859 let config = Config::default();
860 let output = template.render(&adr, &config).unwrap();
861
862 assert!(output.contains("consulted:"));
863 assert!(output.contains(" - Carol"));
864 }
865
866 #[test]
867 fn test_render_madr_with_informed() {
868 let template = Template::builtin(TemplateFormat::Madr);
869 let mut adr = Adr::new(1, "Use Rust");
870 adr.status = AdrStatus::Accepted;
871 adr.informed = vec!["Dave".into(), "Eve".into()];
872
873 let config = Config::default();
874 let output = template.render(&adr, &config).unwrap();
875
876 assert!(output.contains("informed:"));
877 assert!(output.contains(" - Dave"));
878 assert!(output.contains(" - Eve"));
879 }
880
881 #[test]
882 fn test_render_madr_full_frontmatter() {
883 let template = Template::builtin(TemplateFormat::Madr);
884 let mut adr = Adr::new(1, "Use MADR Format");
885 adr.status = AdrStatus::Accepted;
886 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
887 adr.consulted = vec!["Carol".into()];
888 adr.informed = vec!["Dave".into()];
889
890 let config = Config::default();
891 let output = template.render(&adr, &config).unwrap();
892
893 assert!(
895 output.starts_with("---\nnumber: 1\ntitle: Use MADR Format\nstatus: accepted\ndate:")
896 );
897 assert!(output.contains("decision-makers:\n - Alice\n - Bob"));
898 assert!(output.contains("consulted:\n - Carol"));
899 assert!(output.contains("informed:\n - Dave"));
900 assert!(output.contains("---\n\n# Use MADR Format"));
901 }
902
903 #[test]
904 fn test_render_madr_empty_optional_fields() {
905 let template = Template::builtin(TemplateFormat::Madr);
906 let mut adr = Adr::new(1, "Simple ADR");
907 adr.status = AdrStatus::Proposed;
908
909 let config = Config::default();
910 let output = template.render(&adr, &config).unwrap();
911
912 assert!(!output.contains("decision-makers:"));
914 assert!(!output.contains("consulted:"));
915 assert!(!output.contains("informed:"));
916 }
917
918 #[test]
921 fn test_nygard_minimal_template() {
922 let template =
923 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Minimal);
924 let adr = Adr::new(1, "Minimal Test");
925 let config = Config::default();
926 let output = template.render(&adr, &config).unwrap();
927
928 assert!(output.contains("# 1. Minimal Test"));
930 assert!(output.contains("## Status"));
931 assert!(output.contains("## Context"));
932 assert!(output.contains("## Decision"));
933 assert!(output.contains("## Consequences"));
934 assert!(!output.contains("What is the issue"));
936 }
937
938 #[test]
939 fn test_nygard_bare_template() {
940 let template =
941 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Bare);
942 let adr = Adr::new(1, "Bare Test");
943 let config = Config::default();
944 let output = template.render(&adr, &config).unwrap();
945
946 assert!(output.contains("# 1. Bare Test"));
948 assert!(output.contains("## Status"));
949 assert!(output.contains("## Context"));
950 assert!(!output.contains("---"));
952 }
953
954 #[test]
955 fn test_madr_minimal_template() {
956 let template =
957 Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Minimal);
958 let adr = Adr::new(1, "MADR Minimal");
959 let config = Config::default();
960 let output = template.render(&adr, &config).unwrap();
961
962 assert!(!output.starts_with("---"));
964 assert!(output.contains("# MADR Minimal"));
965 assert!(output.contains("## Context and Problem Statement"));
966 assert!(output.contains("## Considered Options"));
967 assert!(output.contains("## Decision Outcome"));
968 assert!(!output.contains("## Decision Drivers"));
970 assert!(!output.contains("## Pros and Cons"));
971 }
972
973 #[test]
974 fn test_madr_bare_template() {
975 let template = Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Bare);
976 let adr = Adr::new(1, "MADR Bare");
977 let config = Config::default();
978 let output = template.render(&adr, &config).unwrap();
979
980 assert!(output.starts_with("---"));
982 assert!(output.contains("status:"));
983 assert!(output.contains("decision-makers:"));
984 assert!(output.contains("consulted:"));
985 assert!(output.contains("informed:"));
986 assert!(output.contains("# MADR Bare"));
987 assert!(output.contains("## Decision Drivers"));
989 assert!(output.contains("## Considered Options"));
990 assert!(output.contains("## Pros and Cons of the Options"));
991 assert!(output.contains("## More Information"));
992 }
993
994 #[test]
995 fn test_madr_bare_minimal_template() {
996 let template =
997 Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::BareMinimal);
998 let adr = Adr::new(1, "MADR Bare Minimal");
999 let config = Config::default();
1000 let output = template.render(&adr, &config).unwrap();
1001
1002 assert!(!output.starts_with("---"));
1004 assert!(output.contains("# MADR Bare Minimal"));
1005 assert!(output.contains("## Context and Problem Statement"));
1006 assert!(output.contains("## Considered Options"));
1007 assert!(output.contains("## Decision Outcome"));
1008 assert!(output.contains("### Consequences"));
1009 assert!(!output.contains("## Decision Drivers"));
1011 assert!(!output.contains("## Pros and Cons"));
1012 }
1013
1014 #[test]
1015 fn test_nygard_bare_minimal_template() {
1016 let template =
1017 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::BareMinimal);
1018 let adr = Adr::new(1, "Nygard Bare Minimal");
1019 let config = Config::default();
1020 let output = template.render(&adr, &config).unwrap();
1021
1022 assert!(output.contains("# 1. Nygard Bare Minimal"));
1024 assert!(output.contains("## Status"));
1025 assert!(output.contains("## Context"));
1026 assert!(output.contains("## Decision"));
1027 assert!(output.contains("## Consequences"));
1028 assert!(!output.contains("---"));
1030 assert!(!output.contains("Date:"));
1031 }
1032
1033 #[test]
1034 fn test_builtin_defaults_to_full() {
1035 let full = Template::builtin(TemplateFormat::Nygard);
1036 let explicit_full =
1037 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Full);
1038
1039 assert_eq!(full.name, explicit_full.name);
1040 assert_eq!(full.content, explicit_full.content);
1041 }
1042
1043 #[test]
1046 fn test_template_engine_new() {
1047 let engine = TemplateEngine::new();
1048 assert_eq!(engine.default_format, TemplateFormat::Nygard);
1049 assert_eq!(engine.default_variant, TemplateVariant::Full);
1050 assert!(engine.custom_template.is_none());
1051 }
1052
1053 #[test]
1054 fn test_template_engine_default() {
1055 let engine = TemplateEngine::default();
1056 assert_eq!(engine.default_format, TemplateFormat::Nygard);
1057 assert_eq!(engine.default_variant, TemplateVariant::Full);
1058 }
1059
1060 #[test]
1061 fn test_template_engine_with_format() {
1062 let engine = TemplateEngine::new().with_format(TemplateFormat::Madr);
1063 assert_eq!(engine.default_format, TemplateFormat::Madr);
1064 }
1065
1066 #[test]
1067 fn test_template_engine_with_custom_template() {
1068 let custom = Template::from_string("custom", "# {{ title }}");
1069 let engine = TemplateEngine::new().with_custom_template(custom);
1070 assert!(engine.custom_template.is_some());
1071 }
1072
1073 #[test]
1074 fn test_template_engine_with_custom_template_file() {
1075 let temp = TempDir::new().unwrap();
1076 let path = temp.path().join("template.md");
1077 std::fs::write(&path, "# {{ title }}").unwrap();
1078
1079 let engine = TemplateEngine::new()
1080 .with_custom_template_file(&path)
1081 .unwrap();
1082 assert!(engine.custom_template.is_some());
1083 }
1084
1085 #[test]
1086 fn test_template_engine_with_custom_template_file_not_found() {
1087 let result = TemplateEngine::new().with_custom_template_file(Path::new("/nonexistent.md"));
1088 assert!(result.is_err());
1089 }
1090
1091 #[test]
1092 fn test_template_engine_template_builtin() {
1093 let engine = TemplateEngine::new();
1094 let template = engine.template();
1095 assert_eq!(template.name, "nygard");
1096 }
1097
1098 #[test]
1099 fn test_template_engine_template_custom() {
1100 let custom = Template::from_string("my-template", "# Custom");
1101 let engine = TemplateEngine::new().with_custom_template(custom);
1102 let template = engine.template();
1103 assert_eq!(template.name, "my-template");
1104 }
1105
1106 #[test]
1107 fn test_template_engine_render() {
1108 let engine = TemplateEngine::new();
1109 let adr = Adr::new(1, "Test");
1110 let config = Config::default();
1111
1112 let output = engine.render(&adr, &config).unwrap();
1113 assert!(output.contains("# 1. Test"));
1114 }
1115
1116 #[test]
1117 fn test_template_engine_render_custom() {
1118 let custom = Template::from_string("custom", "ADR {{ number }}: {{ title }}");
1119 let engine = TemplateEngine::new().with_custom_template(custom);
1120 let adr = Adr::new(42, "Custom ADR");
1121 let config = Config::default();
1122
1123 let output = engine.render(&adr, &config).unwrap();
1124 assert_eq!(output, "ADR 42: Custom ADR");
1125 }
1126
1127 #[test]
1130 fn test_custom_template_all_fields() {
1131 let custom = Template::from_string(
1132 "full",
1133 r#"# {{ number }}. {{ title }}
1134Date: {{ date }}
1135Status: {{ status }}
1136Context: {{ context }}
1137Decision: {{ decision }}
1138Consequences: {{ consequences }}
1139Links: {% for link in links %}{{ link.kind }} {{ link.target }}{% endfor %}"#,
1140 );
1141
1142 let mut adr = Adr::new(1, "Test");
1143 adr.status = AdrStatus::Accepted;
1144 adr.context = "Context text".into();
1145 adr.decision = "Decision text".into();
1146 adr.consequences = "Consequences text".into();
1147 adr.links.push(AdrLink::new(2, LinkKind::Amends));
1148
1149 let config = Config::default();
1150 let output = custom.render(&adr, &config).unwrap();
1151
1152 assert!(output.contains("# 1. Test"));
1153 assert!(output.contains("Status: Accepted"));
1154 assert!(output.contains("Context: Context text"));
1155 assert!(output.contains("Decision: Decision text"));
1156 assert!(output.contains("Consequences: Consequences text"));
1157 assert!(output.contains("Amends 2"));
1158 }
1159
1160 #[test]
1161 fn test_custom_template_is_ng_flag() {
1162 let custom = Template::from_string(
1163 "ng-check",
1164 r#"{% if is_ng %}NextGen Mode{% else %}Compatible Mode{% endif %}"#,
1165 );
1166
1167 let adr = Adr::new(1, "Test");
1168
1169 let compat_config = Config::default();
1170 let output = custom.render(&adr, &compat_config).unwrap();
1171 assert_eq!(output, "Compatible Mode");
1172
1173 let ng_config = Config {
1174 mode: ConfigMode::NextGen,
1175 ..Default::default()
1176 };
1177 let output = custom.render(&adr, &ng_config).unwrap();
1178 assert_eq!(output, "NextGen Mode");
1179 }
1180
1181 #[test]
1182 fn test_custom_template_link_kinds() {
1183 let custom = Template::from_string(
1184 "links",
1185 r#"{% for link in links %}{{ link.kind }}|{% endfor %}"#,
1186 );
1187
1188 let mut adr = Adr::new(1, "Test");
1189 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1190 adr.links.push(AdrLink::new(2, LinkKind::SupersededBy));
1191 adr.links.push(AdrLink::new(3, LinkKind::Amends));
1192 adr.links.push(AdrLink::new(4, LinkKind::AmendedBy));
1193 adr.links.push(AdrLink::new(5, LinkKind::RelatesTo));
1194 adr.links
1195 .push(AdrLink::new(6, LinkKind::Custom("Depends on".into())));
1196
1197 let config = Config::default();
1198 let output = custom.render(&adr, &config).unwrap();
1199
1200 assert!(output.contains("Supersedes|"));
1201 assert!(output.contains("Superseded by|"));
1202 assert!(output.contains("Amends|"));
1203 assert!(output.contains("Amended by|"));
1204 assert!(output.contains("Relates to|"));
1205 assert!(output.contains("Depends on|"));
1206 }
1207
1208 #[test]
1211 fn test_template_invalid_syntax() {
1212 let custom = Template::from_string("invalid", "{{ unclosed");
1213 let adr = Adr::new(1, "Test");
1214 let config = Config::default();
1215
1216 let result = custom.render(&adr, &config);
1217 assert!(result.is_err());
1218 }
1219
1220 #[test]
1221 fn test_template_undefined_variable() {
1222 let custom = Template::from_string("undefined", "{{ nonexistent }}");
1223 let adr = Adr::new(1, "Test");
1224 let config = Config::default();
1225
1226 let result = custom.render(&adr, &config);
1228 assert!(result.is_ok());
1229 }
1230
1231 #[test]
1234 fn test_render_four_digit_number() {
1235 let template = Template::builtin(TemplateFormat::Nygard);
1236 let adr = Adr::new(9999, "Large Number");
1237 let config = Config::default();
1238
1239 let output = template.render(&adr, &config).unwrap();
1240 assert!(output.contains("# 9999. Large Number"));
1241 }
1242
1243 #[test]
1244 fn test_render_link_number_formatting() {
1245 let template = Template::builtin(TemplateFormat::Nygard);
1246 let mut adr = Adr::new(2, "Test");
1247 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1248
1249 let config = Config::default();
1250 let output = template.render(&adr, &config).unwrap();
1251
1252 assert!(output.contains("0001-"));
1254 }
1255
1256 #[test]
1259 fn test_render_tags_in_nextgen_mode() {
1260 let template = Template::builtin(TemplateFormat::Nygard);
1261 let mut adr = Adr::new(1, "Test ADR");
1262 adr.tags = vec!["database".to_string(), "infrastructure".to_string()];
1263
1264 let config = Config {
1265 mode: ConfigMode::NextGen,
1266 ..Default::default()
1267 };
1268 let output = template.render(&adr, &config).unwrap();
1269
1270 assert!(output.contains("tags:"));
1272 assert!(output.contains("- database"));
1273 assert!(output.contains("- infrastructure"));
1274 }
1275
1276 #[test]
1277 fn test_render_tags_in_madr_format() {
1278 let template = Template::builtin(TemplateFormat::Madr);
1279 let mut adr = Adr::new(1, "Test ADR");
1280 adr.tags = vec!["api".to_string(), "security".to_string()];
1281
1282 let config = Config::default();
1283 let output = template.render(&adr, &config).unwrap();
1284
1285 assert!(output.contains("tags:"));
1287 assert!(output.contains("- api"));
1288 assert!(output.contains("- security"));
1289 }
1290
1291 #[test]
1292 fn test_render_no_tags_section_when_empty() {
1293 let template = Template::builtin(TemplateFormat::Nygard);
1294 let adr = Adr::new(1, "Test ADR");
1295
1296 let config = Config {
1297 mode: ConfigMode::NextGen,
1298 ..Default::default()
1299 };
1300 let output = template.render(&adr, &config).unwrap();
1301
1302 assert!(!output.contains("tags:"));
1304 }
1305}