1use crate::{
4 Adr, AdrLink, AdrStatus, Config, ConfigMode, Error, LinkKind, Parser, Result, Template,
5 TemplateEngine, TemplateFormat, TemplateVariant,
6};
7use fuzzy_matcher::FuzzyMatcher;
8use fuzzy_matcher::skim::SkimMatcherV2;
9use std::fs;
10use std::path::{Path, PathBuf};
11use walkdir::WalkDir;
12
13#[derive(Debug)]
15pub struct Repository {
16 root: PathBuf,
18
19 config: Config,
21
22 parser: Parser,
24
25 template_engine: TemplateEngine,
27}
28
29impl Repository {
30 pub fn open(root: impl Into<PathBuf>) -> Result<Self> {
32 let root = root.into();
33 let config = Config::load(&root)?;
34
35 Ok(Self {
36 root,
37 config,
38 parser: Parser::new(),
39 template_engine: TemplateEngine::new(),
40 })
41 }
42
43 pub fn open_or_default(root: impl Into<PathBuf>) -> Self {
45 let root = root.into();
46 let config = Config::load_or_default(&root);
47
48 Self {
49 root,
50 config,
51 parser: Parser::new(),
52 template_engine: TemplateEngine::new(),
53 }
54 }
55
56 pub fn init(root: impl Into<PathBuf>, adr_dir: Option<PathBuf>, ng: bool) -> Result<Self> {
58 let root = root.into();
59 let adr_dir = adr_dir.unwrap_or_else(|| PathBuf::from(crate::config::DEFAULT_ADR_DIR));
60 let adr_path = root.join(&adr_dir);
61
62 if adr_path.exists() {
64 return Err(Error::AdrDirExists(adr_path));
65 }
66
67 fs::create_dir_all(&adr_path)?;
69
70 let config = Config {
72 adr_dir,
73 mode: if ng {
74 ConfigMode::NextGen
75 } else {
76 ConfigMode::Compatible
77 },
78 ..Default::default()
79 };
80 config.save(&root)?;
81
82 let repo = Self {
83 root,
84 config,
85 parser: Parser::new(),
86 template_engine: TemplateEngine::new(),
87 };
88
89 let mut adr = Adr::new(1, "Record architecture decisions");
91 adr.status = AdrStatus::Accepted;
92 adr.context = "We need to record the architectural decisions made on this project.".into();
93 adr.decision = "We will use Architecture Decision Records, as described by Michael Nygard in his article \"Documenting Architecture Decisions\".".into();
94 adr.consequences = "See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's adr-tools.".into();
95 repo.create(&adr)?;
96
97 Ok(repo)
98 }
99
100 pub fn root(&self) -> &Path {
102 &self.root
103 }
104
105 pub fn config(&self) -> &Config {
107 &self.config
108 }
109
110 pub fn adr_path(&self) -> PathBuf {
112 self.config.adr_path(&self.root)
113 }
114
115 pub fn with_template_format(mut self, format: TemplateFormat) -> Self {
117 self.template_engine = self.template_engine.with_format(format);
118 self
119 }
120
121 pub fn with_template_variant(mut self, variant: TemplateVariant) -> Self {
123 self.template_engine = self.template_engine.with_variant(variant);
124 self
125 }
126
127 pub fn with_custom_template(mut self, template: Template) -> Self {
129 self.template_engine = self.template_engine.with_custom_template(template);
130 self
131 }
132
133 pub fn list(&self) -> Result<Vec<Adr>> {
135 let adr_path = self.adr_path();
136 if !adr_path.exists() {
137 return Err(Error::AdrDirNotFound);
138 }
139
140 let mut adrs: Vec<Adr> = WalkDir::new(&adr_path)
141 .max_depth(1)
142 .into_iter()
143 .filter_map(|e| e.ok())
144 .filter(|e| {
145 e.path().extension().is_some_and(|ext| ext == "md")
146 && e.path()
147 .file_name()
148 .and_then(|n| n.to_str())
149 .is_some_and(|n| n.chars().next().is_some_and(|c| c.is_ascii_digit()))
150 })
151 .filter_map(|e| self.parser.parse_file(e.path()).ok())
152 .collect();
153
154 adrs.sort_by_key(|a| a.number);
155 Ok(adrs)
156 }
157
158 pub fn next_number(&self) -> Result<u32> {
160 let adrs = self.list()?;
161 Ok(adrs.last().map(|a| a.number + 1).unwrap_or(1))
162 }
163
164 pub fn get(&self, number: u32) -> Result<Adr> {
166 let adrs = self.list()?;
167 adrs.into_iter()
168 .find(|a| a.number == number)
169 .ok_or_else(|| Error::AdrNotFound(number.to_string()))
170 }
171
172 pub fn find(&self, query: &str) -> Result<Adr> {
174 if let Ok(number) = query.parse::<u32>() {
176 return self.get(number);
177 }
178
179 let adrs = self.list()?;
181 let matcher = SkimMatcherV2::default();
182
183 let mut matches: Vec<_> = adrs
184 .into_iter()
185 .filter_map(|adr| {
186 let score = matcher.fuzzy_match(&adr.title, query)?;
187 Some((adr, score))
188 })
189 .collect();
190
191 matches.sort_by(|a, b| b.1.cmp(&a.1));
192
193 match matches.len() {
194 0 => Err(Error::AdrNotFound(query.to_string())),
195 1 => Ok(matches.remove(0).0),
196 _ => {
197 if matches[0].1 > matches[1].1 * 2 {
199 Ok(matches.remove(0).0)
200 } else {
201 Err(Error::AmbiguousAdr {
202 query: query.to_string(),
203 matches: matches
204 .iter()
205 .take(5)
206 .map(|(a, _)| a.title.clone())
207 .collect(),
208 })
209 }
210 }
211 }
212 }
213
214 pub fn create(&self, adr: &Adr) -> Result<PathBuf> {
216 let path = self.adr_path().join(adr.filename());
217
218 let content = self.template_engine.render(adr, &self.config)?;
219 fs::write(&path, content)?;
220
221 Ok(path)
222 }
223
224 pub fn new_adr(&self, title: impl Into<String>) -> Result<(Adr, PathBuf)> {
226 let number = self.next_number()?;
227 let adr = Adr::new(number, title);
228 let path = self.create(&adr)?;
229 Ok((adr, path))
230 }
231
232 pub fn supersede(&self, title: impl Into<String>, superseded: u32) -> Result<(Adr, PathBuf)> {
234 let number = self.next_number()?;
235 let mut adr = Adr::new(number, title);
236 adr.add_link(AdrLink::new(superseded, LinkKind::Supersedes));
237
238 let mut old_adr = self.get(superseded)?;
240 old_adr.status = AdrStatus::Superseded;
241 old_adr.add_link(AdrLink::new(number, LinkKind::SupersededBy));
242 self.update(&old_adr)?;
243
244 let path = self.create(&adr)?;
245 Ok((adr, path))
246 }
247
248 pub fn set_status(
253 &self,
254 number: u32,
255 status: AdrStatus,
256 superseded_by: Option<u32>,
257 ) -> Result<PathBuf> {
258 let mut adr = self.get(number)?;
259 adr.status = status.clone();
260
261 if let (AdrStatus::Superseded, Some(by)) = (&status, superseded_by) {
263 let _ = self.get(by)?;
265
266 if !adr
268 .links
269 .iter()
270 .any(|l| matches!(l.kind, LinkKind::SupersededBy) && l.target == by)
271 {
272 adr.add_link(AdrLink::new(by, LinkKind::SupersededBy));
273 }
274 }
275
276 self.update(&adr)
277 }
278
279 pub fn link(
281 &self,
282 source: u32,
283 target: u32,
284 source_kind: LinkKind,
285 target_kind: LinkKind,
286 ) -> Result<()> {
287 let mut source_adr = self.get(source)?;
288 let mut target_adr = self.get(target)?;
289
290 source_adr.add_link(AdrLink::new(target, source_kind));
291 target_adr.add_link(AdrLink::new(source, target_kind));
292
293 self.update(&source_adr)?;
294 self.update(&target_adr)?;
295
296 Ok(())
297 }
298
299 pub fn update(&self, adr: &Adr) -> Result<PathBuf> {
301 let path = adr
302 .path
303 .clone()
304 .unwrap_or_else(|| self.adr_path().join(adr.filename()));
305
306 let content = self.template_engine.render(adr, &self.config)?;
307 fs::write(&path, content)?;
308
309 Ok(path)
310 }
311
312 pub fn read_content(&self, adr: &Adr) -> Result<String> {
314 let path = adr
315 .path
316 .as_ref()
317 .cloned()
318 .unwrap_or_else(|| self.adr_path().join(adr.filename()));
319
320 Ok(fs::read_to_string(path)?)
321 }
322
323 pub fn write_content(&self, adr: &Adr, content: &str) -> Result<PathBuf> {
325 let path = adr
326 .path
327 .as_ref()
328 .cloned()
329 .unwrap_or_else(|| self.adr_path().join(adr.filename()));
330
331 fs::write(&path, content)?;
332 Ok(path)
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use tempfile::TempDir;
340
341 #[test]
344 fn test_init_repository() {
345 let temp = TempDir::new().unwrap();
346 let repo = Repository::init(temp.path(), None, false).unwrap();
347
348 assert!(repo.adr_path().exists());
349 assert!(temp.path().join(".adr-dir").exists());
350
351 let adrs = repo.list().unwrap();
352 assert_eq!(adrs.len(), 1);
353 assert_eq!(adrs[0].number, 1);
354 assert_eq!(adrs[0].title, "Record architecture decisions");
355 }
356
357 #[test]
358 fn test_init_repository_ng() {
359 let temp = TempDir::new().unwrap();
360 let repo = Repository::init(temp.path(), None, true).unwrap();
361
362 assert!(temp.path().join("adrs.toml").exists());
363 assert!(repo.config().is_next_gen());
364 }
365
366 #[test]
367 fn test_init_repository_custom_dir() {
368 let temp = TempDir::new().unwrap();
369 let repo = Repository::init(temp.path(), Some("decisions".into()), false).unwrap();
370
371 assert!(temp.path().join("decisions").exists());
372 assert_eq!(repo.config().adr_dir, PathBuf::from("decisions"));
373 }
374
375 #[test]
376 fn test_init_repository_nested_dir() {
377 let temp = TempDir::new().unwrap();
378 let _repo =
379 Repository::init(temp.path(), Some("docs/architecture/adr".into()), false).unwrap();
380
381 assert!(temp.path().join("docs/architecture/adr").exists());
382 }
383
384 #[test]
385 fn test_init_repository_already_exists() {
386 let temp = TempDir::new().unwrap();
387 Repository::init(temp.path(), None, false).unwrap();
388
389 let result = Repository::init(temp.path(), None, false);
390 assert!(result.is_err());
391 }
392
393 #[test]
394 fn test_init_creates_first_adr() {
395 let temp = TempDir::new().unwrap();
396 let repo = Repository::init(temp.path(), None, false).unwrap();
397
398 let adr = repo.get(1).unwrap();
399 assert_eq!(adr.title, "Record architecture decisions");
400 assert_eq!(adr.status, AdrStatus::Accepted);
401 assert!(!adr.context.is_empty());
402 assert!(!adr.decision.is_empty());
403 assert!(!adr.consequences.is_empty());
404 }
405
406 #[test]
409 fn test_open_repository() {
410 let temp = TempDir::new().unwrap();
411 Repository::init(temp.path(), None, false).unwrap();
412
413 let repo = Repository::open(temp.path()).unwrap();
414 assert_eq!(repo.list().unwrap().len(), 1);
415 }
416
417 #[test]
418 fn test_open_repository_not_found() {
419 let temp = TempDir::new().unwrap();
420 let result = Repository::open(temp.path());
421 assert!(result.is_err());
422 }
423
424 #[test]
425 fn test_open_or_default() {
426 let temp = TempDir::new().unwrap();
427 let repo = Repository::open_or_default(temp.path());
428 assert_eq!(repo.config().adr_dir, PathBuf::from("doc/adr"));
429 }
430
431 #[test]
432 fn test_open_or_default_existing() {
433 let temp = TempDir::new().unwrap();
434 Repository::init(temp.path(), Some("custom".into()), false).unwrap();
435
436 let repo = Repository::open_or_default(temp.path());
437 assert_eq!(repo.config().adr_dir, PathBuf::from("custom"));
438 }
439
440 #[test]
443 fn test_create_and_list() {
444 let temp = TempDir::new().unwrap();
445 let repo = Repository::init(temp.path(), None, false).unwrap();
446
447 let (adr, _) = repo.new_adr("Use Rust").unwrap();
448 assert_eq!(adr.number, 2);
449
450 let adrs = repo.list().unwrap();
451 assert_eq!(adrs.len(), 2);
452 }
453
454 #[test]
455 fn test_create_multiple() {
456 let temp = TempDir::new().unwrap();
457 let repo = Repository::init(temp.path(), None, false).unwrap();
458
459 repo.new_adr("Second").unwrap();
460 repo.new_adr("Third").unwrap();
461 repo.new_adr("Fourth").unwrap();
462
463 let adrs = repo.list().unwrap();
464 assert_eq!(adrs.len(), 4);
465 assert_eq!(adrs[0].number, 1);
466 assert_eq!(adrs[1].number, 2);
467 assert_eq!(adrs[2].number, 3);
468 assert_eq!(adrs[3].number, 4);
469 }
470
471 #[test]
472 fn test_list_sorted_by_number() {
473 let temp = TempDir::new().unwrap();
474 let repo = Repository::init(temp.path(), None, false).unwrap();
475
476 repo.new_adr("B").unwrap();
477 repo.new_adr("A").unwrap();
478 repo.new_adr("C").unwrap();
479
480 let adrs = repo.list().unwrap();
481 assert!(adrs.windows(2).all(|w| w[0].number < w[1].number));
482 }
483
484 #[test]
485 fn test_next_number() {
486 let temp = TempDir::new().unwrap();
487 let repo = Repository::init(temp.path(), None, false).unwrap();
488
489 assert_eq!(repo.next_number().unwrap(), 2);
490
491 repo.new_adr("Second").unwrap();
492 assert_eq!(repo.next_number().unwrap(), 3);
493 }
494
495 #[test]
496 fn test_create_file_exists() {
497 let temp = TempDir::new().unwrap();
498 let repo = Repository::init(temp.path(), None, false).unwrap();
499
500 let (_, path) = repo.new_adr("Test ADR").unwrap();
501 assert!(path.exists());
502 assert!(path.to_string_lossy().contains("0002-test-adr.md"));
503 }
504
505 #[test]
508 fn test_get_by_number() {
509 let temp = TempDir::new().unwrap();
510 let repo = Repository::init(temp.path(), None, false).unwrap();
511 repo.new_adr("Second").unwrap();
512
513 let adr = repo.get(2).unwrap();
514 assert_eq!(adr.title, "Second");
515 }
516
517 #[test]
518 fn test_get_not_found() {
519 let temp = TempDir::new().unwrap();
520 let repo = Repository::init(temp.path(), None, false).unwrap();
521
522 let result = repo.get(99);
523 assert!(result.is_err());
524 }
525
526 #[test]
527 fn test_find_by_number() {
528 let temp = TempDir::new().unwrap();
529 let repo = Repository::init(temp.path(), None, false).unwrap();
530
531 let adr = repo.find("1").unwrap();
532 assert_eq!(adr.number, 1);
533 }
534
535 #[test]
536 fn test_find_by_title() {
537 let temp = TempDir::new().unwrap();
538 let repo = Repository::init(temp.path(), None, false).unwrap();
539
540 let adr = repo.find("architecture").unwrap();
541 assert_eq!(adr.number, 1);
542 }
543
544 #[test]
545 fn test_find_fuzzy_match() {
546 let temp = TempDir::new().unwrap();
547 let repo = Repository::init(temp.path(), None, false).unwrap();
548 repo.new_adr("Use PostgreSQL for database").unwrap();
549 repo.new_adr("Use Redis for caching").unwrap();
550
551 let adr = repo.find("postgres").unwrap();
552 assert!(adr.title.contains("PostgreSQL"));
553 }
554
555 #[test]
556 fn test_find_not_found() {
557 let temp = TempDir::new().unwrap();
558 let repo = Repository::init(temp.path(), None, false).unwrap();
559
560 let result = repo.find("nonexistent");
561 assert!(result.is_err());
562 }
563
564 #[test]
567 fn test_supersede() {
568 let temp = TempDir::new().unwrap();
569 let repo = Repository::init(temp.path(), None, false).unwrap();
570
571 let (new_adr, _) = repo.supersede("New approach", 1).unwrap();
572 assert_eq!(new_adr.number, 2);
573 assert_eq!(new_adr.links.len(), 1);
574 assert_eq!(new_adr.links[0].kind, LinkKind::Supersedes);
575
576 let old_adr = repo.get(1).unwrap();
577 assert_eq!(old_adr.status, AdrStatus::Superseded);
578 }
579
580 #[test]
581 fn test_supersede_creates_bidirectional_links() {
582 let temp = TempDir::new().unwrap();
583 let repo = Repository::init(temp.path(), None, false).unwrap();
584
585 repo.supersede("New approach", 1).unwrap();
586
587 let old_adr = repo.get(1).unwrap();
588 assert_eq!(old_adr.links.len(), 1);
589 assert_eq!(old_adr.links[0].target, 2);
590 assert_eq!(old_adr.links[0].kind, LinkKind::SupersededBy);
591
592 let new_adr = repo.get(2).unwrap();
593 assert_eq!(new_adr.links.len(), 1);
594 assert_eq!(new_adr.links[0].target, 1);
595 assert_eq!(new_adr.links[0].kind, LinkKind::Supersedes);
596 }
597
598 #[test]
599 fn test_supersede_not_found() {
600 let temp = TempDir::new().unwrap();
601 let repo = Repository::init(temp.path(), None, false).unwrap();
602
603 let result = repo.supersede("New", 99);
604 assert!(result.is_err());
605 }
606
607 #[test]
610 fn test_set_status_accepted() {
611 let temp = TempDir::new().unwrap();
612 let repo = Repository::init(temp.path(), None, false).unwrap();
613 repo.new_adr("Test Decision").unwrap();
614
615 repo.set_status(2, AdrStatus::Accepted, None).unwrap();
616
617 let adr = repo.get(2).unwrap();
618 assert_eq!(adr.status, AdrStatus::Accepted);
619 }
620
621 #[test]
622 fn test_set_status_deprecated() {
623 let temp = TempDir::new().unwrap();
624 let repo = Repository::init(temp.path(), None, false).unwrap();
625 repo.new_adr("Old Decision").unwrap();
626
627 repo.set_status(2, AdrStatus::Deprecated, None).unwrap();
628
629 let adr = repo.get(2).unwrap();
630 assert_eq!(adr.status, AdrStatus::Deprecated);
631 }
632
633 #[test]
634 fn test_set_status_superseded_with_link() {
635 let temp = TempDir::new().unwrap();
636 let repo = Repository::init(temp.path(), None, false).unwrap();
637 repo.new_adr("First Decision").unwrap();
638 repo.new_adr("Second Decision").unwrap();
639
640 repo.set_status(2, AdrStatus::Superseded, Some(3)).unwrap();
641
642 let adr = repo.get(2).unwrap();
643 assert_eq!(adr.status, AdrStatus::Superseded);
644 assert_eq!(adr.links.len(), 1);
645 assert_eq!(adr.links[0].target, 3);
646 assert_eq!(adr.links[0].kind, LinkKind::SupersededBy);
647 }
648
649 #[test]
650 fn test_set_status_superseded_without_link() {
651 let temp = TempDir::new().unwrap();
652 let repo = Repository::init(temp.path(), None, false).unwrap();
653 repo.new_adr("Decision").unwrap();
654
655 repo.set_status(2, AdrStatus::Superseded, None).unwrap();
656
657 let adr = repo.get(2).unwrap();
658 assert_eq!(adr.status, AdrStatus::Superseded);
659 assert_eq!(adr.links.len(), 0);
660 }
661
662 #[test]
663 fn test_set_status_custom() {
664 let temp = TempDir::new().unwrap();
665 let repo = Repository::init(temp.path(), None, false).unwrap();
666 repo.new_adr("Test Decision").unwrap();
667
668 repo.set_status(2, AdrStatus::Custom("Draft".into()), None)
669 .unwrap();
670
671 let adr = repo.get(2).unwrap();
672 assert_eq!(adr.status, AdrStatus::Custom("Draft".into()));
673 }
674
675 #[test]
676 fn test_set_status_adr_not_found() {
677 let temp = TempDir::new().unwrap();
678 let repo = Repository::init(temp.path(), None, false).unwrap();
679
680 let result = repo.set_status(99, AdrStatus::Accepted, None);
681 assert!(result.is_err());
682 }
683
684 #[test]
685 fn test_set_status_superseded_by_not_found() {
686 let temp = TempDir::new().unwrap();
687 let repo = Repository::init(temp.path(), None, false).unwrap();
688 repo.new_adr("Decision").unwrap();
689
690 let result = repo.set_status(2, AdrStatus::Superseded, Some(99));
691 assert!(result.is_err());
692 }
693
694 #[test]
697 fn test_link_adrs() {
698 let temp = TempDir::new().unwrap();
699 let repo = Repository::init(temp.path(), None, false).unwrap();
700 repo.new_adr("Second").unwrap();
701
702 repo.link(1, 2, LinkKind::Amends, LinkKind::AmendedBy)
703 .unwrap();
704
705 let adr1 = repo.get(1).unwrap();
706 assert_eq!(adr1.links.len(), 1);
707 assert_eq!(adr1.links[0].target, 2);
708 assert_eq!(adr1.links[0].kind, LinkKind::Amends);
709
710 let adr2 = repo.get(2).unwrap();
711 assert_eq!(adr2.links.len(), 1);
712 assert_eq!(adr2.links[0].target, 1);
713 assert_eq!(adr2.links[0].kind, LinkKind::AmendedBy);
714 }
715
716 #[test]
717 fn test_link_relates_to() {
718 let temp = TempDir::new().unwrap();
719 let repo = Repository::init(temp.path(), None, false).unwrap();
720 repo.new_adr("Second").unwrap();
721
722 repo.link(1, 2, LinkKind::RelatesTo, LinkKind::RelatesTo)
723 .unwrap();
724
725 let adr1 = repo.get(1).unwrap();
726 assert_eq!(adr1.links[0].kind, LinkKind::RelatesTo);
727
728 let adr2 = repo.get(2).unwrap();
729 assert_eq!(adr2.links[0].kind, LinkKind::RelatesTo);
730 }
731
732 #[test]
735 fn test_update_adr() {
736 let temp = TempDir::new().unwrap();
737 let repo = Repository::init(temp.path(), None, false).unwrap();
738
739 let mut adr = repo.get(1).unwrap();
740 adr.status = AdrStatus::Deprecated;
741
742 repo.update(&adr).unwrap();
743
744 let updated = repo.get(1).unwrap();
745 assert_eq!(updated.status, AdrStatus::Deprecated);
746 }
747
748 #[test]
749 fn test_update_preserves_content() {
750 let temp = TempDir::new().unwrap();
751 let repo = Repository::init(temp.path(), None, false).unwrap();
752
753 let mut adr = repo.get(1).unwrap();
754 let original_title = adr.title.clone();
755 adr.status = AdrStatus::Deprecated;
756
757 repo.update(&adr).unwrap();
758
759 let updated = repo.get(1).unwrap();
760 assert_eq!(updated.title, original_title);
761 }
762
763 #[test]
766 fn test_read_content() {
767 let temp = TempDir::new().unwrap();
768 let repo = Repository::init(temp.path(), None, false).unwrap();
769
770 let adr = repo.get(1).unwrap();
771 let content = repo.read_content(&adr).unwrap();
772
773 assert!(content.contains("Record architecture decisions"));
774 assert!(content.contains("## Status"));
775 }
776
777 #[test]
778 fn test_write_content() {
779 let temp = TempDir::new().unwrap();
780 let repo = Repository::init(temp.path(), None, false).unwrap();
781
782 let adr = repo.get(1).unwrap();
783 let new_content = "# 1. Modified\n\n## Status\n\nAccepted\n";
784
785 repo.write_content(&adr, new_content).unwrap();
786
787 let content = repo.read_content(&adr).unwrap();
788 assert!(content.contains("Modified"));
789 }
790
791 #[test]
794 fn test_with_template_format() {
795 let temp = TempDir::new().unwrap();
796 let repo = Repository::init(temp.path(), None, false)
797 .unwrap()
798 .with_template_format(TemplateFormat::Madr);
799
800 let (_, path) = repo.new_adr("MADR Test").unwrap();
801 let content = fs::read_to_string(path).unwrap();
802
803 assert!(content.contains("Context and Problem Statement"));
804 }
805
806 #[test]
807 fn test_with_custom_template() {
808 let temp = TempDir::new().unwrap();
809 let custom = Template::from_string("custom", "# ADR {{ number }}: {{ title }}");
810 let repo = Repository::init(temp.path(), None, false)
811 .unwrap()
812 .with_custom_template(custom);
813
814 let (_, path) = repo.new_adr("Custom Test").unwrap();
815 let content = fs::read_to_string(path).unwrap();
816
817 assert_eq!(content, "# ADR 2: Custom Test");
818 }
819
820 #[test]
823 fn test_root() {
824 let temp = TempDir::new().unwrap();
825 let repo = Repository::init(temp.path(), None, false).unwrap();
826
827 assert_eq!(repo.root(), temp.path());
828 }
829
830 #[test]
831 fn test_config() {
832 let temp = TempDir::new().unwrap();
833 let repo = Repository::init(temp.path(), Some("custom".into()), true).unwrap();
834
835 assert_eq!(repo.config().adr_dir, PathBuf::from("custom"));
836 assert!(repo.config().is_next_gen());
837 }
838
839 #[test]
840 fn test_adr_path() {
841 let temp = TempDir::new().unwrap();
842 let repo = Repository::init(temp.path(), Some("my/adrs".into()), false).unwrap();
843
844 assert_eq!(repo.adr_path(), temp.path().join("my/adrs"));
845 }
846
847 #[test]
850 fn test_ng_mode_creates_frontmatter() {
851 let temp = TempDir::new().unwrap();
852 let repo = Repository::init(temp.path(), None, true).unwrap();
853
854 let (_, path) = repo.new_adr("NG Test").unwrap();
855 let content = fs::read_to_string(path).unwrap();
856
857 assert!(content.starts_with("---"));
858 assert!(content.contains("number: 2"));
859 assert!(content.contains("title: NG Test"));
860 }
861
862 #[test]
863 fn test_ng_mode_parses_frontmatter() {
864 let temp = TempDir::new().unwrap();
865 let repo = Repository::init(temp.path(), None, true).unwrap();
866
867 repo.new_adr("NG ADR").unwrap();
868
869 let adr = repo.get(2).unwrap();
870 assert_eq!(adr.title, "NG ADR");
871 assert_eq!(adr.number, 2);
872 }
873
874 #[test]
877 fn test_list_empty_after_init_removal() {
878 let temp = TempDir::new().unwrap();
879 let repo = Repository::init(temp.path(), None, false).unwrap();
880
881 fs::remove_file(
883 repo.adr_path()
884 .join("0001-record-architecture-decisions.md"),
885 )
886 .unwrap();
887
888 let adrs = repo.list().unwrap();
889 assert!(adrs.is_empty());
890 }
891
892 #[test]
893 fn test_list_ignores_non_adr_files() {
894 let temp = TempDir::new().unwrap();
895 let repo = Repository::init(temp.path(), None, false).unwrap();
896
897 fs::write(repo.adr_path().join("README.md"), "# README").unwrap();
899 fs::write(repo.adr_path().join("notes.txt"), "Notes").unwrap();
900
901 let adrs = repo.list().unwrap();
902 assert_eq!(adrs.len(), 1); }
904
905 #[test]
906 fn test_special_characters_in_title() {
907 let temp = TempDir::new().unwrap();
908 let repo = Repository::init(temp.path(), None, false).unwrap();
909
910 let (adr, path) = repo.new_adr("Use C++ & Rust!").unwrap();
911 assert!(path.exists());
912 assert_eq!(adr.title, "Use C++ & Rust!");
913 }
914}