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