use std::path::{Path, PathBuf};
use crate::domain::model::body::Body;
use crate::domain::model::site::{MarkdownEntry, Source};
use crate::domain::usecases::site::readers::PagesReader;
pub struct FsPagesReader {
root: PathBuf,
}
impl FsPagesReader {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
}
impl PagesReader for FsPagesReader {
fn entries<'a>(&'a self) -> Box<dyn Iterator<Item = anyhow::Result<MarkdownEntry>> + 'a> {
if !self.root.is_dir() {
return Box::new(std::iter::empty());
}
let root = self.root.as_path();
let it = walkdir::WalkDir::new(root)
.follow_links(false)
.into_iter()
.filter_map(move |res| match res {
Ok(e) if is_markdown_file(e.path()) => Some(read_entry(root, e.path())),
Ok(_) => None,
Err(e) => Some(Err(anyhow::Error::new(e))),
});
Box::new(it)
}
}
fn is_markdown_file(path: &Path) -> bool {
path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("md")
}
fn read_entry(root: &Path, path: &Path) -> anyhow::Result<MarkdownEntry> {
let relative = path
.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/");
let source = Source::relative_path(&relative)?;
let content = Body::new(std::fs::read_to_string(path)?);
Ok(MarkdownEntry::new(source, content))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_root_yields_no_entries() {
let tmp = tempfile::tempdir().unwrap();
let reader = FsPagesReader::new(tmp.path());
assert_eq!(reader.entries().count(), 0);
}
#[test]
fn non_existing_root_yields_no_entries() {
let reader = FsPagesReader::new("/definitely/not/a/real/path/here");
assert_eq!(reader.entries().count(), 0);
}
#[test]
fn walks_recursively_and_returns_relative_sources() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sub")).unwrap();
std::fs::write(tmp.path().join("a.md"), "---\ntitle: A\n---\nbody-a").unwrap();
std::fs::write(tmp.path().join("sub/b.md"), "---\ntitle: B\n---\nbody-b").unwrap();
std::fs::write(tmp.path().join("sub/ignore.txt"), "ignored").unwrap();
let reader = FsPagesReader::new(tmp.path());
let mut entries: Vec<MarkdownEntry> = reader
.entries()
.collect::<anyhow::Result<_>>()
.expect("all entries readable");
entries.sort_by(|x, y| x.source.as_str().cmp(y.source.as_str()));
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].source.as_str(), "a.md");
assert_eq!(entries[1].source.as_str(), "sub/b.md");
assert!(entries[1].content.as_str().contains("body-b"));
}
}