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