Skip to main content

onevcs_testing/
store.rs

1//! Where a provider's state lives, and the only thing the two flavours differ in.
2//!
3//! Both flavours of each interface are one implementation over one of these, so a
4//! behaviour cannot be taught to the in-memory provider and forgotten on the
5//! file-backed one: there is only one place it could be written.
6
7use std::marker::PhantomData;
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10
11use serde::de::DeserializeOwned;
12use serde::Serialize;
13
14use onevcs::{Error, Result};
15
16/// A state that can say whether it is one a provider may act on, and what it
17/// becomes when it arrived at an older version.
18///
19/// Shape is what serde proves, and shape is not enough for a document that came
20/// off a disk: a session token names a file under the state root and a branch name
21/// goes on to spell a ref, so a seeded state that carries an unusable one is
22/// refused where it is read rather than wherever it first happens to be used.
23///
24/// Both halves belong to the same boundary — the read — which is why they are one
25/// trait. A document is refused or it is the current shape; nothing past here has
26/// to ask which version it came in as.
27pub trait Checked {
28    /// Refuse this state, naming what is wrong with it.
29    fn check(&self) -> Result<()>;
30
31    /// Carry a document that declared an older readable version forward to the one
32    /// this build writes.
33    ///
34    /// Called on every read, after [`check`](Checked::check) has decided the
35    /// version is one this build reads at all. It has no default: a state type that
36    /// says nothing about what an older document becomes is one whose next field
37    /// addition writes a document declaring a version it is not.
38    fn carry_forward(&mut self);
39}
40
41/// A provider's state, however it is kept.
42pub trait Store<S> {
43    /// Read the state, act on it, and keep whatever the action left.
44    ///
45    /// One call rather than a read and a write, so a file-backed provider's
46    /// read-modify-write is atomic from a caller's point of view and an action that
47    /// fails leaves the state it started from.
48    fn with<R, F>(&self, act: F) -> Result<R>
49    where
50        F: FnOnce(&mut S) -> Result<R>;
51
52    /// The state as it stands.
53    fn snapshot(&self) -> Result<S>;
54}
55
56/// State held in this process and nowhere else: no disk, and no visibility to a
57/// second process.
58#[derive(Debug)]
59pub struct MemoryStore<S>(Arc<Mutex<S>>);
60
61impl<S> MemoryStore<S> {
62    /// Hold this state in memory.
63    pub fn new(state: S) -> Self {
64        Self(Arc::new(Mutex::new(state)))
65    }
66}
67
68/// A second handle on the *same* state, which is what lets a host factory hand out
69/// a per-repository host that a journey can still read back through the value it
70/// created.
71impl<S> Clone for MemoryStore<S> {
72    fn clone(&self) -> Self {
73        Self(Arc::clone(&self.0))
74    }
75}
76
77impl<S: Clone> Store<S> for MemoryStore<S> {
78    fn with<R, F>(&self, act: F) -> Result<R>
79    where
80        F: FnOnce(&mut S) -> Result<R>,
81    {
82        // A panic inside an action poisons the lock, and refusing every later call
83        // because an earlier assertion failed would bury the failure a journey is
84        // there to read.
85        let mut guard = self
86            .0
87            .lock()
88            .unwrap_or_else(|poisoned| poisoned.into_inner());
89        act(&mut guard)
90    }
91
92    fn snapshot(&self) -> Result<S> {
93        let guard = self
94            .0
95            .lock()
96            .unwrap_or_else(|poisoned| poisoned.into_inner());
97        Ok(guard.clone())
98    }
99}
100
101/// State held in one JSON document, so a second process — the next `onevcs`
102/// invocation — sees what the first one left.
103#[derive(Debug)]
104pub struct FileStore<S> {
105    path: PathBuf,
106    marker: PhantomData<S>,
107}
108
109impl<S> Clone for FileStore<S> {
110    fn clone(&self) -> Self {
111        Self {
112            path: self.path.clone(),
113            marker: PhantomData,
114        }
115    }
116}
117
118impl<S: Serialize + DeserializeOwned + Checked> FileStore<S> {
119    /// The store at `path`: whatever is already there, or `fallback` written out.
120    ///
121    /// Attaching rather than replacing is what makes a second provider over the
122    /// same path the *same* state — which is the whole reason to keep it in a file.
123    /// A document that is there but is not this shape is refused here rather than
124    /// at whichever call first read it.
125    pub fn attach(path: impl Into<PathBuf>, fallback: &S) -> Result<Self> {
126        let store = Self::at(path)?;
127        if store.path.exists() {
128            store.snapshot()?;
129            return Ok(store);
130        }
131        store.save(fallback)?;
132        Ok(store)
133    }
134
135    /// The store at `path`, holding this state whatever was there before.
136    ///
137    /// Written eagerly rather than on first use: a journey that reads the document
138    /// before anything has driven the provider must find the scenario it seeded.
139    pub fn replace(path: impl Into<PathBuf>, state: &S) -> Result<Self> {
140        let store = Self::at(path)?;
141        store.save(state)?;
142        Ok(store)
143    }
144
145    fn at(path: impl Into<PathBuf>) -> Result<Self> {
146        let path = path.into();
147        if let Some(parent) = path
148            .parent()
149            .filter(|parent| !parent.as_os_str().is_empty())
150        {
151            std::fs::create_dir_all(parent).map_err(|e| Error::Invalid {
152                reason: format!("cannot create {}: {e}", parent.display()),
153            })?;
154        }
155        Ok(Self {
156            path,
157            marker: PhantomData,
158        })
159    }
160
161    /// The document this state is kept in.
162    pub fn path(&self) -> &Path {
163        &self.path
164    }
165
166    fn save(&self, state: &S) -> Result<()> {
167        let json = serde_json::to_string_pretty(state).map_err(|e| Error::Invalid {
168            reason: format!(
169                "cannot serialize the state for {}: {e}",
170                self.path.display()
171            ),
172        })?;
173        std::fs::write(&self.path, format!("{json}\n")).map_err(|e| Error::Invalid {
174            reason: format!("cannot write {}: {e}", self.path.display()),
175        })
176    }
177}
178
179impl<S: Serialize + DeserializeOwned + Checked> Store<S> for FileStore<S> {
180    fn with<R, F>(&self, act: F) -> Result<R>
181    where
182        F: FnOnce(&mut S) -> Result<R>,
183    {
184        let mut state = self.snapshot()?;
185        let outcome = act(&mut state)?;
186        self.save(&state)?;
187        Ok(outcome)
188    }
189
190    fn snapshot(&self) -> Result<S> {
191        let raw = std::fs::read_to_string(&self.path).map_err(|e| Error::Invalid {
192            reason: format!(
193                "cannot read the provider state at {}: {e}",
194                self.path.display()
195            ),
196        })?;
197        let mut state: S = serde_json::from_str(&raw).map_err(|e| Error::Invalid {
198            reason: format!(
199                "the provider state at {} is not the shape this crate writes: {e}",
200                self.path.display()
201            ),
202        })?;
203        state.check().map_err(|e| Error::Invalid {
204            reason: format!("the provider state at {}: {e}", self.path.display()),
205        })?;
206        // On the way in, not on the way out: everything past this point works with
207        // the shape this build writes, so the next write of a document that arrived
208        // at an older version declares the version it now holds rather than the one
209        // it was seeded at.
210        state.carry_forward();
211        Ok(state)
212    }
213}