adrscope/infrastructure/parser/
mod.rs

1//! ADR parsing infrastructure.
2//!
3//! This module provides parsers for extracting frontmatter and converting
4//! markdown to HTML.
5
6mod frontmatter;
7mod markdown;
8
9use std::path::Path;
10
11use crate::domain::{Adr, AdrId};
12use crate::error::Result;
13
14pub use frontmatter::FrontmatterParser;
15pub use markdown::MarkdownRenderer;
16
17/// Trait for parsing ADR files.
18pub trait AdrParser: Send + Sync {
19    /// Parses an ADR from file contents.
20    fn parse(&self, path: &Path, content: &str) -> Result<Adr>;
21}
22
23/// Default ADR parser implementation.
24#[derive(Debug, Clone, Default)]
25pub struct DefaultAdrParser {
26    frontmatter_parser: FrontmatterParser,
27    markdown_renderer: MarkdownRenderer,
28}
29
30impl DefaultAdrParser {
31    /// Creates a new default ADR parser.
32    #[must_use]
33    pub fn new() -> Self {
34        Self::default()
35    }
36}
37
38impl AdrParser for DefaultAdrParser {
39    fn parse(&self, path: &Path, content: &str) -> Result<Adr> {
40        // Extract ID from filename
41        let id = AdrId::from_path(path);
42
43        // Extract filename
44        let filename = path
45            .file_name()
46            .and_then(|n| n.to_str())
47            .unwrap_or("unknown.md")
48            .to_string();
49
50        // Parse frontmatter and get body
51        let (frontmatter, body_markdown) = self.frontmatter_parser.parse(path, content)?;
52
53        // Render markdown to HTML
54        let body_html = self.markdown_renderer.render(body_markdown);
55
56        // Extract plain text for search indexing
57        let body_text = self.markdown_renderer.render_plain_text(body_markdown);
58
59        Ok(Adr::new(
60            id,
61            filename,
62            path.to_path_buf(),
63            frontmatter,
64            body_markdown.to_string(),
65            body_html,
66            body_text,
67        ))
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::domain::Status;
75    use std::path::PathBuf;
76
77    #[test]
78    fn test_parse_full_adr() {
79        let content = r#"---
80title: Use PostgreSQL for Primary Storage
81description: Decision to adopt PostgreSQL as our primary database
82status: accepted
83category: architecture
84tags:
85  - database
86  - postgresql
87author: Architecture Team
88created: "2025-01-15"
89---
90
91# Context
92
93We need a reliable primary database for our application.
94
95## Decision
96
97We will use PostgreSQL.
98
99## Consequences
100
101PostgreSQL provides the features we need.
102"#;
103
104        let parser = DefaultAdrParser::new();
105        let path = PathBuf::from("adr_0001.md");
106        let adr = parser.parse(&path, content).expect("should parse");
107
108        assert_eq!(adr.id().as_str(), "adr_0001");
109        assert_eq!(adr.title(), "Use PostgreSQL for Primary Storage");
110        assert_eq!(adr.status(), Status::Accepted);
111        assert_eq!(adr.category(), "architecture");
112        assert!(adr.body_html().contains("<h1>"));
113        assert!(adr.body_text().contains("Context"));
114    }
115
116    #[test]
117    fn test_parse_minimal_adr() {
118        let content = r"---
119title: Minimal ADR
120---
121
122Simple content.
123";
124
125        let parser = DefaultAdrParser::new();
126        let path = PathBuf::from("minimal.md");
127        let adr = parser.parse(&path, content).expect("should parse");
128
129        assert_eq!(adr.title(), "Minimal ADR");
130        assert_eq!(adr.status(), Status::Proposed); // default
131    }
132}