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