use std::fs;
use std::path::PathBuf;
use crate::domain::model::query::{Queries, Query, QueryDescription, QueryIdentifier, QueryScript};
use crate::domain::usecases::query::QueryStore;
pub struct CozoFileStore {
pub dir: PathBuf,
}
impl CozoFileStore {
pub fn new(dir: PathBuf) -> Self {
Self { dir }
}
}
impl QueryStore for CozoFileStore {
fn list(&self) -> anyhow::Result<Queries> {
let entries = match fs::read_dir(&self.dir) {
Ok(e) => e,
Err(_) => return Ok(Queries::new()),
};
let mut out: Vec<Query> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("cozo") {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
let Ok(name) = QueryIdentifier::new(stem) else {
continue;
};
let Ok(body) = fs::read_to_string(&path) else {
continue;
};
let description = extract_description(&body).map(QueryDescription::new);
out.push(Query::new(name, QueryScript::new(body), description));
}
Ok(out.into_iter().collect())
}
}
fn extract_description(body: &str) -> Option<String> {
for line in body.lines().take(10) {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("# description:") {
return Some(rest.trim().to_owned());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn store_at(dir: &TempDir) -> CozoFileStore {
CozoFileStore::new(dir.path().to_path_buf())
}
fn name(s: &str) -> QueryIdentifier {
QueryIdentifier::new(s).unwrap()
}
#[test]
fn list_empty_dir_yields_empty_collection() {
let dir = TempDir::new().unwrap();
assert!(store_at(&dir).list().unwrap().is_empty());
}
#[test]
fn list_missing_dir_yields_empty_collection_without_error() {
let dir = TempDir::new().unwrap();
let store = CozoFileStore::new(dir.path().join("does-not-exist"));
assert!(store.list().unwrap().is_empty());
}
#[test]
fn list_returns_each_cozo_file_with_its_script_body() {
let dir = TempDir::new().unwrap();
let alpha_body = "# description: alpha doc\n?[x] := x = 1\n";
let bravo_body = "?[x] := x = 2\n";
let charlie_body = "\n# description: charlie doc\n";
fs::write(dir.path().join("alpha.cozo"), alpha_body).unwrap();
fs::write(dir.path().join("bravo.cozo"), bravo_body).unwrap();
fs::write(dir.path().join("charlie.cozo"), charlie_body).unwrap();
fs::write(dir.path().join("ignored.txt"), "not a query\n").unwrap();
let qs = store_at(&dir).list().unwrap();
let alpha = qs.get(&name("alpha")).unwrap();
let bravo = qs.get(&name("bravo")).unwrap();
let charlie = qs.get(&name("charlie")).unwrap();
assert_eq!(alpha.script.as_str(), alpha_body);
assert_eq!(
alpha.description.as_ref().map(|d| d.as_str()),
Some("alpha doc")
);
assert_eq!(bravo.script.as_str(), bravo_body);
assert!(bravo.description.is_none());
assert_eq!(
charlie.description.as_ref().map(|d| d.as_str()),
Some("charlie doc")
);
assert!(qs.get(&name("ignored")).is_none());
}
#[test]
fn files_with_invalid_names_are_silently_skipped() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("Mixed-Case.cozo"), "?[x] := x = 1\n").unwrap();
fs::write(dir.path().join("ok.cozo"), "?[x] := x = 1\n").unwrap();
let qs = store_at(&dir).list().unwrap();
assert!(qs.get(&name("ok")).is_some());
assert_eq!(qs.len(), 1);
}
#[test]
fn description_on_fifth_line_is_extracted() {
assert_eq!(
extract_description("\n\n\n\n# description: late finding\n"),
Some("late finding".to_owned())
);
}
#[test]
fn description_after_line_ten_is_ignored() {
let mut body = String::new();
for _ in 0..10 {
body.push('\n');
}
body.push_str("# description: too late\n");
assert_eq!(extract_description(&body), None);
}
#[test]
fn description_tolerates_leading_whitespace() {
assert_eq!(
extract_description(" # description: spaced out \n"),
Some("spaced out".to_owned())
);
}
#[test]
fn description_picks_first_matching_comment() {
assert_eq!(
extract_description("# not a description\n# description: real one\n"),
Some("real one".to_owned())
);
}
}