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
87fn pad_filter(
91 value: u32,
92 kwargs: minijinja::value::Kwargs,
93) -> std::result::Result<String, minijinja::Error> {
94 let width: Option<u32> = kwargs.get("width")?;
95 kwargs.assert_all_used()?;
96 let w = width.unwrap_or(4) as usize;
97 Ok(format!("{value:0>w$}"))
98}
99
100#[derive(Debug, Clone)]
102pub struct Template {
103 content: String,
105
106 name: String,
108}
109
110impl Template {
111 pub fn from_string(name: impl Into<String>, content: impl Into<String>) -> Self {
113 Self {
114 name: name.into(),
115 content: content.into(),
116 }
117 }
118
119 pub fn content(&self) -> &str {
121 &self.content
122 }
123
124 pub fn name(&self) -> &str {
126 &self.name
127 }
128
129 pub fn from_file(path: &Path) -> Result<Self> {
131 let content = std::fs::read_to_string(path)?;
132 let name = path
133 .file_name()
134 .and_then(|n| n.to_str())
135 .unwrap_or("custom")
136 .to_string();
137 Ok(Self { name, content })
138 }
139
140 pub fn builtin(format: TemplateFormat) -> Self {
142 Self::builtin_with_variant(format, TemplateVariant::Full)
143 }
144
145 pub fn builtin_with_variant(format: TemplateFormat, variant: TemplateVariant) -> Self {
147 let (name, content) = match (format, variant) {
148 (TemplateFormat::Nygard, TemplateVariant::Full) => ("nygard", NYGARD_TEMPLATE),
150 (TemplateFormat::Nygard, TemplateVariant::Minimal) => {
151 ("nygard-minimal", NYGARD_MINIMAL_TEMPLATE)
152 }
153 (TemplateFormat::Nygard, TemplateVariant::Bare) => {
154 ("nygard-bare", NYGARD_BARE_TEMPLATE)
155 }
156 (TemplateFormat::Nygard, TemplateVariant::BareMinimal) => {
157 ("nygard-bare-minimal", NYGARD_BARE_MINIMAL_TEMPLATE)
158 }
159
160 (TemplateFormat::Madr, TemplateVariant::Full) => ("madr", MADR_TEMPLATE),
162 (TemplateFormat::Madr, TemplateVariant::Minimal) => {
163 ("madr-minimal", MADR_MINIMAL_TEMPLATE)
164 }
165 (TemplateFormat::Madr, TemplateVariant::Bare) => ("madr-bare", MADR_BARE_TEMPLATE),
166 (TemplateFormat::Madr, TemplateVariant::BareMinimal) => {
167 ("madr-bare-minimal", MADR_BARE_MINIMAL_TEMPLATE)
168 }
169 };
170 Self::from_string(name, content)
171 }
172
173 pub fn render(
178 &self,
179 adr: &Adr,
180 config: &Config,
181 link_titles: &std::collections::HashMap<u32, (String, String)>,
182 ) -> Result<String> {
183 use crate::LinkKind;
184
185 let mut env = Environment::new();
186 env.add_filter("pad", pad_filter);
187 env.add_template(&self.name, &self.content)
188 .map_err(|e| Error::TemplateError(e.to_string()))?;
189
190 let template = env
191 .get_template(&self.name)
192 .map_err(|e| Error::TemplateError(e.to_string()))?;
193
194 let links: Vec<_> = adr
196 .links
197 .iter()
198 .map(|link| {
199 let kind_display = match &link.kind {
200 LinkKind::Supersedes => "Supersedes",
201 LinkKind::SupersededBy => "Superseded by",
202 LinkKind::Amends => "Amends",
203 LinkKind::AmendedBy => "Amended by",
204 LinkKind::RelatesTo => "Relates to",
205 LinkKind::Custom(s) => s.as_str(),
206 };
207 let (target_title, target_filename) = link_titles
208 .get(&link.target)
209 .cloned()
210 .unwrap_or_else(|| ("...".to_string(), format!("{:04}-....md", link.target)));
211 context! {
212 target => link.target,
213 kind => kind_display,
214 description => &link.description,
215 target_title => target_title,
216 target_filename => target_filename,
217 }
218 })
219 .collect();
220
221 let output = template
222 .render(context! {
223 number => adr.number,
224 title => &adr.title,
225 date => crate::parse::format_date(adr.date),
226 status => adr.status.to_string(),
227 context => &adr.context,
228 decision => &adr.decision,
229 consequences => &adr.consequences,
230 links => links,
231 tags => &adr.tags,
232 is_ng => config.is_next_gen(),
233 decision_makers => &adr.decision_makers,
235 consulted => &adr.consulted,
236 informed => &adr.informed,
237 })
238 .map_err(|e| Error::TemplateError(e.to_string()))?;
239
240 let mut output = output.trim_end().to_string();
247 output.push('\n');
248
249 Ok(output)
250 }
251}
252
253#[derive(Debug)]
255pub struct TemplateEngine {
256 default_format: TemplateFormat,
258
259 default_variant: TemplateVariant,
261
262 custom_template: Option<Template>,
264}
265
266impl Default for TemplateEngine {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272impl TemplateEngine {
273 pub fn new() -> Self {
275 Self {
276 default_format: TemplateFormat::default(),
277 default_variant: TemplateVariant::default(),
278 custom_template: None,
279 }
280 }
281
282 pub fn with_format(mut self, format: TemplateFormat) -> Self {
284 self.default_format = format;
285 self
286 }
287
288 pub fn with_variant(mut self, variant: TemplateVariant) -> Self {
290 self.default_variant = variant;
291 self
292 }
293
294 pub fn with_custom_template(mut self, template: Template) -> Self {
296 self.custom_template = Some(template);
297 self
298 }
299
300 pub fn with_custom_template_file(mut self, path: &Path) -> Result<Self> {
302 self.custom_template = Some(Template::from_file(path)?);
303 Ok(self)
304 }
305
306 pub fn template(&self) -> Template {
308 self.custom_template.clone().unwrap_or_else(|| {
309 Template::builtin_with_variant(self.default_format, self.default_variant)
310 })
311 }
312
313 pub fn render(
315 &self,
316 adr: &Adr,
317 config: &Config,
318 link_titles: &std::collections::HashMap<u32, (String, String)>,
319 ) -> Result<String> {
320 self.template().render(adr, config, link_titles)
321 }
322}
323
324const NYGARD_TEMPLATE: &str = r#"{% if is_ng %}---
326number: {{ number }}
327title: {{ title }}
328date: {{ date }}
329status: {{ status | lower }}
330{% if links %}links:
331{% for link in links %} - target: {{ link.target }}
332 kind: {{ link.kind | lower }}
333{% endfor %}{% endif %}{% if tags %}tags:
334{% for tag in tags %} - {{ tag }}
335{% endfor %}{% endif %}---
336
337{% endif %}# {{ number }}. {{ title }}
338
339Date: {{ date }}
340
341## Status
342
343{{ status }}
344{% for link in links %}
345{{ link.kind }} [{{ link.target }}. {{ link.target_title }}]({{ link.target_filename }})
346{% endfor %}
347## Context
348
349{{ context if context else "What is the issue that we're seeing that is motivating this decision or change?" }}
350
351## Decision
352
353{{ decision if decision else "What is the change that we're proposing and/or doing?" }}
354
355## Consequences
356
357{{ consequences if consequences else "What becomes easier or more difficult to do because of this change?" }}
358
359"#;
360
361const MADR_TEMPLATE: &str = r#"---
363number: {{ number }}
364title: {{ title }}
365status: {{ status | lower }}
366date: {{ date }}
367{% if decision_makers %}decision-makers:
368{% for dm in decision_makers %} - {{ dm }}
369{% endfor %}{% endif %}{% if consulted %}consulted:
370{% for c in consulted %} - {{ c }}
371{% endfor %}{% endif %}{% if informed %}informed:
372{% for i in informed %} - {{ i }}
373{% endfor %}{% endif %}{% if tags %}tags:
374{% for tag in tags %} - {{ tag }}
375{% endfor %}{% endif %}---
376
377# {{ title }}
378
379## Context and Problem Statement
380
381{{ 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.}" }}
382
383<!-- This is an optional element. Feel free to remove. -->
384## Decision Drivers
385
386* {decision driver 1, e.g., a force, facing concern, ...}
387* {decision driver 2, e.g., a force, facing concern, ...}
388* ... <!-- numbers of drivers can vary -->
389
390## Considered Options
391
392* {title of option 1}
393* {title of option 2}
394* {title of option 3}
395* ... <!-- numbers of options can vary -->
396
397## Decision Outcome
398
399{{ 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)}." }}
400
401<!-- This is an optional element. Feel free to remove. -->
402### Consequences
403
404{{ 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 -->" }}
405
406<!-- This is an optional element. Feel free to remove. -->
407### Confirmation
408
409{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.}
410
411<!-- This is an optional element. Feel free to remove. -->
412## Pros and Cons of the Options
413
414### {title of option 1}
415
416<!-- This is an optional element. Feel free to remove. -->
417{example | description | pointer to more information | ...}
418
419* Good, because {argument a}
420* Good, because {argument b}
421<!-- use "neutral" if the given argument weights neither for good nor bad -->
422* Neutral, because {argument c}
423* Bad, because {argument d}
424* ... <!-- numbers of pros and cons can vary -->
425
426### {title of other option}
427
428{example | description | pointer to more information | ...}
429
430* Good, because {argument a}
431* Good, because {argument b}
432* Neutral, because {argument c}
433* Bad, because {argument d}
434* ...
435
436<!-- This is an optional element. Feel free to remove. -->
437## More Information
438
439{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.}
440"#;
441
442const NYGARD_MINIMAL_TEMPLATE: &str = r#"{% if is_ng %}---
444number: {{ number }}
445title: {{ title }}
446date: {{ date }}
447status: {{ status | lower }}
448{% if links %}links:
449{% for link in links %} - target: {{ link.target }}
450 kind: {{ link.kind | lower }}
451{% endfor %}{% endif %}{% if tags %}tags:
452{% for tag in tags %} - {{ tag }}
453{% endfor %}{% endif %}---
454
455{% endif %}# {{ number }}. {{ title }}
456
457Date: {{ date }}
458
459## Status
460
461{{ status }}
462{% for link in links %}
463{{ link.kind }} [{{ link.target }}. {{ link.target_title }}]({{ link.target_filename }})
464{% endfor %}
465## Context
466
467{{ context if context else "" }}
468
469## Decision
470
471{{ decision if decision else "" }}
472
473## Consequences
474
475{{ consequences if consequences else "" }}
476"#;
477
478const NYGARD_BARE_TEMPLATE: &str = r#"# {{ number }}. {{ title }}
480
481Date: {{ date }}
482
483## Status
484
485{{ status }}
486
487## Context
488
489
490
491## Decision
492
493
494
495## Consequences
496
497"#;
498
499const NYGARD_BARE_MINIMAL_TEMPLATE: &str = r#"# {{ number }}. {{ title }}
501
502Date: {{ date }}
503
504## Status
505
506{{ status }}
507
508## Context
509
510
511
512## Decision
513
514
515
516## Consequences
517
518"#;
519
520const MADR_MINIMAL_TEMPLATE: &str = r#"# {{ title }}
523
524## Context and Problem Statement
525
526{{ 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.}" }}
527
528## Considered Options
529
530* {title of option 1}
531* {title of option 2}
532* {title of option 3}
533* ... <!-- numbers of options can vary -->
534
535## Decision Outcome
536
537{{ 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)}." }}
538
539<!-- This is an optional element. Feel free to remove. -->
540### Consequences
541
542{{ 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 -->" }}
543"#;
544
545const MADR_BARE_TEMPLATE: &str = r#"---
548number: {{ number }}
549title: {{ title }}
550status: {{ status | lower }}
551date: {{ date }}
552{% if decision_makers %}decision-makers:
553{% for dm in decision_makers %} - {{ dm }}
554{% endfor %}{% endif %}{% if consulted %}consulted:
555{% for c in consulted %} - {{ c }}
556{% endfor %}{% endif %}{% if informed %}informed:
557{% for i in informed %} - {{ i }}
558{% endfor %}{% endif %}{% if tags %}tags:
559{% for tag in tags %} - {{ tag }}
560{% endfor %}{% endif %}---
561
562# {{ title }}
563
564## Context and Problem Statement
565
566
567
568## Decision Drivers
569
570* <!-- decision driver -->
571
572## Considered Options
573
574* <!-- option -->
575
576## Decision Outcome
577
578Chosen option: "", because
579
580### Consequences
581
582* Good, because
583* Bad, because
584
585### Confirmation
586
587
588
589## Pros and Cons of the Options
590
591### <!-- title of option -->
592
593* Good, because
594* Neutral, because
595* Bad, because
596
597## More Information
598
599"#;
600
601const MADR_BARE_MINIMAL_TEMPLATE: &str = r#"# {{ title }}
604
605## Context and Problem Statement
606
607
608
609## Considered Options
610
611
612
613## Decision Outcome
614
615
616
617### Consequences
618
619"#;
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624 use crate::{AdrLink, AdrStatus, ConfigMode, LinkKind};
625 use std::collections::HashMap;
626 use tempfile::TempDir;
627 use test_case::test_case;
628
629 fn no_link_titles() -> HashMap<u32, (String, String)> {
630 HashMap::new()
631 }
632
633 #[test]
636 fn test_template_format_default() {
637 assert_eq!(TemplateFormat::default(), TemplateFormat::Nygard);
638 }
639
640 #[test_case("nygard" => TemplateFormat::Nygard; "nygard")]
641 #[test_case("Nygard" => TemplateFormat::Nygard; "nygard capitalized")]
642 #[test_case("NYGARD" => TemplateFormat::Nygard; "nygard uppercase")]
643 #[test_case("default" => TemplateFormat::Nygard; "default alias")]
644 #[test_case("madr" => TemplateFormat::Madr; "madr")]
645 #[test_case("MADR" => TemplateFormat::Madr; "madr uppercase")]
646 fn test_template_format_parse(input: &str) -> TemplateFormat {
647 input.parse().unwrap()
648 }
649
650 #[test]
651 fn test_template_format_parse_unknown() {
652 let result: Result<TemplateFormat> = "unknown".parse();
653 assert!(result.is_err());
654 }
655
656 #[test]
657 fn test_template_format_display() {
658 assert_eq!(TemplateFormat::Nygard.to_string(), "nygard");
659 assert_eq!(TemplateFormat::Madr.to_string(), "madr");
660 }
661
662 #[test]
665 fn test_template_variant_default() {
666 assert_eq!(TemplateVariant::default(), TemplateVariant::Full);
667 }
668
669 #[test_case("full" => TemplateVariant::Full; "full")]
670 #[test_case("Full" => TemplateVariant::Full; "full capitalized")]
671 #[test_case("default" => TemplateVariant::Full; "default alias")]
672 #[test_case("minimal" => TemplateVariant::Minimal; "minimal")]
673 #[test_case("min" => TemplateVariant::Minimal; "min alias")]
674 #[test_case("bare" => TemplateVariant::Bare; "bare")]
675 #[test_case("bare-minimal" => TemplateVariant::BareMinimal; "bare-minimal")]
676 #[test_case("bareminimal" => TemplateVariant::BareMinimal; "bareminimal")]
677 #[test_case("empty" => TemplateVariant::BareMinimal; "empty alias")]
678 fn test_template_variant_parse(input: &str) -> TemplateVariant {
679 input.parse().unwrap()
680 }
681
682 #[test]
683 fn test_template_variant_parse_unknown() {
684 let result: Result<TemplateVariant> = "unknown".parse();
685 assert!(result.is_err());
686 }
687
688 #[test]
689 fn test_template_variant_display() {
690 assert_eq!(TemplateVariant::Full.to_string(), "full");
691 assert_eq!(TemplateVariant::Minimal.to_string(), "minimal");
692 assert_eq!(TemplateVariant::Bare.to_string(), "bare");
693 assert_eq!(TemplateVariant::BareMinimal.to_string(), "bare-minimal");
694 }
695
696 #[test]
699 fn test_template_from_string() {
700 let template = Template::from_string("test", "# {{ title }}");
701 assert_eq!(template.name, "test");
702 assert_eq!(template.content, "# {{ title }}");
703 }
704
705 #[test]
706 fn test_template_from_file() {
707 let temp = TempDir::new().unwrap();
708 let path = temp.path().join("custom.md");
709 std::fs::write(&path, "# {{ number }}. {{ title }}").unwrap();
710
711 let template = Template::from_file(&path).unwrap();
712 assert_eq!(template.name, "custom.md");
713 assert!(template.content.contains("{{ number }}"));
714 }
715
716 #[test]
717 fn test_template_from_file_not_found() {
718 let result = Template::from_file(Path::new("/nonexistent/template.md"));
719 assert!(result.is_err());
720 }
721
722 #[test]
723 fn test_template_builtin_nygard() {
724 let template = Template::builtin(TemplateFormat::Nygard);
725 assert_eq!(template.name, "nygard");
726 assert!(template.content.contains("## Status"));
727 assert!(template.content.contains("## Context"));
728 assert!(template.content.contains("## Decision"));
729 assert!(template.content.contains("## Consequences"));
730 }
731
732 #[test]
733 fn test_template_builtin_madr() {
734 let template = Template::builtin(TemplateFormat::Madr);
735 assert_eq!(template.name, "madr");
736 assert!(template.content.contains("Context and Problem Statement"));
737 assert!(template.content.contains("Decision Drivers"));
738 assert!(template.content.contains("Considered Options"));
739 assert!(template.content.contains("Decision Outcome"));
740 }
741
742 #[test]
745 fn test_render_nygard_compatible() {
746 let template = Template::builtin(TemplateFormat::Nygard);
747 let mut adr = Adr::new(1, "Use Rust");
748 adr.status = AdrStatus::Accepted;
749
750 let config = Config::default();
751 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
752
753 assert!(output.contains("# 1. Use Rust"));
754 assert!(output.contains("## Status"));
755 assert!(output.contains("Accepted"));
756 assert!(!output.starts_with("---")); }
758
759 #[test]
760 fn test_madr_bare_roundtrips_when_empty() {
761 let template = Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Bare);
764 let adr = Adr::new(2, "Bare MADR decision");
765 let config = Config::default();
766 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
767
768 assert!(!output.contains("decision-makers:"));
770
771 let parsed = crate::Parser::new().parse(&output).unwrap();
773 assert_eq!(parsed.title, "Bare MADR decision");
774 assert!(parsed.decision_makers.is_empty());
775 assert!(parsed.tags.is_empty());
776 }
777
778 #[test]
779 fn test_madr_bare_renders_decision_makers() {
780 let template = Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Bare);
783 let mut adr = Adr::new(2, "With deciders");
784 adr.decision_makers = vec!["alice".to_string(), "bob".to_string()];
785 let config = Config::default();
786 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
787
788 assert!(output.contains("decision-makers:"));
789 let parsed = crate::Parser::new().parse(&output).unwrap();
790 assert_eq!(parsed.decision_makers, vec!["alice", "bob"]);
791 }
792
793 #[test]
794 fn test_render_nygard_all_statuses() {
795 let template = Template::builtin(TemplateFormat::Nygard);
796 let config = Config::default();
797
798 for (status, expected_text) in [
799 (AdrStatus::Proposed, "Proposed"),
800 (AdrStatus::Accepted, "Accepted"),
801 (AdrStatus::Deprecated, "Deprecated"),
802 (AdrStatus::Superseded, "Superseded"),
803 (AdrStatus::Custom("Draft".into()), "Draft"),
804 ] {
805 let mut adr = Adr::new(1, "Test");
806 adr.status = status;
807
808 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
809 assert!(
810 output.contains(expected_text),
811 "Output should contain '{expected_text}': {output}"
812 );
813 }
814 }
815
816 #[test]
817 fn test_render_nygard_with_content() {
818 let template = Template::builtin(TemplateFormat::Nygard);
819 let mut adr = Adr::new(1, "Use Rust");
820 adr.status = AdrStatus::Accepted;
821 adr.context = "We need a safe language.".to_string();
822 adr.decision = "We will use Rust.".to_string();
823 adr.consequences = "Better memory safety.".to_string();
824
825 let config = Config::default();
826 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
827
828 assert!(output.contains("We need a safe language."));
829 assert!(output.contains("We will use Rust."));
830 assert!(output.contains("Better memory safety."));
831 }
832
833 #[test]
834 fn test_render_nygard_with_links() {
835 let template = Template::builtin(TemplateFormat::Nygard);
836 let mut adr = Adr::new(2, "Use PostgreSQL");
837 adr.status = AdrStatus::Accepted;
838 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
839
840 let config = Config::default();
841 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
842
843 assert!(output.contains("Supersedes"));
844 assert!(output.contains("[1. ...]"));
845 assert!(output.contains("0001-....md"));
846 }
847
848 #[test]
849 fn test_render_nygard_with_multiple_links() {
850 let template = Template::builtin(TemplateFormat::Nygard);
851 let mut adr = Adr::new(5, "Combined Decision");
852 adr.status = AdrStatus::Accepted;
853 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
854 adr.links.push(AdrLink::new(2, LinkKind::Amends));
855 adr.links.push(AdrLink::new(3, LinkKind::SupersededBy));
856
857 let config = Config::default();
858 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
859
860 assert!(output.contains("Supersedes"));
861 assert!(output.contains("Amends"));
862 assert!(output.contains("Superseded by"));
863 }
864
865 #[test]
868 fn test_render_nygard_with_resolved_link_titles() {
869 let template = Template::builtin(TemplateFormat::Nygard);
870 let mut adr = Adr::new(3, "Use PostgreSQL instead");
871 adr.status = AdrStatus::Accepted;
872 adr.links.push(AdrLink::new(2, LinkKind::Supersedes));
873
874 let mut link_titles = HashMap::new();
875 link_titles.insert(
876 2,
877 (
878 "Use MySQL for persistence".to_string(),
879 "0002-use-mysql-for-persistence.md".to_string(),
880 ),
881 );
882
883 let config = Config::default();
884 let output = template.render(&adr, &config, &link_titles).unwrap();
885
886 assert!(
887 output.contains(
888 "Supersedes [2. Use MySQL for persistence](0002-use-mysql-for-persistence.md)"
889 ),
890 "Link should contain resolved title and filename. Got:\n{output}"
891 );
892 }
893
894 #[test]
895 fn test_render_nygard_with_resolved_superseded_by_link() {
896 let template = Template::builtin(TemplateFormat::Nygard);
897 let mut adr = Adr::new(2, "Use MySQL");
898 adr.status = AdrStatus::Superseded;
899 adr.links.push(AdrLink::new(3, LinkKind::SupersededBy));
900
901 let mut link_titles = HashMap::new();
902 link_titles.insert(
903 3,
904 (
905 "Use PostgreSQL instead".to_string(),
906 "0003-use-postgresql-instead.md".to_string(),
907 ),
908 );
909
910 let config = Config::default();
911 let output = template.render(&adr, &config, &link_titles).unwrap();
912
913 assert!(
914 output.contains(
915 "Superseded by [3. Use PostgreSQL instead](0003-use-postgresql-instead.md)"
916 ),
917 "Superseded-by link should contain resolved title and filename. Got:\n{output}"
918 );
919 }
920
921 #[test]
922 fn test_render_nygard_with_multiple_resolved_links() {
923 let template = Template::builtin(TemplateFormat::Nygard);
924 let mut adr = Adr::new(5, "Combined Decision");
925 adr.status = AdrStatus::Accepted;
926 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
927 adr.links.push(AdrLink::new(2, LinkKind::Amends));
928
929 let mut link_titles = HashMap::new();
930 link_titles.insert(
931 1,
932 (
933 "Initial Decision".to_string(),
934 "0001-initial-decision.md".to_string(),
935 ),
936 );
937 link_titles.insert(
938 2,
939 (
940 "Second Decision".to_string(),
941 "0002-second-decision.md".to_string(),
942 ),
943 );
944
945 let config = Config::default();
946 let output = template.render(&adr, &config, &link_titles).unwrap();
947
948 assert!(
949 output.contains("Supersedes [1. Initial Decision](0001-initial-decision.md)"),
950 "First link should be resolved. Got:\n{output}"
951 );
952 assert!(
953 output.contains("Amends [2. Second Decision](0002-second-decision.md)"),
954 "Second link should be resolved. Got:\n{output}"
955 );
956 }
957
958 #[test]
959 fn test_render_nygard_unresolved_link_falls_back() {
960 let template = Template::builtin(TemplateFormat::Nygard);
961 let mut adr = Adr::new(2, "Test");
962 adr.status = AdrStatus::Accepted;
963 adr.links.push(AdrLink::new(99, LinkKind::Supersedes));
964
965 let config = Config::default();
966 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
968
969 assert!(
970 output.contains("Supersedes [99. ...](0099-....md)"),
971 "Unresolved link should fall back to '...' placeholder. Got:\n{output}"
972 );
973 }
974
975 #[test]
976 fn test_render_nygard_minimal_with_resolved_links() {
977 let template =
978 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Minimal);
979 let mut adr = Adr::new(2, "New Approach");
980 adr.status = AdrStatus::Accepted;
981 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
982
983 let mut link_titles = HashMap::new();
984 link_titles.insert(
985 1,
986 (
987 "Old Approach".to_string(),
988 "0001-old-approach.md".to_string(),
989 ),
990 );
991
992 let config = Config::default();
993 let output = template.render(&adr, &config, &link_titles).unwrap();
994
995 assert!(
996 output.contains("Supersedes [1. Old Approach](0001-old-approach.md)"),
997 "Minimal template should also resolve link titles. Got:\n{output}"
998 );
999 }
1000
1001 #[test]
1002 fn test_render_nygard_ng_with_resolved_links() {
1003 let template = Template::builtin(TemplateFormat::Nygard);
1004 let mut adr = Adr::new(2, "New Approach");
1005 adr.status = AdrStatus::Accepted;
1006 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1007
1008 let mut link_titles = HashMap::new();
1009 link_titles.insert(
1010 1,
1011 (
1012 "Old Approach".to_string(),
1013 "0001-old-approach.md".to_string(),
1014 ),
1015 );
1016
1017 let config = Config {
1018 mode: ConfigMode::NextGen,
1019 ..Default::default()
1020 };
1021 let output = template.render(&adr, &config, &link_titles).unwrap();
1022
1023 assert!(
1025 output.contains("Supersedes [1. Old Approach](0001-old-approach.md)"),
1026 "NG mode body should have resolved links. Got:\n{output}"
1027 );
1028 assert!(output.contains("links:"));
1030 assert!(output.contains("target: 1"));
1031 }
1032
1033 #[test]
1036 fn test_render_nygard_ng() {
1037 let template = Template::builtin(TemplateFormat::Nygard);
1038 let mut adr = Adr::new(1, "Use Rust");
1039 adr.status = AdrStatus::Accepted;
1040
1041 let config = Config {
1042 mode: ConfigMode::NextGen,
1043 ..Default::default()
1044 };
1045 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1046
1047 assert!(output.starts_with("---")); assert!(output.contains("number: 1"));
1049 assert!(output.contains("title: Use Rust"));
1050 assert!(output.contains("status: accepted"));
1051 }
1052
1053 #[test]
1054 fn test_render_nygard_ng_with_links() {
1055 let template = Template::builtin(TemplateFormat::Nygard);
1056 let mut adr = Adr::new(2, "Test");
1057 adr.status = AdrStatus::Accepted;
1058 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1059
1060 let config = Config {
1061 mode: ConfigMode::NextGen,
1062 ..Default::default()
1063 };
1064 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1065
1066 assert!(output.contains("links:"));
1067 assert!(output.contains("target: 1"));
1068 }
1069
1070 #[test]
1073 fn test_render_madr_basic() {
1074 let template = Template::builtin(TemplateFormat::Madr);
1075 let mut adr = Adr::new(1, "Use Rust");
1076 adr.status = AdrStatus::Accepted;
1077
1078 let config = Config::default();
1079 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1080
1081 assert!(output.starts_with("---")); assert!(output.contains("status: accepted"));
1083 assert!(output.contains("# Use Rust"));
1084 assert!(output.contains("## Context and Problem Statement"));
1085 assert!(output.contains("## Decision Drivers"));
1086 assert!(output.contains("## Considered Options"));
1087 assert!(output.contains("## Decision Outcome"));
1088 assert!(output.contains("## Pros and Cons of the Options"));
1089 }
1090
1091 #[test]
1092 fn test_render_madr_with_decision_makers() {
1093 let template = Template::builtin(TemplateFormat::Madr);
1094 let mut adr = Adr::new(1, "Use Rust");
1095 adr.status = AdrStatus::Accepted;
1096 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
1097
1098 let config = Config::default();
1099 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1100
1101 assert!(output.contains("decision-makers:"));
1102 assert!(output.contains(" - Alice"));
1103 assert!(output.contains(" - Bob"));
1104 }
1105
1106 #[test]
1107 fn test_render_madr_with_consulted() {
1108 let template = Template::builtin(TemplateFormat::Madr);
1109 let mut adr = Adr::new(1, "Use Rust");
1110 adr.status = AdrStatus::Accepted;
1111 adr.consulted = vec!["Carol".into()];
1112
1113 let config = Config::default();
1114 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1115
1116 assert!(output.contains("consulted:"));
1117 assert!(output.contains(" - Carol"));
1118 }
1119
1120 #[test]
1121 fn test_render_madr_with_informed() {
1122 let template = Template::builtin(TemplateFormat::Madr);
1123 let mut adr = Adr::new(1, "Use Rust");
1124 adr.status = AdrStatus::Accepted;
1125 adr.informed = vec!["Dave".into(), "Eve".into()];
1126
1127 let config = Config::default();
1128 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1129
1130 assert!(output.contains("informed:"));
1131 assert!(output.contains(" - Dave"));
1132 assert!(output.contains(" - Eve"));
1133 }
1134
1135 #[test]
1136 fn test_render_madr_full_frontmatter() {
1137 let template = Template::builtin(TemplateFormat::Madr);
1138 let mut adr = Adr::new(1, "Use MADR Format");
1139 adr.status = AdrStatus::Accepted;
1140 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
1141 adr.consulted = vec!["Carol".into()];
1142 adr.informed = vec!["Dave".into()];
1143
1144 let config = Config::default();
1145 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1146
1147 assert!(
1149 output.starts_with("---\nnumber: 1\ntitle: Use MADR Format\nstatus: accepted\ndate:")
1150 );
1151 assert!(output.contains("decision-makers:\n - Alice\n - Bob"));
1152 assert!(output.contains("consulted:\n - Carol"));
1153 assert!(output.contains("informed:\n - Dave"));
1154 assert!(output.contains("---\n\n# Use MADR Format"));
1155 }
1156
1157 #[test]
1158 fn test_render_madr_empty_optional_fields() {
1159 let template = Template::builtin(TemplateFormat::Madr);
1160 let mut adr = Adr::new(1, "Simple ADR");
1161 adr.status = AdrStatus::Proposed;
1162
1163 let config = Config::default();
1164 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1165
1166 assert!(!output.contains("decision-makers:"));
1168 assert!(!output.contains("consulted:"));
1169 assert!(!output.contains("informed:"));
1170 }
1171
1172 #[test]
1175 fn test_nygard_minimal_template() {
1176 let template =
1177 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Minimal);
1178 let adr = Adr::new(1, "Minimal Test");
1179 let config = Config::default();
1180 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1181
1182 assert!(output.contains("# 1. Minimal Test"));
1184 assert!(output.contains("## Status"));
1185 assert!(output.contains("## Context"));
1186 assert!(output.contains("## Decision"));
1187 assert!(output.contains("## Consequences"));
1188 assert!(!output.contains("What is the issue"));
1190 }
1191
1192 #[test]
1193 fn test_nygard_bare_template() {
1194 let template =
1195 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Bare);
1196 let adr = Adr::new(1, "Bare Test");
1197 let config = Config::default();
1198 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1199
1200 assert!(output.contains("# 1. Bare Test"));
1202 assert!(output.contains("## Status"));
1203 assert!(output.contains("## Context"));
1204 assert!(!output.contains("---"));
1206 }
1207
1208 #[test]
1209 fn test_madr_minimal_template() {
1210 let template =
1211 Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Minimal);
1212 let adr = Adr::new(1, "MADR Minimal");
1213 let config = Config::default();
1214 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1215
1216 assert!(!output.starts_with("---"));
1218 assert!(output.contains("# MADR Minimal"));
1219 assert!(output.contains("## Context and Problem Statement"));
1220 assert!(output.contains("## Considered Options"));
1221 assert!(output.contains("## Decision Outcome"));
1222 assert!(!output.contains("## Decision Drivers"));
1224 assert!(!output.contains("## Pros and Cons"));
1225 }
1226
1227 #[test]
1228 fn test_madr_bare_template() {
1229 let template = Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Bare);
1230 let adr = Adr::new(1, "MADR Bare");
1231 let config = Config::default();
1232 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1233
1234 assert!(output.starts_with("---"));
1237 assert!(output.contains("status:"));
1238 assert!(!output.contains("decision-makers:"));
1239 assert!(!output.contains("consulted:"));
1240 assert!(!output.contains("informed:"));
1241 assert!(output.contains("# MADR Bare"));
1242 assert!(output.contains("## Decision Drivers"));
1244 assert!(output.contains("## Considered Options"));
1245 assert!(output.contains("## Pros and Cons of the Options"));
1246 assert!(output.contains("## More Information"));
1247 }
1248
1249 #[test]
1250 fn test_madr_bare_minimal_template() {
1251 let template =
1252 Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::BareMinimal);
1253 let adr = Adr::new(1, "MADR Bare Minimal");
1254 let config = Config::default();
1255 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1256
1257 assert!(!output.starts_with("---"));
1259 assert!(output.contains("# MADR Bare Minimal"));
1260 assert!(output.contains("## Context and Problem Statement"));
1261 assert!(output.contains("## Considered Options"));
1262 assert!(output.contains("## Decision Outcome"));
1263 assert!(output.contains("### Consequences"));
1264 assert!(!output.contains("## Decision Drivers"));
1266 assert!(!output.contains("## Pros and Cons"));
1267 }
1268
1269 #[test]
1270 fn test_nygard_bare_minimal_template() {
1271 let template =
1272 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::BareMinimal);
1273 let adr = Adr::new(1, "Nygard Bare Minimal");
1274 let config = Config::default();
1275 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1276
1277 assert!(output.contains("# 1. Nygard Bare Minimal"));
1280 assert!(output.contains("Date:"));
1281 assert!(output.contains("## Status"));
1282 assert!(output.contains("## Context"));
1283 assert!(output.contains("## Decision"));
1284 assert!(output.contains("## Consequences"));
1285 assert!(!output.contains("---"));
1287 }
1288
1289 #[test]
1290 fn test_builtin_defaults_to_full() {
1291 let full = Template::builtin(TemplateFormat::Nygard);
1292 let explicit_full =
1293 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Full);
1294
1295 assert_eq!(full.name, explicit_full.name);
1296 assert_eq!(full.content, explicit_full.content);
1297 }
1298
1299 #[test]
1300 fn test_every_builtin_render_ends_with_exactly_one_newline() {
1301 let formats = [TemplateFormat::Nygard, TemplateFormat::Madr];
1306 let variants = [
1307 TemplateVariant::Full,
1308 TemplateVariant::Minimal,
1309 TemplateVariant::Bare,
1310 TemplateVariant::BareMinimal,
1311 ];
1312 let empty = Adr::new(1, "Empty fields");
1313 let mut populated = Adr::new(2, "Populated fields");
1314 populated.context = "Some context.".into();
1315 populated.decision = "Some decision.".into();
1316 populated.consequences = "Some consequences.".into();
1317
1318 for format in formats {
1319 for variant in variants {
1320 let template = Template::builtin_with_variant(format, variant);
1321 for adr in [&empty, &populated] {
1322 let output = template
1323 .render(adr, &Config::default(), &no_link_titles())
1324 .unwrap();
1325 assert!(
1326 output.ends_with('\n') && !output.ends_with("\n\n"),
1327 "{format}-{variant} (adr {}) must end with exactly one newline, got tail {:?}",
1328 adr.number,
1329 &output[output.len().saturating_sub(12)..]
1330 );
1331 }
1332 }
1333 }
1334 }
1335
1336 #[test]
1339 fn test_template_engine_new() {
1340 let engine = TemplateEngine::new();
1341 assert_eq!(engine.default_format, TemplateFormat::Nygard);
1342 assert_eq!(engine.default_variant, TemplateVariant::Full);
1343 assert!(engine.custom_template.is_none());
1344 }
1345
1346 #[test]
1347 fn test_template_engine_default() {
1348 let engine = TemplateEngine::default();
1349 assert_eq!(engine.default_format, TemplateFormat::Nygard);
1350 assert_eq!(engine.default_variant, TemplateVariant::Full);
1351 }
1352
1353 #[test]
1354 fn test_template_engine_with_format() {
1355 let engine = TemplateEngine::new().with_format(TemplateFormat::Madr);
1356 assert_eq!(engine.default_format, TemplateFormat::Madr);
1357 }
1358
1359 #[test]
1360 fn test_template_engine_with_custom_template() {
1361 let custom = Template::from_string("custom", "# {{ title }}");
1362 let engine = TemplateEngine::new().with_custom_template(custom);
1363 assert!(engine.custom_template.is_some());
1364 }
1365
1366 #[test]
1367 fn test_template_engine_with_custom_template_file() {
1368 let temp = TempDir::new().unwrap();
1369 let path = temp.path().join("template.md");
1370 std::fs::write(&path, "# {{ title }}").unwrap();
1371
1372 let engine = TemplateEngine::new()
1373 .with_custom_template_file(&path)
1374 .unwrap();
1375 assert!(engine.custom_template.is_some());
1376 }
1377
1378 #[test]
1379 fn test_template_engine_with_custom_template_file_not_found() {
1380 let result = TemplateEngine::new().with_custom_template_file(Path::new("/nonexistent.md"));
1381 assert!(result.is_err());
1382 }
1383
1384 #[test]
1385 fn test_template_engine_template_builtin() {
1386 let engine = TemplateEngine::new();
1387 let template = engine.template();
1388 assert_eq!(template.name, "nygard");
1389 }
1390
1391 #[test]
1392 fn test_template_engine_template_custom() {
1393 let custom = Template::from_string("my-template", "# Custom");
1394 let engine = TemplateEngine::new().with_custom_template(custom);
1395 let template = engine.template();
1396 assert_eq!(template.name, "my-template");
1397 }
1398
1399 #[test]
1400 fn test_template_engine_render() {
1401 let engine = TemplateEngine::new();
1402 let adr = Adr::new(1, "Test");
1403 let config = Config::default();
1404
1405 let output = engine.render(&adr, &config, &no_link_titles()).unwrap();
1406 assert!(output.contains("# 1. Test"));
1407 }
1408
1409 #[test]
1410 fn test_template_engine_render_custom() {
1411 let custom = Template::from_string("custom", "ADR {{ number }}: {{ title }}");
1412 let engine = TemplateEngine::new().with_custom_template(custom);
1413 let adr = Adr::new(42, "Custom ADR");
1414 let config = Config::default();
1415
1416 let output = engine.render(&adr, &config, &no_link_titles()).unwrap();
1417 assert_eq!(output, "ADR 42: Custom ADR\n");
1418 }
1419
1420 #[test]
1423 fn test_custom_template_all_fields() {
1424 let custom = Template::from_string(
1425 "full",
1426 r#"# {{ number }}. {{ title }}
1427Date: {{ date }}
1428Status: {{ status }}
1429Context: {{ context }}
1430Decision: {{ decision }}
1431Consequences: {{ consequences }}
1432Links: {% for link in links %}{{ link.kind }} {{ link.target }}{% endfor %}"#,
1433 );
1434
1435 let mut adr = Adr::new(1, "Test");
1436 adr.status = AdrStatus::Accepted;
1437 adr.context = "Context text".into();
1438 adr.decision = "Decision text".into();
1439 adr.consequences = "Consequences text".into();
1440 adr.links.push(AdrLink::new(2, LinkKind::Amends));
1441
1442 let config = Config::default();
1443 let output = custom.render(&adr, &config, &no_link_titles()).unwrap();
1444
1445 assert!(output.contains("# 1. Test"));
1446 assert!(output.contains("Status: Accepted"));
1447 assert!(output.contains("Context: Context text"));
1448 assert!(output.contains("Decision: Decision text"));
1449 assert!(output.contains("Consequences: Consequences text"));
1450 assert!(output.contains("Amends 2"));
1451 }
1452
1453 #[test]
1454 fn test_custom_template_is_ng_flag() {
1455 let custom = Template::from_string(
1456 "ng-check",
1457 r#"{% if is_ng %}NextGen Mode{% else %}Compatible Mode{% endif %}"#,
1458 );
1459
1460 let adr = Adr::new(1, "Test");
1461
1462 let compat_config = Config::default();
1463 let output = custom
1464 .render(&adr, &compat_config, &no_link_titles())
1465 .unwrap();
1466 assert_eq!(output, "Compatible Mode\n");
1467
1468 let ng_config = Config {
1469 mode: ConfigMode::NextGen,
1470 ..Default::default()
1471 };
1472 let output = custom.render(&adr, &ng_config, &no_link_titles()).unwrap();
1473 assert_eq!(output, "NextGen Mode\n");
1474 }
1475
1476 #[test]
1477 fn test_custom_template_link_kinds() {
1478 let custom = Template::from_string(
1479 "links",
1480 r#"{% for link in links %}{{ link.kind }}|{% endfor %}"#,
1481 );
1482
1483 let mut adr = Adr::new(1, "Test");
1484 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1485 adr.links.push(AdrLink::new(2, LinkKind::SupersededBy));
1486 adr.links.push(AdrLink::new(3, LinkKind::Amends));
1487 adr.links.push(AdrLink::new(4, LinkKind::AmendedBy));
1488 adr.links.push(AdrLink::new(5, LinkKind::RelatesTo));
1489 adr.links
1490 .push(AdrLink::new(6, LinkKind::Custom("Depends on".into())));
1491
1492 let config = Config::default();
1493 let output = custom.render(&adr, &config, &no_link_titles()).unwrap();
1494
1495 assert!(output.contains("Supersedes|"));
1496 assert!(output.contains("Superseded by|"));
1497 assert!(output.contains("Amends|"));
1498 assert!(output.contains("Amended by|"));
1499 assert!(output.contains("Relates to|"));
1500 assert!(output.contains("Depends on|"));
1501 }
1502
1503 #[test]
1506 fn test_template_invalid_syntax() {
1507 let custom = Template::from_string("invalid", "{{ unclosed");
1508 let adr = Adr::new(1, "Test");
1509 let config = Config::default();
1510
1511 let result = custom.render(&adr, &config, &no_link_titles());
1512 assert!(result.is_err());
1513 }
1514
1515 #[test]
1516 fn test_template_undefined_variable() {
1517 let custom = Template::from_string("undefined", "{{ nonexistent }}");
1518 let adr = Adr::new(1, "Test");
1519 let config = Config::default();
1520
1521 let result = custom.render(&adr, &config, &no_link_titles());
1523 assert!(result.is_ok());
1524 }
1525
1526 #[test]
1529 fn test_render_four_digit_number() {
1530 let template = Template::builtin(TemplateFormat::Nygard);
1531 let adr = Adr::new(9999, "Large Number");
1532 let config = Config::default();
1533
1534 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1535 assert!(output.contains("# 9999. Large Number"));
1536 }
1537
1538 #[test]
1539 fn test_render_link_number_formatting() {
1540 let template = Template::builtin(TemplateFormat::Nygard);
1541 let mut adr = Adr::new(2, "Test");
1542 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1543
1544 let config = Config::default();
1545 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1546
1547 assert!(output.contains("0001-"));
1549 }
1550
1551 #[test]
1554 fn test_render_tags_in_nextgen_mode() {
1555 let template = Template::builtin(TemplateFormat::Nygard);
1556 let mut adr = Adr::new(1, "Test ADR");
1557 adr.tags = vec!["database".to_string(), "infrastructure".to_string()];
1558
1559 let config = Config {
1560 mode: ConfigMode::NextGen,
1561 ..Default::default()
1562 };
1563 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1564
1565 assert!(output.contains("tags:"));
1567 assert!(output.contains("- database"));
1568 assert!(output.contains("- infrastructure"));
1569 }
1570
1571 #[test]
1572 fn test_render_tags_in_madr_format() {
1573 let template = Template::builtin(TemplateFormat::Madr);
1574 let mut adr = Adr::new(1, "Test ADR");
1575 adr.tags = vec!["api".to_string(), "security".to_string()];
1576
1577 let config = Config::default();
1578 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1579
1580 assert!(output.contains("tags:"));
1582 assert!(output.contains("- api"));
1583 assert!(output.contains("- security"));
1584 }
1585
1586 #[test]
1587 fn test_render_no_tags_section_when_empty() {
1588 let template = Template::builtin(TemplateFormat::Nygard);
1589 let adr = Adr::new(1, "Test ADR");
1590
1591 let config = Config {
1592 mode: ConfigMode::NextGen,
1593 ..Default::default()
1594 };
1595 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1596
1597 assert!(!output.contains("tags:"));
1599 }
1600
1601 #[test]
1604 fn test_pad_filter_default_width() {
1605 let template = Template::from_string("test", "{{ number | pad }}");
1606 let adr = Adr::new(1, "Test");
1607 let config = Config::default();
1608 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1609
1610 assert_eq!(output, "0001\n");
1611 }
1612
1613 #[test]
1614 fn test_pad_filter_custom_width() {
1615 let template = Template::from_string("test", "{{ number | pad(width=6) }}");
1616 let adr = Adr::new(1, "Test");
1617 let config = Config::default();
1618 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1619
1620 assert_eq!(output, "000001\n");
1621 }
1622 #[test]
1625 fn test_template_content_accessor() {
1626 let template = Template::from_string("test", "# {{ title }}");
1627 assert_eq!(template.content(), "# {{ title }}");
1628 }
1629
1630 #[test]
1631 fn test_template_name_accessor() {
1632 let template = Template::from_string("my-template", "# {{ title }}");
1633 assert_eq!(template.name(), "my-template");
1634 }
1635
1636 #[test]
1639 fn test_template_engine_with_variant() {
1640 let engine = TemplateEngine::new().with_variant(TemplateVariant::Minimal);
1641 assert_eq!(engine.default_variant, TemplateVariant::Minimal);
1642 }
1643
1644 #[test]
1645 fn test_template_engine_with_variant_bare() {
1646 let engine = TemplateEngine::new().with_variant(TemplateVariant::Bare);
1647 assert_eq!(engine.default_variant, TemplateVariant::Bare);
1648 let template = engine.template();
1649 assert_eq!(template.name(), "nygard-bare");
1650 }
1651
1652 #[test]
1653 fn test_template_engine_with_format_and_variant() {
1654 let engine = TemplateEngine::new()
1655 .with_format(TemplateFormat::Madr)
1656 .with_variant(TemplateVariant::Minimal);
1657 assert_eq!(engine.default_format, TemplateFormat::Madr);
1658 assert_eq!(engine.default_variant, TemplateVariant::Minimal);
1659 let template = engine.template();
1660 assert_eq!(template.name(), "madr-minimal");
1661 }
1662}