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}