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}