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