ito_core/
spec_repository.rs1use std::path::Path;
4
5use chrono::{DateTime, Utc};
6
7use ito_common::paths;
8use ito_domain::errors::DomainError;
9use ito_domain::specs::{SpecDocument, SpecRepository, SpecSummary};
10
11pub struct FsSpecRepository<'a> {
13 ito_path: &'a Path,
14}
15
16impl<'a> FsSpecRepository<'a> {
17 pub fn new(ito_path: &'a Path) -> Self {
19 Self { ito_path }
20 }
21
22 fn spec_ids(&self) -> Result<Vec<String>, DomainError> {
23 let fs = ito_common::fs::StdFs;
24 let mut ids = ito_domain::discovery::list_spec_dir_names(&fs, self.ito_path)?;
25 ids.sort();
26 Ok(ids)
27 }
28
29 fn last_modified(&self, path: &Path) -> DateTime<Utc> {
30 let Ok(metadata) = std::fs::metadata(path) else {
31 return Utc::now();
32 };
33 let Ok(modified) = metadata.modified() else {
34 return Utc::now();
35 };
36 DateTime::<Utc>::from(modified)
37 }
38}
39
40impl SpecRepository for FsSpecRepository<'_> {
41 fn list(&self) -> Result<Vec<SpecSummary>, DomainError> {
42 let ids = self.spec_ids()?;
43 let mut specs = Vec::with_capacity(ids.len());
44 for id in ids {
45 let path = paths::spec_markdown_path(self.ito_path, &id);
46 if !path.is_file() {
47 continue;
48 }
49 specs.push(SpecSummary {
50 id,
51 path: path.clone(),
52 last_modified: self.last_modified(&path),
53 });
54 }
55 Ok(specs)
56 }
57
58 fn get(&self, id: &str) -> Result<SpecDocument, DomainError> {
59 let path = paths::spec_markdown_path(self.ito_path, id);
60 let markdown = ito_common::io::read_to_string(&path).map_err(|err| {
61 DomainError::io(
62 "reading promoted spec",
63 std::io::Error::other(err.to_string()),
64 )
65 })?;
66 Ok(SpecDocument {
67 id: id.to_string(),
68 path: path.clone(),
69 markdown,
70 last_modified: self.last_modified(&path),
71 })
72 }
73}