stealthscraper_rs/state/
store.rs1use std::collections::HashMap;
4use std::sync::Mutex;
5
6use crate::Error;
7
8use super::model::DomainState;
9
10pub trait StateStore: Send + Sync {
16 fn get(&self, host: &str) -> Result<Option<DomainState>, Error>;
18 fn put(&self, state: &DomainState) -> Result<(), Error>;
20 fn remove(&self, host: &str) -> Result<(), Error>;
22
23 fn update(
34 &self,
35 host: &str,
36 update: &mut dyn FnMut(DomainState) -> DomainState,
37 ) -> Result<DomainState, Error> {
38 let current = self.get(host)?.unwrap_or_else(|| DomainState::new(host));
39 let next = update(current);
40 self.put(&next)?;
41 Ok(next)
42 }
43}
44
45#[derive(Debug, Default)]
48pub struct InMemoryStateStore {
49 inner: Mutex<HashMap<String, DomainState>>,
50}
51
52impl InMemoryStateStore {
53 pub fn new() -> Self {
55 Self::default()
56 }
57}
58
59impl StateStore for InMemoryStateStore {
60 fn get(&self, host: &str) -> Result<Option<DomainState>, Error> {
61 Ok(self
62 .inner
63 .lock()
64 .expect("state store lock poisoned")
65 .get(host)
66 .cloned())
67 }
68
69 fn put(&self, state: &DomainState) -> Result<(), Error> {
70 self.inner
71 .lock()
72 .expect("state store lock poisoned")
73 .insert(state.host.clone(), state.clone());
74 Ok(())
75 }
76
77 fn remove(&self, host: &str) -> Result<(), Error> {
78 self.inner
79 .lock()
80 .expect("state store lock poisoned")
81 .remove(host);
82 Ok(())
83 }
84
85 fn update(
86 &self,
87 host: &str,
88 update: &mut dyn FnMut(DomainState) -> DomainState,
89 ) -> Result<DomainState, Error> {
90 let mut guard = self.inner.lock().expect("state store lock poisoned");
91 let current = guard
92 .get(host)
93 .cloned()
94 .unwrap_or_else(|| DomainState::new(host));
95 let next = update(current);
96 guard.insert(next.host.clone(), next.clone());
97 Ok(next)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::state::Outcome;
105 use std::time::Duration;
106
107 #[test]
108 fn in_memory_round_trips_state() {
109 let store = InMemoryStateStore::new();
110 assert_eq!(store.get("example.com").unwrap(), None);
111
112 let state = DomainState::new("example.com").record(
113 Outcome::Success,
114 Some("http://p:1".into()),
115 10,
116 Duration::ZERO,
117 );
118 store.put(&state).unwrap();
119
120 let loaded = store.get("example.com").unwrap().unwrap();
121 assert_eq!(loaded, state);
122 }
123
124 #[test]
125 fn in_memory_update_creates_then_modifies() {
126 let store = InMemoryStateStore::new();
127
128 let s1 = store
130 .update("example.com", &mut |cur| {
131 cur.record(Outcome::Blocked, None, 1, Duration::ZERO)
132 })
133 .unwrap();
134 assert_eq!(s1.failures, 1);
135 assert_eq!(s1.host, "example.com");
136
137 let s2 = store
139 .update("example.com", &mut |cur| {
140 cur.record(Outcome::Success, None, 2, Duration::ZERO)
141 })
142 .unwrap();
143 assert_eq!(s2.failures, 1);
144 assert_eq!(s2.successes, 1);
145 assert_eq!(store.get("example.com").unwrap().unwrap(), s2);
146 }
147
148 #[test]
149 fn in_memory_remove_deletes() {
150 let store = InMemoryStateStore::new();
151 store.put(&DomainState::new("h")).unwrap();
152 store.remove("h").unwrap();
153 assert_eq!(store.get("h").unwrap(), None);
154 store.remove("h").unwrap();
156 }
157}