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, transition: &Transition<'_>) -> Result<(), NotHeld> {
99 let mut records = self
100 .records
101 .lock()
102 .expect("registry mutex should not be poisoned");
103 for record in records.iter_mut() {
104 if let Ok(next) = transition(&record.state) {
105 record.state = next;
106 return Ok(());
107 }
108 }
109 Err(NotHeld)
110 }
111}
112
113pub struct MemoryRegistry {
115 store: Store,
116}
117
118impl MemoryRegistry {
119 #[must_use]
121 pub fn new(lease_millis: u64) -> Self {
122 Self::seeded(Vec::new(), lease_millis)
123 }
124
125 #[must_use]
128 pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
129 Self {
130 store: Store::seeded(pacts, lease_millis),
131 }
132 }
133}
134
135impl Registry for MemoryRegistry {
136 type Error = NotHeld;
137
138 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
139 Ok(self.store.claim(dockets, now))
140 }
141
142 fn lease_millis(&self) -> u64 {
143 self.store.lease_millis()
144 }
145
146 fn apply(&self, _retainer: &Retainer, transition: &Transition<'_>) -> Result<(), Self::Error> {
147 self.store.apply(transition)
148 }
149}
150
151#[cfg(feature = "async")]
156pub struct MemoryRegistryAsync {
157 store: Store,
158}
159
160#[cfg(feature = "async")]
161impl MemoryRegistryAsync {
162 #[must_use]
164 pub fn new(lease_millis: u64) -> Self {
165 Self::seeded(Vec::new(), lease_millis)
166 }
167
168 #[must_use]
170 pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
171 Self {
172 store: Store::seeded(pacts, lease_millis),
173 }
174 }
175}
176
177#[cfg(feature = "async")]
178impl pacta_contract::AsyncRegistry for MemoryRegistryAsync {
179 type Error = NotHeld;
180
181 async fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, NotHeld> {
182 Ok(self.store.claim(dockets, now))
183 }
184
185 fn lease_millis(&self) -> u64 {
186 self.store.lease_millis()
187 }
188
189 async fn apply(
190 &self,
191 _retainer: &Retainer,
192 transition: &Transition<'_>,
193 ) -> Result<(), NotHeld> {
194 self.store.apply(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 fn a_pact() -> Pact {
210 Pact::new(Uuid::new_v4(), "d".to_string(), "k".to_string(), Vec::new())
211 }
212
213 #[test]
214 fn release_rejects_a_non_holder() {
215 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
216 registry
217 .claim(&["d"], Timestamp::from_millis(0))
218 .expect("claim should not error")
219 .expect("a pact should be claimable");
220 let stranger = Retainer::new(Uuid::new_v4());
221 assert_eq!(
222 registry.release(&stranger, Timestamp::from_millis(0)),
223 Err(NotHeld),
224 "release by a non-holder must be rejected, like fulfill and breach"
225 );
226 }
227
228 #[test]
229 fn a_settled_pact_cannot_be_released() {
230 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
231 let claim = registry
232 .claim(&["d"], Timestamp::from_millis(0))
233 .expect("claim should not error")
234 .expect("a pact should be claimable");
235 registry
236 .fulfill(&claim.retainer)
237 .expect("fulfill should settle");
238 assert_eq!(
239 registry.release(&claim.retainer, Timestamp::from_millis(0)),
240 Err(NotHeld),
241 "a concluded obligation has no claim to relinquish"
242 );
243 }
244
245 #[cfg(feature = "async")]
248 #[test]
249 fn passes_async_conformance() {
250 pacta_conformance::run_async(MemoryRegistryAsync::seeded);
251 }
252
253 #[cfg(feature = "async")]
256 #[test]
257 fn passes_async_contention() {
258 pacta_conformance::run_async_contention(MemoryRegistryAsync::seeded);
259 }
260}