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