Skip to main content

loftspec_store/
lib.rs

1//! Memory and file stores for Loft Specification Format documents: one interface that loads a
2//! document, saves it and reports its roots, held in memory or as a `spec/` tree, so the same
3//! scenarios run against both and the two agree.
4
5pub mod clock;
6mod error;
7pub mod file;
8pub mod fs;
9pub mod journal;
10pub mod memory;
11pub mod merge_driver;
12pub mod repository;
13
14use loftspec::codec::{Files, Role, SpecPath};
15use loftspec::document::Document;
16use loftspec::hash::{Roots, hash_document};
17
18pub use error::StoreError;
19pub use file::FileStore;
20pub use memory::MemoryStore;
21
22/// Where a document is held between commands.
23pub trait DocumentStore {
24    /// The document the store holds.
25    ///
26    /// # Errors
27    ///
28    /// The store holds no document, or its document cannot be read.
29    fn load(&self) -> Result<Document, StoreError>;
30
31    /// Replaces the document the store holds, and reports the files of the document's `spec/`
32    /// tree that the save wrote and removed.
33    ///
34    /// # Errors
35    ///
36    /// The document cannot be saved; a file store then leaves every file as it was or finishes
37    /// the save the next time it is opened.
38    fn save(&mut self, document: &Document) -> Result<Saved, StoreError>;
39
40    /// The roots of the document the store holds.
41    ///
42    /// # Errors
43    ///
44    /// As [`DocumentStore::load`], or the document does not index.
45    fn roots(&self) -> Result<Roots, StoreError> {
46        hash_document(&self.load()?)
47            .map(|hashes| hashes.roots)
48            .map_err(StoreError::Index)
49    }
50}
51
52/// The files of the `spec/` tree a save wrote and removed.
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct Saved {
55    pub written: Vec<SpecPath>,
56    pub removed: Vec<SpecPath>,
57}
58
59impl Saved {
60    /// What a save that turns the tree `current` into `next` writes and removes: each file whose
61    /// bytes differ, and each file the writer owns that `next` no longer has. A file the document
62    /// does not hold, such as `lint.yaml`, is never removed.
63    #[must_use]
64    pub fn between(current: &Files, next: &Files) -> Self {
65        let written = next
66            .iter()
67            .filter(|(path, bytes)| current.get(*path) != Some(*bytes))
68            .map(|(path, _)| path.clone())
69            .collect();
70        let removed = current
71            .keys()
72            .filter(|path| !next.contains_key(*path))
73            .filter(|path| Role::of(path).is_some_and(|role| role.is_written()))
74            .cloned()
75            .collect();
76        Self { written, removed }
77    }
78
79    #[must_use]
80    pub fn is_empty(&self) -> bool {
81        self.written.is_empty() && self.removed.is_empty()
82    }
83}