1#![forbid(unsafe_code)]
15#![warn(missing_docs)]
16
17use std::sync::Mutex;
18
19use pacta_contract::lifecycle::{self, State};
20use pacta_contract::{Claim, Pact, Registry, Retainer, Timestamp, Transition};
21use uuid::Uuid;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct NotHeld;
27
28impl std::fmt::Display for NotHeld {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(f, "retainer is not the current holder of any claim")
31 }
32}
33
34impl std::error::Error for NotHeld {}
35
36impl From<lifecycle::NotCurrentHolder> for NotHeld {
37 fn from(_: lifecycle::NotCurrentHolder) -> Self {
38 NotHeld
39 }
40}
41
42struct Record {
43 pact: Pact,
44 state: State,
45}
46
47struct Store {
51 records: Mutex<Vec<Record>>,
52 lease_millis: u64,
53}
54
55impl Store {
56 fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
57 Self {
58 records: Mutex::new(
59 pacts
60 .into_iter()
61 .map(|pact| Record {
62 pact,
63 state: State::Available,
64 })
65 .collect(),
66 ),
67 lease_millis,
68 }
69 }
70
71 fn lease_millis(&self) -> u64 {
72 self.lease_millis
73 }
74
75 fn claim(&self, dockets: &[&str], now: Timestamp) -> Option<Claim> {
76 let mut records = self
77 .records
78 .lock()
79 .expect("registry mutex should not be poisoned");
80 let index = records.iter().position(|record| {
83 dockets.contains(&record.pact.docket.as_str())
84 && lifecycle::is_claimable(&record.state, now)
85 })?;
86 let retainer = Retainer::new(Uuid::new_v4());
88 records[index].state = lifecycle::on_claim(&retainer, now, self.lease_millis);
89 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
90 Some(Claim::new(records[index].pact.clone(), retainer, expiry))
91 }
92
93 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), NotHeld> {
103 let mut records = self
104 .records
105 .lock()
106 .expect("registry mutex should not be poisoned");
107 let record = records
108 .iter_mut()
109 .find(|record| matches!(&record.state, State::Held { retainer: held, .. } if held == retainer))
110 .ok_or(NotHeld)?;
111 record.state = transition(&record.state)?;
112 Ok(())
113 }
114}
115
116pub struct MemoryRegistry {
118 store: Store,
119}
120
121impl MemoryRegistry {
122 #[must_use]
124 pub fn new(lease_millis: u64) -> Self {
125 Self::seeded(Vec::new(), lease_millis)
126 }
127
128 #[must_use]
131 pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
132 Self {
133 store: Store::seeded(pacts, lease_millis),
134 }
135 }
136}
137
138impl Registry for MemoryRegistry {
139 type Error = NotHeld;
140
141 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
142 Ok(self.store.claim(dockets, now))
143 }
144
145 fn lease_millis(&self) -> u64 {
146 self.store.lease_millis()
147 }
148
149 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), Self::Error> {
150 self.store.apply(retainer, transition)
151 }
152}
153
154#[cfg(feature = "async")]
159pub struct MemoryRegistryAsync {
160 store: Store,
161}
162
163#[cfg(feature = "async")]
164impl MemoryRegistryAsync {
165 #[must_use]
167 pub fn new(lease_millis: u64) -> Self {
168 Self::seeded(Vec::new(), lease_millis)
169 }
170
171 #[must_use]
173 pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
174 Self {
175 store: Store::seeded(pacts, lease_millis),
176 }
177 }
178}
179
180#[cfg(feature = "async")]
181impl pacta_contract::AsyncRegistry for MemoryRegistryAsync {
182 type Error = NotHeld;
183
184 async fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, NotHeld> {
185 Ok(self.store.claim(dockets, now))
186 }
187
188 fn lease_millis(&self) -> u64 {
189 self.store.lease_millis()
190 }
191
192 async fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), NotHeld> {
193 self.store.apply(retainer, transition)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn passes_registry_conformance() {
206 pacta_conformance::run(MemoryRegistry::seeded);
207 }
208
209 #[test]
212 fn passes_sync_contention() {
213 pacta_conformance::run_contention(MemoryRegistry::seeded);
214 }
215
216 fn a_pact() -> Pact {
217 Pact::new(Uuid::new_v4(), "d".to_string(), "k".to_string(), Vec::new())
218 }
219
220 #[test]
221 fn release_rejects_a_non_holder() {
222 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
223 registry
224 .claim(&["d"], Timestamp::from_millis(0))
225 .expect("claim should not error")
226 .expect("a pact should be claimable");
227 let stranger = Retainer::new(Uuid::new_v4());
228 assert_eq!(
229 registry.release(&stranger, Timestamp::from_millis(0)),
230 Err(NotHeld),
231 "release by a non-holder must be rejected, like fulfill and breach"
232 );
233 }
234
235 #[test]
236 fn a_settled_pact_cannot_be_released() {
237 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
238 let claim = registry
239 .claim(&["d"], Timestamp::from_millis(0))
240 .expect("claim should not error")
241 .expect("a pact should be claimable");
242 registry
243 .fulfill(&claim.retainer)
244 .expect("fulfill should settle");
245 assert_eq!(
246 registry.release(&claim.retainer, Timestamp::from_millis(0)),
247 Err(NotHeld),
248 "a concluded obligation has no claim to relinquish"
249 );
250 }
251
252 #[test]
257 fn apply_rejects_a_stranger_even_with_an_any_state_transition() {
258 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
259 let claim = registry
260 .claim(&["d"], Timestamp::from_millis(0))
261 .expect("claim should not error")
262 .expect("a pact should be claimable");
263 let stranger = Retainer::new(Uuid::new_v4());
264 let accept_any = |_state: &State| Ok::<State, lifecycle::NotCurrentHolder>(State::Settled);
267 assert_eq!(
268 registry.apply(&stranger, &accept_any),
269 Err(NotHeld),
270 "a retainer that holds no record cannot apply, even an any-state transition"
271 );
272 registry
274 .fulfill(&claim.retainer)
275 .expect("the held state was untouched, so the holder still settles");
276 }
277
278 #[test]
281 fn apply_admits_the_true_holder() {
282 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
283 let claim = registry
284 .claim(&["d"], Timestamp::from_millis(0))
285 .expect("claim should not error")
286 .expect("a pact should be claimable");
287 registry
288 .heartbeat(&claim.retainer, Timestamp::from_millis(500))
289 .expect("the holder's heartbeat extends the lease");
290 registry
291 .release(&claim.retainer, Timestamp::from_millis(0))
292 .expect("the holder releases");
293 assert_eq!(
294 registry.fulfill(&claim.retainer),
295 Err(NotHeld),
296 "release rotated authority, so the prior retainer no longer holds a record"
297 );
298 }
299
300 #[cfg(feature = "async")]
303 #[test]
304 fn passes_async_conformance() {
305 pacta_conformance::run_async(MemoryRegistryAsync::seeded);
306 }
307
308 #[cfg(feature = "async")]
311 #[tokio::test]
312 async fn async_apply_rejects_a_stranger_even_with_an_any_state_transition() {
313 use pacta_contract::AsyncRegistry;
314
315 let registry = MemoryRegistryAsync::seeded(vec![a_pact()], 1000);
316 let claim = registry
317 .claim(&["d"], Timestamp::from_millis(0))
318 .await
319 .expect("claim should not error")
320 .expect("a pact should be claimable");
321 let stranger = Retainer::new(Uuid::new_v4());
322 let accept_any = |_state: &State| Ok::<State, lifecycle::NotCurrentHolder>(State::Settled);
323 assert_eq!(
324 registry.apply(&stranger, &accept_any).await,
325 Err(NotHeld),
326 "the async binding also locates by retainer"
327 );
328 registry
329 .fulfill(&claim.retainer)
330 .await
331 .expect("the held state was untouched, so the holder still settles");
332 }
333
334 #[cfg(feature = "async")]
339 #[test]
340 fn passes_async_contention() {
341 pacta_conformance::run_async_contention(MemoryRegistryAsync::seeded);
342 }
343}