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 }}
540decision-makers:
541consulted:
542informed:
543{% if tags %}tags:
544{% for tag in tags %} - {{ tag }}
545{% endfor %}{% else %}tags:
546{% endif %}---
547
548# {{ title }}
549
550## Context and Problem Statement
551
552
553
554## Decision Drivers
555
556* <!-- decision driver -->
557
558## Considered Options
559
560* <!-- option -->
561
562## Decision Outcome
563
564Chosen option: "", because
565
566### Consequences
567
568* Good, because
569* Bad, because
570
571### Confirmation
572
573
574
575## Pros and Cons of the Options
576
577### <!-- title of option -->
578
579* Good, because
580* Neutral, because
581* Bad, because
582
583## More Information
584
585"#;
586
587const MADR_BARE_MINIMAL_TEMPLATE: &str = r#"# {{ title }}
590
591## Context and Problem Statement
592
593
594
595## Considered Options
596
597
598
599## Decision Outcome
600
601
602
603### Consequences
604
605"#;
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::{AdrLink, AdrStatus, ConfigMode, LinkKind};
611 use std::collections::HashMap;
612 use tempfile::TempDir;
613 use test_case::test_case;
614
615 fn no_link_titles() -> HashMap<u32, (String, String)> {
616 HashMap::new()
617 }
618
619 #[test]
622 fn test_template_format_default() {
623 assert_eq!(TemplateFormat::default(), TemplateFormat::Nygard);
624 }
625
626 #[test_case("nygard" => TemplateFormat::Nygard; "nygard")]
627 #[test_case("Nygard" => TemplateFormat::Nygard; "nygard capitalized")]
628 #[test_case("NYGARD" => TemplateFormat::Nygard; "nygard uppercase")]
629 #[test_case("default" => TemplateFormat::Nygard; "default alias")]
630 #[test_case("madr" => TemplateFormat::Madr; "madr")]
631 #[test_case("MADR" => TemplateFormat::Madr; "madr uppercase")]
632 fn test_template_format_parse(input: &str) -> TemplateFormat {
633 input.parse().unwrap()
634 }
635
636 #[test]
637 fn test_template_format_parse_unknown() {
638 let result: Result<TemplateFormat> = "unknown".parse();
639 assert!(result.is_err());
640 }
641
642 #[test]
643 fn test_template_format_display() {
644 assert_eq!(TemplateFormat::Nygard.to_string(), "nygard");
645 assert_eq!(TemplateFormat::Madr.to_string(), "madr");
646 }
647
648 #[test]
651 fn test_template_variant_default() {
652 assert_eq!(TemplateVariant::default(), TemplateVariant::Full);
653 }
654
655 #[test_case("full" => TemplateVariant::Full; "full")]
656 #[test_case("Full" => TemplateVariant::Full; "full capitalized")]
657 #[test_case("default" => TemplateVariant::Full; "default alias")]
658 #[test_case("minimal" => TemplateVariant::Minimal; "minimal")]
659 #[test_case("min" => TemplateVariant::Minimal; "min alias")]
660 #[test_case("bare" => TemplateVariant::Bare; "bare")]
661 #[test_case("bare-minimal" => TemplateVariant::BareMinimal; "bare-minimal")]
662 #[test_case("bareminimal" => TemplateVariant::BareMinimal; "bareminimal")]
663 #[test_case("empty" => TemplateVariant::BareMinimal; "empty alias")]
664 fn test_template_variant_parse(input: &str) -> TemplateVariant {
665 input.parse().unwrap()
666 }
667
668 #[test]
669 fn test_template_variant_parse_unknown() {
670 let result: Result<TemplateVariant> = "unknown".parse();
671 assert!(result.is_err());
672 }
673
674 #[test]
675 fn test_template_variant_display() {
676 assert_eq!(TemplateVariant::Full.to_string(), "full");
677 assert_eq!(TemplateVariant::Minimal.to_string(), "minimal");
678 assert_eq!(TemplateVariant::Bare.to_string(), "bare");
679 assert_eq!(TemplateVariant::BareMinimal.to_string(), "bare-minimal");
680 }
681
682 #[test]
685 fn test_template_from_string() {
686 let template = Template::from_string("test", "# {{ title }}");
687 assert_eq!(template.name, "test");
688 assert_eq!(template.content, "# {{ title }}");
689 }
690
691 #[test]
692 fn test_template_from_file() {
693 let temp = TempDir::new().unwrap();
694 let path = temp.path().join("custom.md");
695 std::fs::write(&path, "# {{ number }}. {{ title }}").unwrap();
696
697 let template = Template::from_file(&path).unwrap();
698 assert_eq!(template.name, "custom.md");
699 assert!(template.content.contains("{{ number }}"));
700 }
701
702 #[test]
703 fn test_template_from_file_not_found() {
704 let result = Template::from_file(Path::new("/nonexistent/template.md"));
705 assert!(result.is_err());
706 }
707
708 #[test]
709 fn test_template_builtin_nygard() {
710 let template = Template::builtin(TemplateFormat::Nygard);
711 assert_eq!(template.name, "nygard");
712 assert!(template.content.contains("## Status"));
713 assert!(template.content.contains("## Context"));
714 assert!(template.content.contains("## Decision"));
715 assert!(template.content.contains("## Consequences"));
716 }
717
718 #[test]
719 fn test_template_builtin_madr() {
720 let template = Template::builtin(TemplateFormat::Madr);
721 assert_eq!(template.name, "madr");
722 assert!(template.content.contains("Context and Problem Statement"));
723 assert!(template.content.contains("Decision Drivers"));
724 assert!(template.content.contains("Considered Options"));
725 assert!(template.content.contains("Decision Outcome"));
726 }
727
728 #[test]
731 fn test_render_nygard_compatible() {
732 let template = Template::builtin(TemplateFormat::Nygard);
733 let mut adr = Adr::new(1, "Use Rust");
734 adr.status = AdrStatus::Accepted;
735
736 let config = Config::default();
737 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
738
739 assert!(output.contains("# 1. Use Rust"));
740 assert!(output.contains("## Status"));
741 assert!(output.contains("Accepted"));
742 assert!(!output.starts_with("---")); }
744
745 #[test]
746 fn test_render_nygard_all_statuses() {
747 let template = Template::builtin(TemplateFormat::Nygard);
748 let config = Config::default();
749
750 for (status, expected_text) in [
751 (AdrStatus::Proposed, "Proposed"),
752 (AdrStatus::Accepted, "Accepted"),
753 (AdrStatus::Deprecated, "Deprecated"),
754 (AdrStatus::Superseded, "Superseded"),
755 (AdrStatus::Custom("Draft".into()), "Draft"),
756 ] {
757 let mut adr = Adr::new(1, "Test");
758 adr.status = status;
759
760 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
761 assert!(
762 output.contains(expected_text),
763 "Output should contain '{expected_text}': {output}"
764 );
765 }
766 }
767
768 #[test]
769 fn test_render_nygard_with_content() {
770 let template = Template::builtin(TemplateFormat::Nygard);
771 let mut adr = Adr::new(1, "Use Rust");
772 adr.status = AdrStatus::Accepted;
773 adr.context = "We need a safe language.".to_string();
774 adr.decision = "We will use Rust.".to_string();
775 adr.consequences = "Better memory safety.".to_string();
776
777 let config = Config::default();
778 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
779
780 assert!(output.contains("We need a safe language."));
781 assert!(output.contains("We will use Rust."));
782 assert!(output.contains("Better memory safety."));
783 }
784
785 #[test]
786 fn test_render_nygard_with_links() {
787 let template = Template::builtin(TemplateFormat::Nygard);
788 let mut adr = Adr::new(2, "Use PostgreSQL");
789 adr.status = AdrStatus::Accepted;
790 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
791
792 let config = Config::default();
793 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
794
795 assert!(output.contains("Supersedes"));
796 assert!(output.contains("[1. ...]"));
797 assert!(output.contains("0001-....md"));
798 }
799
800 #[test]
801 fn test_render_nygard_with_multiple_links() {
802 let template = Template::builtin(TemplateFormat::Nygard);
803 let mut adr = Adr::new(5, "Combined Decision");
804 adr.status = AdrStatus::Accepted;
805 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
806 adr.links.push(AdrLink::new(2, LinkKind::Amends));
807 adr.links.push(AdrLink::new(3, LinkKind::SupersededBy));
808
809 let config = Config::default();
810 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
811
812 assert!(output.contains("Supersedes"));
813 assert!(output.contains("Amends"));
814 assert!(output.contains("Superseded by"));
815 }
816
817 #[test]
820 fn test_render_nygard_with_resolved_link_titles() {
821 let template = Template::builtin(TemplateFormat::Nygard);
822 let mut adr = Adr::new(3, "Use PostgreSQL instead");
823 adr.status = AdrStatus::Accepted;
824 adr.links.push(AdrLink::new(2, LinkKind::Supersedes));
825
826 let mut link_titles = HashMap::new();
827 link_titles.insert(
828 2,
829 (
830 "Use MySQL for persistence".to_string(),
831 "0002-use-mysql-for-persistence.md".to_string(),
832 ),
833 );
834
835 let config = Config::default();
836 let output = template.render(&adr, &config, &link_titles).unwrap();
837
838 assert!(
839 output.contains(
840 "Supersedes [2. Use MySQL for persistence](0002-use-mysql-for-persistence.md)"
841 ),
842 "Link should contain resolved title and filename. Got:\n{output}"
843 );
844 }
845
846 #[test]
847 fn test_render_nygard_with_resolved_superseded_by_link() {
848 let template = Template::builtin(TemplateFormat::Nygard);
849 let mut adr = Adr::new(2, "Use MySQL");
850 adr.status = AdrStatus::Superseded;
851 adr.links.push(AdrLink::new(3, LinkKind::SupersededBy));
852
853 let mut link_titles = HashMap::new();
854 link_titles.insert(
855 3,
856 (
857 "Use PostgreSQL instead".to_string(),
858 "0003-use-postgresql-instead.md".to_string(),
859 ),
860 );
861
862 let config = Config::default();
863 let output = template.render(&adr, &config, &link_titles).unwrap();
864
865 assert!(
866 output.contains(
867 "Superseded by [3. Use PostgreSQL instead](0003-use-postgresql-instead.md)"
868 ),
869 "Superseded-by link should contain resolved title and filename. Got:\n{output}"
870 );
871 }
872
873 #[test]
874 fn test_render_nygard_with_multiple_resolved_links() {
875 let template = Template::builtin(TemplateFormat::Nygard);
876 let mut adr = Adr::new(5, "Combined Decision");
877 adr.status = AdrStatus::Accepted;
878 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
879 adr.links.push(AdrLink::new(2, LinkKind::Amends));
880
881 let mut link_titles = HashMap::new();
882 link_titles.insert(
883 1,
884 (
885 "Initial Decision".to_string(),
886 "0001-initial-decision.md".to_string(),
887 ),
888 );
889 link_titles.insert(
890 2,
891 (
892 "Second Decision".to_string(),
893 "0002-second-decision.md".to_string(),
894 ),
895 );
896
897 let config = Config::default();
898 let output = template.render(&adr, &config, &link_titles).unwrap();
899
900 assert!(
901 output.contains("Supersedes [1. Initial Decision](0001-initial-decision.md)"),
902 "First link should be resolved. Got:\n{output}"
903 );
904 assert!(
905 output.contains("Amends [2. Second Decision](0002-second-decision.md)"),
906 "Second link should be resolved. Got:\n{output}"
907 );
908 }
909
910 #[test]
911 fn test_render_nygard_unresolved_link_falls_back() {
912 let template = Template::builtin(TemplateFormat::Nygard);
913 let mut adr = Adr::new(2, "Test");
914 adr.status = AdrStatus::Accepted;
915 adr.links.push(AdrLink::new(99, LinkKind::Supersedes));
916
917 let config = Config::default();
918 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
920
921 assert!(
922 output.contains("Supersedes [99. ...](0099-....md)"),
923 "Unresolved link should fall back to '...' placeholder. Got:\n{output}"
924 );
925 }
926
927 #[test]
928 fn test_render_nygard_minimal_with_resolved_links() {
929 let template =
930 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Minimal);
931 let mut adr = Adr::new(2, "New Approach");
932 adr.status = AdrStatus::Accepted;
933 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
934
935 let mut link_titles = HashMap::new();
936 link_titles.insert(
937 1,
938 (
939 "Old Approach".to_string(),
940 "0001-old-approach.md".to_string(),
941 ),
942 );
943
944 let config = Config::default();
945 let output = template.render(&adr, &config, &link_titles).unwrap();
946
947 assert!(
948 output.contains("Supersedes [1. Old Approach](0001-old-approach.md)"),
949 "Minimal template should also resolve link titles. Got:\n{output}"
950 );
951 }
952
953 #[test]
954 fn test_render_nygard_ng_with_resolved_links() {
955 let template = Template::builtin(TemplateFormat::Nygard);
956 let mut adr = Adr::new(2, "New Approach");
957 adr.status = AdrStatus::Accepted;
958 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
959
960 let mut link_titles = HashMap::new();
961 link_titles.insert(
962 1,
963 (
964 "Old Approach".to_string(),
965 "0001-old-approach.md".to_string(),
966 ),
967 );
968
969 let config = Config {
970 mode: ConfigMode::NextGen,
971 ..Default::default()
972 };
973 let output = template.render(&adr, &config, &link_titles).unwrap();
974
975 assert!(
977 output.contains("Supersedes [1. Old Approach](0001-old-approach.md)"),
978 "NG mode body should have resolved links. Got:\n{output}"
979 );
980 assert!(output.contains("links:"));
982 assert!(output.contains("target: 1"));
983 }
984
985 #[test]
988 fn test_render_nygard_ng() {
989 let template = Template::builtin(TemplateFormat::Nygard);
990 let mut adr = Adr::new(1, "Use Rust");
991 adr.status = AdrStatus::Accepted;
992
993 let config = Config {
994 mode: ConfigMode::NextGen,
995 ..Default::default()
996 };
997 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
998
999 assert!(output.starts_with("---")); assert!(output.contains("number: 1"));
1001 assert!(output.contains("title: Use Rust"));
1002 assert!(output.contains("status: accepted"));
1003 }
1004
1005 #[test]
1006 fn test_render_nygard_ng_with_links() {
1007 let template = Template::builtin(TemplateFormat::Nygard);
1008 let mut adr = Adr::new(2, "Test");
1009 adr.status = AdrStatus::Accepted;
1010 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1011
1012 let config = Config {
1013 mode: ConfigMode::NextGen,
1014 ..Default::default()
1015 };
1016 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1017
1018 assert!(output.contains("links:"));
1019 assert!(output.contains("target: 1"));
1020 }
1021
1022 #[test]
1025 fn test_render_madr_basic() {
1026 let template = Template::builtin(TemplateFormat::Madr);
1027 let mut adr = Adr::new(1, "Use Rust");
1028 adr.status = AdrStatus::Accepted;
1029
1030 let config = Config::default();
1031 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1032
1033 assert!(output.starts_with("---")); assert!(output.contains("status: accepted"));
1035 assert!(output.contains("# Use Rust"));
1036 assert!(output.contains("## Context and Problem Statement"));
1037 assert!(output.contains("## Decision Drivers"));
1038 assert!(output.contains("## Considered Options"));
1039 assert!(output.contains("## Decision Outcome"));
1040 assert!(output.contains("## Pros and Cons of the Options"));
1041 }
1042
1043 #[test]
1044 fn test_render_madr_with_decision_makers() {
1045 let template = Template::builtin(TemplateFormat::Madr);
1046 let mut adr = Adr::new(1, "Use Rust");
1047 adr.status = AdrStatus::Accepted;
1048 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
1049
1050 let config = Config::default();
1051 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1052
1053 assert!(output.contains("decision-makers:"));
1054 assert!(output.contains(" - Alice"));
1055 assert!(output.contains(" - Bob"));
1056 }
1057
1058 #[test]
1059 fn test_render_madr_with_consulted() {
1060 let template = Template::builtin(TemplateFormat::Madr);
1061 let mut adr = Adr::new(1, "Use Rust");
1062 adr.status = AdrStatus::Accepted;
1063 adr.consulted = vec!["Carol".into()];
1064
1065 let config = Config::default();
1066 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1067
1068 assert!(output.contains("consulted:"));
1069 assert!(output.contains(" - Carol"));
1070 }
1071
1072 #[test]
1073 fn test_render_madr_with_informed() {
1074 let template = Template::builtin(TemplateFormat::Madr);
1075 let mut adr = Adr::new(1, "Use Rust");
1076 adr.status = AdrStatus::Accepted;
1077 adr.informed = vec!["Dave".into(), "Eve".into()];
1078
1079 let config = Config::default();
1080 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1081
1082 assert!(output.contains("informed:"));
1083 assert!(output.contains(" - Dave"));
1084 assert!(output.contains(" - Eve"));
1085 }
1086
1087 #[test]
1088 fn test_render_madr_full_frontmatter() {
1089 let template = Template::builtin(TemplateFormat::Madr);
1090 let mut adr = Adr::new(1, "Use MADR Format");
1091 adr.status = AdrStatus::Accepted;
1092 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
1093 adr.consulted = vec!["Carol".into()];
1094 adr.informed = vec!["Dave".into()];
1095
1096 let config = Config::default();
1097 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1098
1099 assert!(
1101 output.starts_with("---\nnumber: 1\ntitle: Use MADR Format\nstatus: accepted\ndate:")
1102 );
1103 assert!(output.contains("decision-makers:\n - Alice\n - Bob"));
1104 assert!(output.contains("consulted:\n - Carol"));
1105 assert!(output.contains("informed:\n - Dave"));
1106 assert!(output.contains("---\n\n# Use MADR Format"));
1107 }
1108
1109 #[test]
1110 fn test_render_madr_empty_optional_fields() {
1111 let template = Template::builtin(TemplateFormat::Madr);
1112 let mut adr = Adr::new(1, "Simple ADR");
1113 adr.status = AdrStatus::Proposed;
1114
1115 let config = Config::default();
1116 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1117
1118 assert!(!output.contains("decision-makers:"));
1120 assert!(!output.contains("consulted:"));
1121 assert!(!output.contains("informed:"));
1122 }
1123
1124 #[test]
1127 fn test_nygard_minimal_template() {
1128 let template =
1129 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Minimal);
1130 let adr = Adr::new(1, "Minimal Test");
1131 let config = Config::default();
1132 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1133
1134 assert!(output.contains("# 1. Minimal Test"));
1136 assert!(output.contains("## Status"));
1137 assert!(output.contains("## Context"));
1138 assert!(output.contains("## Decision"));
1139 assert!(output.contains("## Consequences"));
1140 assert!(!output.contains("What is the issue"));
1142 }
1143
1144 #[test]
1145 fn test_nygard_bare_template() {
1146 let template =
1147 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Bare);
1148 let adr = Adr::new(1, "Bare Test");
1149 let config = Config::default();
1150 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1151
1152 assert!(output.contains("# 1. Bare Test"));
1154 assert!(output.contains("## Status"));
1155 assert!(output.contains("## Context"));
1156 assert!(!output.contains("---"));
1158 }
1159
1160 #[test]
1161 fn test_madr_minimal_template() {
1162 let template =
1163 Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Minimal);
1164 let adr = Adr::new(1, "MADR Minimal");
1165 let config = Config::default();
1166 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1167
1168 assert!(!output.starts_with("---"));
1170 assert!(output.contains("# MADR Minimal"));
1171 assert!(output.contains("## Context and Problem Statement"));
1172 assert!(output.contains("## Considered Options"));
1173 assert!(output.contains("## Decision Outcome"));
1174 assert!(!output.contains("## Decision Drivers"));
1176 assert!(!output.contains("## Pros and Cons"));
1177 }
1178
1179 #[test]
1180 fn test_madr_bare_template() {
1181 let template = Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Bare);
1182 let adr = Adr::new(1, "MADR Bare");
1183 let config = Config::default();
1184 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1185
1186 assert!(output.starts_with("---"));
1188 assert!(output.contains("status:"));
1189 assert!(output.contains("decision-makers:"));
1190 assert!(output.contains("consulted:"));
1191 assert!(output.contains("informed:"));
1192 assert!(output.contains("# MADR Bare"));
1193 assert!(output.contains("## Decision Drivers"));
1195 assert!(output.contains("## Considered Options"));
1196 assert!(output.contains("## Pros and Cons of the Options"));
1197 assert!(output.contains("## More Information"));
1198 }
1199
1200 #[test]
1201 fn test_madr_bare_minimal_template() {
1202 let template =
1203 Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::BareMinimal);
1204 let adr = Adr::new(1, "MADR Bare Minimal");
1205 let config = Config::default();
1206 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1207
1208 assert!(!output.starts_with("---"));
1210 assert!(output.contains("# MADR Bare Minimal"));
1211 assert!(output.contains("## Context and Problem Statement"));
1212 assert!(output.contains("## Considered Options"));
1213 assert!(output.contains("## Decision Outcome"));
1214 assert!(output.contains("### Consequences"));
1215 assert!(!output.contains("## Decision Drivers"));
1217 assert!(!output.contains("## Pros and Cons"));
1218 }
1219
1220 #[test]
1221 fn test_nygard_bare_minimal_template() {
1222 let template =
1223 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::BareMinimal);
1224 let adr = Adr::new(1, "Nygard Bare Minimal");
1225 let config = Config::default();
1226 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1227
1228 assert!(output.contains("# 1. Nygard Bare Minimal"));
1230 assert!(output.contains("## Status"));
1231 assert!(output.contains("## Context"));
1232 assert!(output.contains("## Decision"));
1233 assert!(output.contains("## Consequences"));
1234 assert!(!output.contains("---"));
1236 assert!(!output.contains("Date:"));
1237 }
1238
1239 #[test]
1240 fn test_builtin_defaults_to_full() {
1241 let full = Template::builtin(TemplateFormat::Nygard);
1242 let explicit_full =
1243 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Full);
1244
1245 assert_eq!(full.name, explicit_full.name);
1246 assert_eq!(full.content, explicit_full.content);
1247 }
1248
1249 #[test]
1252 fn test_template_engine_new() {
1253 let engine = TemplateEngine::new();
1254 assert_eq!(engine.default_format, TemplateFormat::Nygard);
1255 assert_eq!(engine.default_variant, TemplateVariant::Full);
1256 assert!(engine.custom_template.is_none());
1257 }
1258
1259 #[test]
1260 fn test_template_engine_default() {
1261 let engine = TemplateEngine::default();
1262 assert_eq!(engine.default_format, TemplateFormat::Nygard);
1263 assert_eq!(engine.default_variant, TemplateVariant::Full);
1264 }
1265
1266 #[test]
1267 fn test_template_engine_with_format() {
1268 let engine = TemplateEngine::new().with_format(TemplateFormat::Madr);
1269 assert_eq!(engine.default_format, TemplateFormat::Madr);
1270 }
1271
1272 #[test]
1273 fn test_template_engine_with_custom_template() {
1274 let custom = Template::from_string("custom", "# {{ title }}");
1275 let engine = TemplateEngine::new().with_custom_template(custom);
1276 assert!(engine.custom_template.is_some());
1277 }
1278
1279 #[test]
1280 fn test_template_engine_with_custom_template_file() {
1281 let temp = TempDir::new().unwrap();
1282 let path = temp.path().join("template.md");
1283 std::fs::write(&path, "# {{ title }}").unwrap();
1284
1285 let engine = TemplateEngine::new()
1286 .with_custom_template_file(&path)
1287 .unwrap();
1288 assert!(engine.custom_template.is_some());
1289 }
1290
1291 #[test]
1292 fn test_template_engine_with_custom_template_file_not_found() {
1293 let result = TemplateEngine::new().with_custom_template_file(Path::new("/nonexistent.md"));
1294 assert!(result.is_err());
1295 }
1296
1297 #[test]
1298 fn test_template_engine_template_builtin() {
1299 let engine = TemplateEngine::new();
1300 let template = engine.template();
1301 assert_eq!(template.name, "nygard");
1302 }
1303
1304 #[test]
1305 fn test_template_engine_template_custom() {
1306 let custom = Template::from_string("my-template", "# Custom");
1307 let engine = TemplateEngine::new().with_custom_template(custom);
1308 let template = engine.template();
1309 assert_eq!(template.name, "my-template");
1310 }
1311
1312 #[test]
1313 fn test_template_engine_render() {
1314 let engine = TemplateEngine::new();
1315 let adr = Adr::new(1, "Test");
1316 let config = Config::default();
1317
1318 let output = engine.render(&adr, &config, &no_link_titles()).unwrap();
1319 assert!(output.contains("# 1. Test"));
1320 }
1321
1322 #[test]
1323 fn test_template_engine_render_custom() {
1324 let custom = Template::from_string("custom", "ADR {{ number }}: {{ title }}");
1325 let engine = TemplateEngine::new().with_custom_template(custom);
1326 let adr = Adr::new(42, "Custom ADR");
1327 let config = Config::default();
1328
1329 let output = engine.render(&adr, &config, &no_link_titles()).unwrap();
1330 assert_eq!(output, "ADR 42: Custom ADR");
1331 }
1332
1333 #[test]
1336 fn test_custom_template_all_fields() {
1337 let custom = Template::from_string(
1338 "full",
1339 r#"# {{ number }}. {{ title }}
1340Date: {{ date }}
1341Status: {{ status }}
1342Context: {{ context }}
1343Decision: {{ decision }}
1344Consequences: {{ consequences }}
1345Links: {% for link in links %}{{ link.kind }} {{ link.target }}{% endfor %}"#,
1346 );
1347
1348 let mut adr = Adr::new(1, "Test");
1349 adr.status = AdrStatus::Accepted;
1350 adr.context = "Context text".into();
1351 adr.decision = "Decision text".into();
1352 adr.consequences = "Consequences text".into();
1353 adr.links.push(AdrLink::new(2, LinkKind::Amends));
1354
1355 let config = Config::default();
1356 let output = custom.render(&adr, &config, &no_link_titles()).unwrap();
1357
1358 assert!(output.contains("# 1. Test"));
1359 assert!(output.contains("Status: Accepted"));
1360 assert!(output.contains("Context: Context text"));
1361 assert!(output.contains("Decision: Decision text"));
1362 assert!(output.contains("Consequences: Consequences text"));
1363 assert!(output.contains("Amends 2"));
1364 }
1365
1366 #[test]
1367 fn test_custom_template_is_ng_flag() {
1368 let custom = Template::from_string(
1369 "ng-check",
1370 r#"{% if is_ng %}NextGen Mode{% else %}Compatible Mode{% endif %}"#,
1371 );
1372
1373 let adr = Adr::new(1, "Test");
1374
1375 let compat_config = Config::default();
1376 let output = custom
1377 .render(&adr, &compat_config, &no_link_titles())
1378 .unwrap();
1379 assert_eq!(output, "Compatible Mode");
1380
1381 let ng_config = Config {
1382 mode: ConfigMode::NextGen,
1383 ..Default::default()
1384 };
1385 let output = custom.render(&adr, &ng_config, &no_link_titles()).unwrap();
1386 assert_eq!(output, "NextGen Mode");
1387 }
1388
1389 #[test]
1390 fn test_custom_template_link_kinds() {
1391 let custom = Template::from_string(
1392 "links",
1393 r#"{% for link in links %}{{ link.kind }}|{% endfor %}"#,
1394 );
1395
1396 let mut adr = Adr::new(1, "Test");
1397 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1398 adr.links.push(AdrLink::new(2, LinkKind::SupersededBy));
1399 adr.links.push(AdrLink::new(3, LinkKind::Amends));
1400 adr.links.push(AdrLink::new(4, LinkKind::AmendedBy));
1401 adr.links.push(AdrLink::new(5, LinkKind::RelatesTo));
1402 adr.links
1403 .push(AdrLink::new(6, LinkKind::Custom("Depends on".into())));
1404
1405 let config = Config::default();
1406 let output = custom.render(&adr, &config, &no_link_titles()).unwrap();
1407
1408 assert!(output.contains("Supersedes|"));
1409 assert!(output.contains("Superseded by|"));
1410 assert!(output.contains("Amends|"));
1411 assert!(output.contains("Amended by|"));
1412 assert!(output.contains("Relates to|"));
1413 assert!(output.contains("Depends on|"));
1414 }
1415
1416 #[test]
1419 fn test_template_invalid_syntax() {
1420 let custom = Template::from_string("invalid", "{{ unclosed");
1421 let adr = Adr::new(1, "Test");
1422 let config = Config::default();
1423
1424 let result = custom.render(&adr, &config, &no_link_titles());
1425 assert!(result.is_err());
1426 }
1427
1428 #[test]
1429 fn test_template_undefined_variable() {
1430 let custom = Template::from_string("undefined", "{{ nonexistent }}");
1431 let adr = Adr::new(1, "Test");
1432 let config = Config::default();
1433
1434 let result = custom.render(&adr, &config, &no_link_titles());
1436 assert!(result.is_ok());
1437 }
1438
1439 #[test]
1442 fn test_render_four_digit_number() {
1443 let template = Template::builtin(TemplateFormat::Nygard);
1444 let adr = Adr::new(9999, "Large Number");
1445 let config = Config::default();
1446
1447 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1448 assert!(output.contains("# 9999. Large Number"));
1449 }
1450
1451 #[test]
1452 fn test_render_link_number_formatting() {
1453 let template = Template::builtin(TemplateFormat::Nygard);
1454 let mut adr = Adr::new(2, "Test");
1455 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1456
1457 let config = Config::default();
1458 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1459
1460 assert!(output.contains("0001-"));
1462 }
1463
1464 #[test]
1467 fn test_render_tags_in_nextgen_mode() {
1468 let template = Template::builtin(TemplateFormat::Nygard);
1469 let mut adr = Adr::new(1, "Test ADR");
1470 adr.tags = vec!["database".to_string(), "infrastructure".to_string()];
1471
1472 let config = Config {
1473 mode: ConfigMode::NextGen,
1474 ..Default::default()
1475 };
1476 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1477
1478 assert!(output.contains("tags:"));
1480 assert!(output.contains("- database"));
1481 assert!(output.contains("- infrastructure"));
1482 }
1483
1484 #[test]
1485 fn test_render_tags_in_madr_format() {
1486 let template = Template::builtin(TemplateFormat::Madr);
1487 let mut adr = Adr::new(1, "Test ADR");
1488 adr.tags = vec!["api".to_string(), "security".to_string()];
1489
1490 let config = Config::default();
1491 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1492
1493 assert!(output.contains("tags:"));
1495 assert!(output.contains("- api"));
1496 assert!(output.contains("- security"));
1497 }
1498
1499 #[test]
1500 fn test_render_no_tags_section_when_empty() {
1501 let template = Template::builtin(TemplateFormat::Nygard);
1502 let adr = Adr::new(1, "Test ADR");
1503
1504 let config = Config {
1505 mode: ConfigMode::NextGen,
1506 ..Default::default()
1507 };
1508 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1509
1510 assert!(!output.contains("tags:"));
1512 }
1513
1514 #[test]
1517 fn test_pad_filter_default_width() {
1518 let template = Template::from_string("test", "{{ number | pad }}");
1519 let adr = Adr::new(1, "Test");
1520 let config = Config::default();
1521 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1522
1523 assert_eq!(output, "0001");
1524 }
1525
1526 #[test]
1527 fn test_pad_filter_custom_width() {
1528 let template = Template::from_string("test", "{{ number | pad(width=6) }}");
1529 let adr = Adr::new(1, "Test");
1530 let config = Config::default();
1531 let output = template.render(&adr, &config, &no_link_titles()).unwrap();
1532
1533 assert_eq!(output, "000001");
1534 }
1535}