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