adrscope/application/
wiki.rs1use std::path::Path;
6
7use crate::domain::Adr;
8use crate::error::Result;
9use crate::infrastructure::renderer::WikiRenderer;
10use crate::infrastructure::{AdrParser, DefaultAdrParser, FileSystem};
11
12#[derive(Debug, Clone)]
14pub struct WikiOptions {
15 pub input_dir: String,
17 pub output_dir: String,
19 pub pages_url: Option<String>,
21 pub pattern: String,
23}
24
25impl Default for WikiOptions {
26 fn default() -> Self {
27 Self {
28 input_dir: "docs/decisions".to_string(),
29 output_dir: "wiki".to_string(),
30 pages_url: None,
31 pattern: "**/*.md".to_string(),
32 }
33 }
34}
35
36impl WikiOptions {
37 #[must_use]
39 pub fn new(input_dir: impl Into<String>) -> Self {
40 Self {
41 input_dir: input_dir.into(),
42 ..Default::default()
43 }
44 }
45
46 #[must_use]
48 pub fn with_output_dir(mut self, output_dir: impl Into<String>) -> Self {
49 self.output_dir = output_dir.into();
50 self
51 }
52
53 #[must_use]
55 pub fn with_pages_url(mut self, url: impl Into<String>) -> Self {
56 self.pages_url = Some(url.into());
57 self
58 }
59
60 #[must_use]
62 pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
63 self.pattern = pattern.into();
64 self
65 }
66}
67
68#[derive(Debug)]
70pub struct WikiUseCase<F: FileSystem> {
71 fs: F,
72 parser: DefaultAdrParser,
73 renderer: WikiRenderer,
74}
75
76impl<F: FileSystem> WikiUseCase<F> {
77 #[must_use]
79 pub fn new(fs: F) -> Self {
80 Self {
81 fs,
82 parser: DefaultAdrParser::new(),
83 renderer: WikiRenderer::new(),
84 }
85 }
86
87 pub fn execute(&self, options: &WikiOptions) -> Result<WikiResult> {
97 let base = Path::new(&options.input_dir);
99 let files = self.fs.glob(base, &options.pattern)?;
100
101 if files.is_empty() {
102 return Err(crate::error::Error::NoAdrsFound {
103 path: base.to_path_buf(),
104 });
105 }
106
107 let mut adrs = Vec::with_capacity(files.len());
109 let mut errors = Vec::new();
110
111 for file_path in &files {
112 match self.parse_adr(file_path) {
113 Ok(adr) => adrs.push(adr),
114 Err(e) => errors.push((file_path.clone(), e)),
115 }
116 }
117
118 adrs.sort_by(|a, b| a.id().cmp(b.id()));
120
121 let pages = self
123 .renderer
124 .render_all(&adrs, options.pages_url.as_deref())?;
125
126 self.fs.create_dir_all(Path::new(&options.output_dir))?;
128
129 let mut generated_files = Vec::with_capacity(pages.len());
131 for (filename, content) in pages {
132 let output_path = format!("{}/{}", options.output_dir, filename);
133 self.fs.write(Path::new(&output_path), &content)?;
134 generated_files.push(output_path);
135 }
136
137 for adr in &adrs {
139 let dest_path = format!("{}/{}", options.output_dir, adr.filename());
140 let content = self.fs.read_to_string(adr.source_path())?;
141 self.fs.write(Path::new(&dest_path), &content)?;
142 generated_files.push(dest_path);
143 }
144
145 Ok(WikiResult {
146 output_dir: options.output_dir.clone(),
147 generated_files,
148 adr_count: adrs.len(),
149 parse_errors: errors,
150 })
151 }
152
153 fn parse_adr(&self, path: &Path) -> Result<Adr> {
154 let content = self.fs.read_to_string(path)?;
155 self.parser.parse(path, &content)
156 }
157}
158
159#[derive(Debug)]
161pub struct WikiResult {
162 pub output_dir: String,
164 pub generated_files: Vec<String>,
166 pub adr_count: usize,
168 pub parse_errors: Vec<(std::path::PathBuf, crate::error::Error)>,
170}
171
172impl WikiResult {
173 #[must_use]
175 pub fn has_errors(&self) -> bool {
176 !self.parse_errors.is_empty()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::infrastructure::fs::test_support::InMemoryFileSystem;
184
185 fn sample_adr_content() -> &'static str {
186 r"---
187title: Use PostgreSQL for persistence
188status: accepted
189category: database
190created: 2025-01-15
191description: We decided to use PostgreSQL as our primary database.
192---
193
194# Use PostgreSQL for persistence
195
196## Context
197
198We need a database for our application.
199
200## Decision
201
202We will use PostgreSQL.
203
204## Consequences
205
206- We get ACID compliance
207- We need to manage database migrations
208"
209 }
210
211 #[test]
212 fn test_wiki_success() {
213 let fs = InMemoryFileSystem::new();
214 fs.add_file("docs/decisions/adr-0001.md", sample_adr_content());
215
216 let use_case = WikiUseCase::new(fs);
217 let options = WikiOptions::new("docs/decisions")
218 .with_output_dir("wiki")
219 .with_pages_url("https://example.com/adrs");
220
221 let result = use_case.execute(&options);
222 assert!(result.is_ok());
223
224 let result = result.unwrap();
225 assert_eq!(result.adr_count, 1);
226 assert!(!result.has_errors());
227 assert!(result.generated_files.len() >= 5);
229 }
230
231 #[test]
232 fn test_wiki_no_adrs() {
233 let fs = InMemoryFileSystem::new();
234 let use_case = WikiUseCase::new(fs);
235 let options = WikiOptions::new("empty/dir");
236
237 let result = use_case.execute(&options);
238 assert!(result.is_err());
239 }
240
241 #[test]
242 fn test_wiki_options_builder() {
243 let options = WikiOptions::new("input")
244 .with_output_dir("wiki")
245 .with_pages_url("https://example.com")
246 .with_pattern("*.md");
247
248 assert_eq!(options.input_dir, "input");
249 assert_eq!(options.output_dir, "wiki");
250 assert_eq!(options.pages_url, Some("https://example.com".to_string()));
251 assert_eq!(options.pattern, "*.md");
252 }
253}