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)]
41pub enum TemplateVariant {
42 #[default]
44 Full,
45
46 Minimal,
48
49 Bare,
51}
52
53impl std::fmt::Display for TemplateVariant {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::Full => write!(f, "full"),
57 Self::Minimal => write!(f, "minimal"),
58 Self::Bare => write!(f, "bare"),
59 }
60 }
61}
62
63impl std::str::FromStr for TemplateVariant {
64 type Err = Error;
65
66 fn from_str(s: &str) -> Result<Self> {
67 match s.to_lowercase().as_str() {
68 "full" | "default" => Ok(Self::Full),
69 "minimal" | "min" => Ok(Self::Minimal),
70 "bare" | "empty" => Ok(Self::Bare),
71 _ => Err(Error::TemplateNotFound(format!("Unknown variant: {s}"))),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct Template {
79 content: String,
81
82 name: String,
84}
85
86impl Template {
87 pub fn from_string(name: impl Into<String>, content: impl Into<String>) -> Self {
89 Self {
90 name: name.into(),
91 content: content.into(),
92 }
93 }
94
95 pub fn from_file(path: &Path) -> Result<Self> {
97 let content = std::fs::read_to_string(path)?;
98 let name = path
99 .file_name()
100 .and_then(|n| n.to_str())
101 .unwrap_or("custom")
102 .to_string();
103 Ok(Self { name, content })
104 }
105
106 pub fn builtin(format: TemplateFormat) -> Self {
108 Self::builtin_with_variant(format, TemplateVariant::Full)
109 }
110
111 pub fn builtin_with_variant(format: TemplateFormat, variant: TemplateVariant) -> Self {
113 let (name, content) = match (format, variant) {
114 (TemplateFormat::Nygard, TemplateVariant::Full) => ("nygard", NYGARD_TEMPLATE),
116 (TemplateFormat::Nygard, TemplateVariant::Minimal) => {
117 ("nygard-minimal", NYGARD_MINIMAL_TEMPLATE)
118 }
119 (TemplateFormat::Nygard, TemplateVariant::Bare) => {
120 ("nygard-bare", NYGARD_BARE_TEMPLATE)
121 }
122
123 (TemplateFormat::Madr, TemplateVariant::Full) => ("madr", MADR_TEMPLATE),
125 (TemplateFormat::Madr, TemplateVariant::Minimal) => {
126 ("madr-minimal", MADR_MINIMAL_TEMPLATE)
127 }
128 (TemplateFormat::Madr, TemplateVariant::Bare) => ("madr-bare", MADR_BARE_TEMPLATE),
129 };
130 Self::from_string(name, content)
131 }
132
133 pub fn render(&self, adr: &Adr, config: &Config) -> Result<String> {
135 use crate::LinkKind;
136
137 let mut env = Environment::new();
138 env.add_template(&self.name, &self.content)
139 .map_err(|e| Error::TemplateError(e.to_string()))?;
140
141 let template = env
142 .get_template(&self.name)
143 .map_err(|e| Error::TemplateError(e.to_string()))?;
144
145 let links: Vec<_> = adr
147 .links
148 .iter()
149 .map(|link| {
150 let kind_display = match &link.kind {
151 LinkKind::Supersedes => "Supersedes",
152 LinkKind::SupersededBy => "Superseded by",
153 LinkKind::Amends => "Amends",
154 LinkKind::AmendedBy => "Amended by",
155 LinkKind::RelatesTo => "Relates to",
156 LinkKind::Custom(s) => s.as_str(),
157 };
158 context! {
159 target => link.target,
160 kind => kind_display,
161 description => &link.description,
162 }
163 })
164 .collect();
165
166 let output = template
167 .render(context! {
168 number => adr.number,
169 title => &adr.title,
170 date => crate::parse::format_date(adr.date),
171 status => adr.status.to_string(),
172 context => &adr.context,
173 decision => &adr.decision,
174 consequences => &adr.consequences,
175 links => links,
176 is_ng => config.is_next_gen(),
177 decision_makers => &adr.decision_makers,
179 consulted => &adr.consulted,
180 informed => &adr.informed,
181 })
182 .map_err(|e| Error::TemplateError(e.to_string()))?;
183
184 Ok(output)
185 }
186}
187
188#[derive(Debug)]
190pub struct TemplateEngine {
191 default_format: TemplateFormat,
193
194 default_variant: TemplateVariant,
196
197 custom_template: Option<Template>,
199}
200
201impl Default for TemplateEngine {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207impl TemplateEngine {
208 pub fn new() -> Self {
210 Self {
211 default_format: TemplateFormat::default(),
212 default_variant: TemplateVariant::default(),
213 custom_template: None,
214 }
215 }
216
217 pub fn with_format(mut self, format: TemplateFormat) -> Self {
219 self.default_format = format;
220 self
221 }
222
223 pub fn with_variant(mut self, variant: TemplateVariant) -> Self {
225 self.default_variant = variant;
226 self
227 }
228
229 pub fn with_custom_template(mut self, template: Template) -> Self {
231 self.custom_template = Some(template);
232 self
233 }
234
235 pub fn with_custom_template_file(mut self, path: &Path) -> Result<Self> {
237 self.custom_template = Some(Template::from_file(path)?);
238 Ok(self)
239 }
240
241 pub fn template(&self) -> Template {
243 self.custom_template.clone().unwrap_or_else(|| {
244 Template::builtin_with_variant(self.default_format, self.default_variant)
245 })
246 }
247
248 pub fn render(&self, adr: &Adr, config: &Config) -> Result<String> {
250 self.template().render(adr, config)
251 }
252}
253
254const NYGARD_TEMPLATE: &str = r#"{% if is_ng %}---
256number: {{ number }}
257title: {{ title }}
258date: {{ date }}
259status: {{ status | lower }}
260{% if links %}links:
261{% for link in links %} - target: {{ link.target }}
262 kind: {{ link.kind | lower }}
263{% endfor %}{% endif %}---
264
265{% endif %}# {{ number }}. {{ title }}
266
267Date: {{ date }}
268
269## Status
270
271{{ status }}
272{% for link in links %}
273{{ link.kind }} [{{ link.target }}. ...]({{ "%04d" | format(link.target) }}-....md)
274{% endfor %}
275## Context
276
277{{ context if context else "What is the issue that we're seeing that is motivating this decision or change?" }}
278
279## Decision
280
281{{ decision if decision else "What is the change that we're proposing and/or doing?" }}
282
283## Consequences
284
285{{ consequences if consequences else "What becomes easier or more difficult to do because of this change?" }}
286"#;
287
288const MADR_TEMPLATE: &str = r#"---
290status: {{ status | lower }}
291date: {{ date }}
292{% if decision_makers %}decision-makers:
293{% for dm in decision_makers %} - {{ dm }}
294{% endfor %}{% endif %}{% if consulted %}consulted:
295{% for c in consulted %} - {{ c }}
296{% endfor %}{% endif %}{% if informed %}informed:
297{% for i in informed %} - {{ i }}
298{% endfor %}{% endif %}---
299
300# {{ title }}
301
302## Context and Problem Statement
303
304{{ 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.}" }}
305
306<!-- This is an optional element. Feel free to remove. -->
307## Decision Drivers
308
309* {decision driver 1, e.g., a force, facing concern, ...}
310* {decision driver 2, e.g., a force, facing concern, ...}
311* ... <!-- numbers of drivers can vary -->
312
313## Considered Options
314
315* {title of option 1}
316* {title of option 2}
317* {title of option 3}
318* ... <!-- numbers of options can vary -->
319
320## Decision Outcome
321
322{{ 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)}." }}
323
324<!-- This is an optional element. Feel free to remove. -->
325### Consequences
326
327{{ 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 -->" }}
328
329<!-- This is an optional element. Feel free to remove. -->
330### Confirmation
331
332{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.}
333
334<!-- This is an optional element. Feel free to remove. -->
335## Pros and Cons of the Options
336
337### {title of option 1}
338
339<!-- This is an optional element. Feel free to remove. -->
340{example | description | pointer to more information | ...}
341
342* Good, because {argument a}
343* Good, because {argument b}
344<!-- use "neutral" if the given argument weights neither for good nor bad -->
345* Neutral, because {argument c}
346* Bad, because {argument d}
347* ... <!-- numbers of pros and cons can vary -->
348
349### {title of other option}
350
351{example | description | pointer to more information | ...}
352
353* Good, because {argument a}
354* Good, because {argument b}
355* Neutral, because {argument c}
356* Bad, because {argument d}
357* ...
358
359<!-- This is an optional element. Feel free to remove. -->
360## More Information
361
362{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.}
363"#;
364
365const NYGARD_MINIMAL_TEMPLATE: &str = r#"{% if is_ng %}---
367number: {{ number }}
368title: {{ title }}
369date: {{ date }}
370status: {{ status | lower }}
371{% if links %}links:
372{% for link in links %} - target: {{ link.target }}
373 kind: {{ link.kind | lower }}
374{% endfor %}{% endif %}---
375
376{% endif %}# {{ number }}. {{ title }}
377
378Date: {{ date }}
379
380## Status
381
382{{ status }}
383{% for link in links %}
384{{ link.kind }} [{{ link.target }}. ...]({{ "%04d" | format(link.target) }}-....md)
385{% endfor %}
386## Context
387
388{{ context if context else "" }}
389
390## Decision
391
392{{ decision if decision else "" }}
393
394## Consequences
395
396{{ consequences if consequences else "" }}
397"#;
398
399const NYGARD_BARE_TEMPLATE: &str = r#"# {{ number }}. {{ title }}
401
402Date: {{ date }}
403
404## Status
405
406{{ status }}
407
408## Context
409
410
411
412## Decision
413
414
415
416## Consequences
417
418"#;
419
420const MADR_MINIMAL_TEMPLATE: &str = r#"---
422status: {{ status | lower }}
423date: {{ date }}
424{% if decision_makers %}decision-makers:
425{% for dm in decision_makers %} - {{ dm }}
426{% endfor %}{% endif %}{% if consulted %}consulted:
427{% for c in consulted %} - {{ c }}
428{% endfor %}{% endif %}{% if informed %}informed:
429{% for i in informed %} - {{ i }}
430{% endfor %}{% endif %}---
431
432# {{ title }}
433
434## Context and Problem Statement
435
436{{ context if context else "" }}
437
438## Decision Outcome
439
440{{ decision if decision else "" }}
441
442### Consequences
443
444{{ consequences if consequences else "" }}
445"#;
446
447const MADR_BARE_TEMPLATE: &str = r#"---
449status: {{ status | lower }}
450date: {{ date }}
451---
452
453# {{ title }}
454
455## Context and Problem Statement
456
457
458
459## Decision Outcome
460
461
462
463### Consequences
464
465"#;
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::{AdrLink, AdrStatus, ConfigMode, LinkKind};
471 use tempfile::TempDir;
472 use test_case::test_case;
473
474 #[test]
477 fn test_template_format_default() {
478 assert_eq!(TemplateFormat::default(), TemplateFormat::Nygard);
479 }
480
481 #[test_case("nygard" => TemplateFormat::Nygard; "nygard")]
482 #[test_case("Nygard" => TemplateFormat::Nygard; "nygard capitalized")]
483 #[test_case("NYGARD" => TemplateFormat::Nygard; "nygard uppercase")]
484 #[test_case("default" => TemplateFormat::Nygard; "default alias")]
485 #[test_case("madr" => TemplateFormat::Madr; "madr")]
486 #[test_case("MADR" => TemplateFormat::Madr; "madr uppercase")]
487 fn test_template_format_parse(input: &str) -> TemplateFormat {
488 input.parse().unwrap()
489 }
490
491 #[test]
492 fn test_template_format_parse_unknown() {
493 let result: Result<TemplateFormat> = "unknown".parse();
494 assert!(result.is_err());
495 }
496
497 #[test]
498 fn test_template_format_display() {
499 assert_eq!(TemplateFormat::Nygard.to_string(), "nygard");
500 assert_eq!(TemplateFormat::Madr.to_string(), "madr");
501 }
502
503 #[test]
506 fn test_template_variant_default() {
507 assert_eq!(TemplateVariant::default(), TemplateVariant::Full);
508 }
509
510 #[test_case("full" => TemplateVariant::Full; "full")]
511 #[test_case("Full" => TemplateVariant::Full; "full capitalized")]
512 #[test_case("default" => TemplateVariant::Full; "default alias")]
513 #[test_case("minimal" => TemplateVariant::Minimal; "minimal")]
514 #[test_case("min" => TemplateVariant::Minimal; "min alias")]
515 #[test_case("bare" => TemplateVariant::Bare; "bare")]
516 #[test_case("empty" => TemplateVariant::Bare; "empty alias")]
517 fn test_template_variant_parse(input: &str) -> TemplateVariant {
518 input.parse().unwrap()
519 }
520
521 #[test]
522 fn test_template_variant_parse_unknown() {
523 let result: Result<TemplateVariant> = "unknown".parse();
524 assert!(result.is_err());
525 }
526
527 #[test]
528 fn test_template_variant_display() {
529 assert_eq!(TemplateVariant::Full.to_string(), "full");
530 assert_eq!(TemplateVariant::Minimal.to_string(), "minimal");
531 assert_eq!(TemplateVariant::Bare.to_string(), "bare");
532 }
533
534 #[test]
537 fn test_template_from_string() {
538 let template = Template::from_string("test", "# {{ title }}");
539 assert_eq!(template.name, "test");
540 assert_eq!(template.content, "# {{ title }}");
541 }
542
543 #[test]
544 fn test_template_from_file() {
545 let temp = TempDir::new().unwrap();
546 let path = temp.path().join("custom.md");
547 std::fs::write(&path, "# {{ number }}. {{ title }}").unwrap();
548
549 let template = Template::from_file(&path).unwrap();
550 assert_eq!(template.name, "custom.md");
551 assert!(template.content.contains("{{ number }}"));
552 }
553
554 #[test]
555 fn test_template_from_file_not_found() {
556 let result = Template::from_file(Path::new("/nonexistent/template.md"));
557 assert!(result.is_err());
558 }
559
560 #[test]
561 fn test_template_builtin_nygard() {
562 let template = Template::builtin(TemplateFormat::Nygard);
563 assert_eq!(template.name, "nygard");
564 assert!(template.content.contains("## Status"));
565 assert!(template.content.contains("## Context"));
566 assert!(template.content.contains("## Decision"));
567 assert!(template.content.contains("## Consequences"));
568 }
569
570 #[test]
571 fn test_template_builtin_madr() {
572 let template = Template::builtin(TemplateFormat::Madr);
573 assert_eq!(template.name, "madr");
574 assert!(template.content.contains("Context and Problem Statement"));
575 assert!(template.content.contains("Decision Drivers"));
576 assert!(template.content.contains("Considered Options"));
577 assert!(template.content.contains("Decision Outcome"));
578 }
579
580 #[test]
583 fn test_render_nygard_compatible() {
584 let template = Template::builtin(TemplateFormat::Nygard);
585 let mut adr = Adr::new(1, "Use Rust");
586 adr.status = AdrStatus::Accepted;
587
588 let config = Config::default();
589 let output = template.render(&adr, &config).unwrap();
590
591 assert!(output.contains("# 1. Use Rust"));
592 assert!(output.contains("## Status"));
593 assert!(output.contains("Accepted"));
594 assert!(!output.starts_with("---")); }
596
597 #[test]
598 fn test_render_nygard_all_statuses() {
599 let template = Template::builtin(TemplateFormat::Nygard);
600 let config = Config::default();
601
602 for (status, expected_text) in [
603 (AdrStatus::Proposed, "Proposed"),
604 (AdrStatus::Accepted, "Accepted"),
605 (AdrStatus::Deprecated, "Deprecated"),
606 (AdrStatus::Superseded, "Superseded"),
607 (AdrStatus::Custom("Draft".into()), "Draft"),
608 ] {
609 let mut adr = Adr::new(1, "Test");
610 adr.status = status;
611
612 let output = template.render(&adr, &config).unwrap();
613 assert!(
614 output.contains(expected_text),
615 "Output should contain '{expected_text}': {output}"
616 );
617 }
618 }
619
620 #[test]
621 fn test_render_nygard_with_content() {
622 let template = Template::builtin(TemplateFormat::Nygard);
623 let mut adr = Adr::new(1, "Use Rust");
624 adr.status = AdrStatus::Accepted;
625 adr.context = "We need a safe language.".to_string();
626 adr.decision = "We will use Rust.".to_string();
627 adr.consequences = "Better memory safety.".to_string();
628
629 let config = Config::default();
630 let output = template.render(&adr, &config).unwrap();
631
632 assert!(output.contains("We need a safe language."));
633 assert!(output.contains("We will use Rust."));
634 assert!(output.contains("Better memory safety."));
635 }
636
637 #[test]
638 fn test_render_nygard_with_links() {
639 let template = Template::builtin(TemplateFormat::Nygard);
640 let mut adr = Adr::new(2, "Use PostgreSQL");
641 adr.status = AdrStatus::Accepted;
642 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
643
644 let config = Config::default();
645 let output = template.render(&adr, &config).unwrap();
646
647 assert!(output.contains("Supersedes"));
648 assert!(output.contains("[1. ...]"));
649 assert!(output.contains("0001-....md"));
650 }
651
652 #[test]
653 fn test_render_nygard_with_multiple_links() {
654 let template = Template::builtin(TemplateFormat::Nygard);
655 let mut adr = Adr::new(5, "Combined Decision");
656 adr.status = AdrStatus::Accepted;
657 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
658 adr.links.push(AdrLink::new(2, LinkKind::Amends));
659 adr.links.push(AdrLink::new(3, LinkKind::SupersededBy));
660
661 let config = Config::default();
662 let output = template.render(&adr, &config).unwrap();
663
664 assert!(output.contains("Supersedes"));
665 assert!(output.contains("Amends"));
666 assert!(output.contains("Superseded by"));
667 }
668
669 #[test]
672 fn test_render_nygard_ng() {
673 let template = Template::builtin(TemplateFormat::Nygard);
674 let mut adr = Adr::new(1, "Use Rust");
675 adr.status = AdrStatus::Accepted;
676
677 let config = Config {
678 mode: ConfigMode::NextGen,
679 ..Default::default()
680 };
681 let output = template.render(&adr, &config).unwrap();
682
683 assert!(output.starts_with("---")); assert!(output.contains("number: 1"));
685 assert!(output.contains("title: Use Rust"));
686 assert!(output.contains("status: accepted"));
687 }
688
689 #[test]
690 fn test_render_nygard_ng_with_links() {
691 let template = Template::builtin(TemplateFormat::Nygard);
692 let mut adr = Adr::new(2, "Test");
693 adr.status = AdrStatus::Accepted;
694 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
695
696 let config = Config {
697 mode: ConfigMode::NextGen,
698 ..Default::default()
699 };
700 let output = template.render(&adr, &config).unwrap();
701
702 assert!(output.contains("links:"));
703 assert!(output.contains("target: 1"));
704 }
705
706 #[test]
709 fn test_render_madr_basic() {
710 let template = Template::builtin(TemplateFormat::Madr);
711 let mut adr = Adr::new(1, "Use Rust");
712 adr.status = AdrStatus::Accepted;
713
714 let config = Config::default();
715 let output = template.render(&adr, &config).unwrap();
716
717 assert!(output.starts_with("---")); assert!(output.contains("status: accepted"));
719 assert!(output.contains("# Use Rust"));
720 assert!(output.contains("## Context and Problem Statement"));
721 assert!(output.contains("## Decision Drivers"));
722 assert!(output.contains("## Considered Options"));
723 assert!(output.contains("## Decision Outcome"));
724 assert!(output.contains("## Pros and Cons of the Options"));
725 }
726
727 #[test]
728 fn test_render_madr_with_decision_makers() {
729 let template = Template::builtin(TemplateFormat::Madr);
730 let mut adr = Adr::new(1, "Use Rust");
731 adr.status = AdrStatus::Accepted;
732 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
733
734 let config = Config::default();
735 let output = template.render(&adr, &config).unwrap();
736
737 assert!(output.contains("decision-makers:"));
738 assert!(output.contains(" - Alice"));
739 assert!(output.contains(" - Bob"));
740 }
741
742 #[test]
743 fn test_render_madr_with_consulted() {
744 let template = Template::builtin(TemplateFormat::Madr);
745 let mut adr = Adr::new(1, "Use Rust");
746 adr.status = AdrStatus::Accepted;
747 adr.consulted = vec!["Carol".into()];
748
749 let config = Config::default();
750 let output = template.render(&adr, &config).unwrap();
751
752 assert!(output.contains("consulted:"));
753 assert!(output.contains(" - Carol"));
754 }
755
756 #[test]
757 fn test_render_madr_with_informed() {
758 let template = Template::builtin(TemplateFormat::Madr);
759 let mut adr = Adr::new(1, "Use Rust");
760 adr.status = AdrStatus::Accepted;
761 adr.informed = vec!["Dave".into(), "Eve".into()];
762
763 let config = Config::default();
764 let output = template.render(&adr, &config).unwrap();
765
766 assert!(output.contains("informed:"));
767 assert!(output.contains(" - Dave"));
768 assert!(output.contains(" - Eve"));
769 }
770
771 #[test]
772 fn test_render_madr_full_frontmatter() {
773 let template = Template::builtin(TemplateFormat::Madr);
774 let mut adr = Adr::new(1, "Use MADR Format");
775 adr.status = AdrStatus::Accepted;
776 adr.decision_makers = vec!["Alice".into(), "Bob".into()];
777 adr.consulted = vec!["Carol".into()];
778 adr.informed = vec!["Dave".into()];
779
780 let config = Config::default();
781 let output = template.render(&adr, &config).unwrap();
782
783 assert!(output.starts_with("---\nstatus: accepted\ndate:"));
785 assert!(output.contains("decision-makers:\n - Alice\n - Bob"));
786 assert!(output.contains("consulted:\n - Carol"));
787 assert!(output.contains("informed:\n - Dave"));
788 assert!(output.contains("---\n\n# Use MADR Format"));
789 }
790
791 #[test]
792 fn test_render_madr_empty_optional_fields() {
793 let template = Template::builtin(TemplateFormat::Madr);
794 let mut adr = Adr::new(1, "Simple ADR");
795 adr.status = AdrStatus::Proposed;
796
797 let config = Config::default();
798 let output = template.render(&adr, &config).unwrap();
799
800 assert!(!output.contains("decision-makers:"));
802 assert!(!output.contains("consulted:"));
803 assert!(!output.contains("informed:"));
804 }
805
806 #[test]
809 fn test_nygard_minimal_template() {
810 let template =
811 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Minimal);
812 let adr = Adr::new(1, "Minimal Test");
813 let config = Config::default();
814 let output = template.render(&adr, &config).unwrap();
815
816 assert!(output.contains("# 1. Minimal Test"));
818 assert!(output.contains("## Status"));
819 assert!(output.contains("## Context"));
820 assert!(output.contains("## Decision"));
821 assert!(output.contains("## Consequences"));
822 assert!(!output.contains("What is the issue"));
824 }
825
826 #[test]
827 fn test_nygard_bare_template() {
828 let template =
829 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Bare);
830 let adr = Adr::new(1, "Bare Test");
831 let config = Config::default();
832 let output = template.render(&adr, &config).unwrap();
833
834 assert!(output.contains("# 1. Bare Test"));
836 assert!(output.contains("## Status"));
837 assert!(output.contains("## Context"));
838 assert!(!output.contains("---"));
840 }
841
842 #[test]
843 fn test_madr_minimal_template() {
844 let template =
845 Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Minimal);
846 let adr = Adr::new(1, "MADR Minimal");
847 let config = Config::default();
848 let output = template.render(&adr, &config).unwrap();
849
850 assert!(output.starts_with("---"));
852 assert!(output.contains("# MADR Minimal"));
853 assert!(output.contains("## Context and Problem Statement"));
854 assert!(output.contains("## Decision Outcome"));
855 assert!(!output.contains("## Decision Drivers"));
857 assert!(!output.contains("## Considered Options"));
858 }
859
860 #[test]
861 fn test_madr_bare_template() {
862 let template = Template::builtin_with_variant(TemplateFormat::Madr, TemplateVariant::Bare);
863 let adr = Adr::new(1, "MADR Bare");
864 let config = Config::default();
865 let output = template.render(&adr, &config).unwrap();
866
867 assert!(output.starts_with("---"));
869 assert!(output.contains("status:"));
870 assert!(output.contains("# MADR Bare"));
871 assert!(!output.contains("decision-makers"));
873 }
874
875 #[test]
876 fn test_builtin_defaults_to_full() {
877 let full = Template::builtin(TemplateFormat::Nygard);
878 let explicit_full =
879 Template::builtin_with_variant(TemplateFormat::Nygard, TemplateVariant::Full);
880
881 assert_eq!(full.name, explicit_full.name);
882 assert_eq!(full.content, explicit_full.content);
883 }
884
885 #[test]
888 fn test_template_engine_new() {
889 let engine = TemplateEngine::new();
890 assert_eq!(engine.default_format, TemplateFormat::Nygard);
891 assert_eq!(engine.default_variant, TemplateVariant::Full);
892 assert!(engine.custom_template.is_none());
893 }
894
895 #[test]
896 fn test_template_engine_default() {
897 let engine = TemplateEngine::default();
898 assert_eq!(engine.default_format, TemplateFormat::Nygard);
899 assert_eq!(engine.default_variant, TemplateVariant::Full);
900 }
901
902 #[test]
903 fn test_template_engine_with_format() {
904 let engine = TemplateEngine::new().with_format(TemplateFormat::Madr);
905 assert_eq!(engine.default_format, TemplateFormat::Madr);
906 }
907
908 #[test]
909 fn test_template_engine_with_custom_template() {
910 let custom = Template::from_string("custom", "# {{ title }}");
911 let engine = TemplateEngine::new().with_custom_template(custom);
912 assert!(engine.custom_template.is_some());
913 }
914
915 #[test]
916 fn test_template_engine_with_custom_template_file() {
917 let temp = TempDir::new().unwrap();
918 let path = temp.path().join("template.md");
919 std::fs::write(&path, "# {{ title }}").unwrap();
920
921 let engine = TemplateEngine::new()
922 .with_custom_template_file(&path)
923 .unwrap();
924 assert!(engine.custom_template.is_some());
925 }
926
927 #[test]
928 fn test_template_engine_with_custom_template_file_not_found() {
929 let result = TemplateEngine::new().with_custom_template_file(Path::new("/nonexistent.md"));
930 assert!(result.is_err());
931 }
932
933 #[test]
934 fn test_template_engine_template_builtin() {
935 let engine = TemplateEngine::new();
936 let template = engine.template();
937 assert_eq!(template.name, "nygard");
938 }
939
940 #[test]
941 fn test_template_engine_template_custom() {
942 let custom = Template::from_string("my-template", "# Custom");
943 let engine = TemplateEngine::new().with_custom_template(custom);
944 let template = engine.template();
945 assert_eq!(template.name, "my-template");
946 }
947
948 #[test]
949 fn test_template_engine_render() {
950 let engine = TemplateEngine::new();
951 let adr = Adr::new(1, "Test");
952 let config = Config::default();
953
954 let output = engine.render(&adr, &config).unwrap();
955 assert!(output.contains("# 1. Test"));
956 }
957
958 #[test]
959 fn test_template_engine_render_custom() {
960 let custom = Template::from_string("custom", "ADR {{ number }}: {{ title }}");
961 let engine = TemplateEngine::new().with_custom_template(custom);
962 let adr = Adr::new(42, "Custom ADR");
963 let config = Config::default();
964
965 let output = engine.render(&adr, &config).unwrap();
966 assert_eq!(output, "ADR 42: Custom ADR");
967 }
968
969 #[test]
972 fn test_custom_template_all_fields() {
973 let custom = Template::from_string(
974 "full",
975 r#"# {{ number }}. {{ title }}
976Date: {{ date }}
977Status: {{ status }}
978Context: {{ context }}
979Decision: {{ decision }}
980Consequences: {{ consequences }}
981Links: {% for link in links %}{{ link.kind }} {{ link.target }}{% endfor %}"#,
982 );
983
984 let mut adr = Adr::new(1, "Test");
985 adr.status = AdrStatus::Accepted;
986 adr.context = "Context text".into();
987 adr.decision = "Decision text".into();
988 adr.consequences = "Consequences text".into();
989 adr.links.push(AdrLink::new(2, LinkKind::Amends));
990
991 let config = Config::default();
992 let output = custom.render(&adr, &config).unwrap();
993
994 assert!(output.contains("# 1. Test"));
995 assert!(output.contains("Status: Accepted"));
996 assert!(output.contains("Context: Context text"));
997 assert!(output.contains("Decision: Decision text"));
998 assert!(output.contains("Consequences: Consequences text"));
999 assert!(output.contains("Amends 2"));
1000 }
1001
1002 #[test]
1003 fn test_custom_template_is_ng_flag() {
1004 let custom = Template::from_string(
1005 "ng-check",
1006 r#"{% if is_ng %}NextGen Mode{% else %}Compatible Mode{% endif %}"#,
1007 );
1008
1009 let adr = Adr::new(1, "Test");
1010
1011 let compat_config = Config::default();
1012 let output = custom.render(&adr, &compat_config).unwrap();
1013 assert_eq!(output, "Compatible Mode");
1014
1015 let ng_config = Config {
1016 mode: ConfigMode::NextGen,
1017 ..Default::default()
1018 };
1019 let output = custom.render(&adr, &ng_config).unwrap();
1020 assert_eq!(output, "NextGen Mode");
1021 }
1022
1023 #[test]
1024 fn test_custom_template_link_kinds() {
1025 let custom = Template::from_string(
1026 "links",
1027 r#"{% for link in links %}{{ link.kind }}|{% endfor %}"#,
1028 );
1029
1030 let mut adr = Adr::new(1, "Test");
1031 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1032 adr.links.push(AdrLink::new(2, LinkKind::SupersededBy));
1033 adr.links.push(AdrLink::new(3, LinkKind::Amends));
1034 adr.links.push(AdrLink::new(4, LinkKind::AmendedBy));
1035 adr.links.push(AdrLink::new(5, LinkKind::RelatesTo));
1036 adr.links
1037 .push(AdrLink::new(6, LinkKind::Custom("Depends on".into())));
1038
1039 let config = Config::default();
1040 let output = custom.render(&adr, &config).unwrap();
1041
1042 assert!(output.contains("Supersedes|"));
1043 assert!(output.contains("Superseded by|"));
1044 assert!(output.contains("Amends|"));
1045 assert!(output.contains("Amended by|"));
1046 assert!(output.contains("Relates to|"));
1047 assert!(output.contains("Depends on|"));
1048 }
1049
1050 #[test]
1053 fn test_template_invalid_syntax() {
1054 let custom = Template::from_string("invalid", "{{ unclosed");
1055 let adr = Adr::new(1, "Test");
1056 let config = Config::default();
1057
1058 let result = custom.render(&adr, &config);
1059 assert!(result.is_err());
1060 }
1061
1062 #[test]
1063 fn test_template_undefined_variable() {
1064 let custom = Template::from_string("undefined", "{{ nonexistent }}");
1065 let adr = Adr::new(1, "Test");
1066 let config = Config::default();
1067
1068 let result = custom.render(&adr, &config);
1070 assert!(result.is_ok());
1071 }
1072
1073 #[test]
1076 fn test_render_four_digit_number() {
1077 let template = Template::builtin(TemplateFormat::Nygard);
1078 let adr = Adr::new(9999, "Large Number");
1079 let config = Config::default();
1080
1081 let output = template.render(&adr, &config).unwrap();
1082 assert!(output.contains("# 9999. Large Number"));
1083 }
1084
1085 #[test]
1086 fn test_render_link_number_formatting() {
1087 let template = Template::builtin(TemplateFormat::Nygard);
1088 let mut adr = Adr::new(2, "Test");
1089 adr.links.push(AdrLink::new(1, LinkKind::Supersedes));
1090
1091 let config = Config::default();
1092 let output = template.render(&adr, &config).unwrap();
1093
1094 assert!(output.contains("0001-"));
1096 }
1097}