Skip to main content

rust_elm/infra/
env.rs

1use std::cell::RefCell;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use parking_lot::{Mutex, RwLock};
8
9use rust_dependencies::{Clock, ClockDep, DependencyError, DependencyValues};
10
11pub use rust_dependencies::{
12    ClockKey, DepRng, DependencyKey, LiveRng, LiveUuidGen, Now, NowDep, NowKey, RealClock,
13    RngDep, RngKey, SeededRng, SeededUuidGen, TestNow, UuidGen, UuidKey,
14};
15
16/// Test double clock for deterministic effect tests.
17#[derive(Debug, Clone)]
18pub struct FakeClock {
19    inner: Arc<Mutex<Instant>>,
20}
21
22impl FakeClock {
23    pub fn new(start: Instant) -> Self {
24        Self {
25            inner: Arc::new(Mutex::new(start)),
26        }
27    }
28
29    pub fn now(&self) -> Instant {
30        *self.inner.lock()
31    }
32
33    pub fn advance(&self, duration: Duration) {
34        *self.inner.lock() += duration;
35    }
36}
37
38impl Clock for FakeClock {
39    fn now(&self) -> Instant {
40        FakeClock::now(self)
41    }
42}
43
44/// Minimal HTTP mock for env-backed effects.
45#[derive(Debug, Clone, Default)]
46pub struct MockHttp {
47    responses: Arc<Mutex<Vec<(String, String)>>>,
48}
49
50impl MockHttp {
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    pub fn stub(&self, url: impl Into<String>, body: impl Into<String>) {
56        self.responses.lock().push((url.into(), body.into()));
57    }
58
59    pub fn get(&self, url: &str) -> Option<String> {
60        self.responses
61            .lock()
62            .iter()
63            .find(|(u, _)| u == url)
64            .map(|(_, b)| b.clone())
65    }
66}
67
68/// Layered environment with typed dependencies (UDF `DependencyValues` + scoped overrides).
69#[derive(Clone)]
70pub struct Environment {
71    layers: Arc<Mutex<Vec<DependencyValues>>>,
72    merged_cache: Arc<RwLock<Option<DependencyValues>>>,
73}
74
75impl Default for Environment {
76    fn default() -> Self {
77        Self::live()
78    }
79}
80
81impl std::fmt::Debug for Environment {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("Environment")
84            .field("layers", &self.layers.lock().len())
85            .finish()
86    }
87}
88
89impl Environment {
90    pub fn new() -> Self {
91        Self::live()
92    }
93
94    pub fn live() -> Self {
95        Self::from_values(DependencyValues::live())
96    }
97
98    pub fn test() -> Self {
99        Self::from_values(DependencyValues::test())
100    }
101
102    pub fn from_values(base: DependencyValues) -> Self {
103        Self {
104            layers: Arc::new(Mutex::new(vec![base])),
105            merged_cache: Arc::new(RwLock::new(None)),
106        }
107    }
108
109    fn invalidate_merged_cache(&self) {
110        *self.merged_cache.write() = None;
111    }
112
113    pub fn values(&self) -> DependencyValues {
114        if let Some(cached) = self.merged_cache.read().clone() {
115            return cached;
116        }
117        let layers = self.layers.lock();
118        let merged = DependencyValues::new();
119        for layer in layers.iter() {
120            merged.merge_from(layer);
121        }
122        *self.merged_cache.write() = Some(merged.clone());
123        merged
124    }
125
126    pub fn get<D: Send + Sync + 'static>(&self) -> Option<Arc<D>> {
127        let layers = self.layers.lock();
128        for layer in layers.iter().rev() {
129            if let Some(value) = layer.get::<D>() {
130                return Some(value);
131            }
132        }
133        None
134    }
135
136    pub fn require<D: Send + Sync + 'static>(&self) -> Result<Arc<D>, DependencyError> {
137        self.get::<D>()
138            .ok_or_else(|| DependencyError::missing(std::any::type_name::<D>()))
139    }
140
141    pub fn with<D: Send + Sync + 'static>(self, value: D) -> Self {
142        let overlay = DependencyValues::new();
143        overlay.insert(value);
144        self.push_values(overlay);
145        self
146    }
147
148    pub fn push_values(&self, overlay: DependencyValues) {
149        self.layers.lock().push(overlay);
150        self.invalidate_merged_cache();
151    }
152
153    /// Fork the environment and append overlay dependency layers (for `Effect::provide`).
154    pub fn scoped_with(&self, overlay: Environment) -> Self {
155        let mut layers = self.layers.lock().clone();
156        layers.extend(overlay.layers.lock().iter().cloned());
157        Self {
158            layers: Arc::new(Mutex::new(layers)),
159            merged_cache: Arc::new(RwLock::new(None)),
160        }
161    }
162
163    pub fn with_clock(self, clock: FakeClock) -> Self {
164        self.with(clock.clone()).with(ClockDep(Arc::new(clock)))
165    }
166
167    pub fn with_http(self, http: MockHttp) -> Self {
168        self.with(http)
169    }
170
171    pub fn clock(&self) -> Option<FakeClock> {
172        self.get::<FakeClock>().map(|c| c.as_ref().clone())
173    }
174
175    pub fn http(&self) -> Option<MockHttp> {
176        self.get::<MockHttp>().map(|arc| (*arc).clone())
177    }
178
179    /// Back-compat alias for [`Self::scoped_with`].
180    pub fn push_layer(&self, overlay: Environment) {
181        self.layers
182            .lock()
183            .extend(overlay.layers.lock().iter().cloned());
184        self.invalidate_merged_cache();
185    }
186}
187
188thread_local! {
189    static BATCH_QUEUE: RefCell<Vec<Box<dyn FnOnce() + 'static>>> = RefCell::new(Vec::new());
190}
191
192/// Fiber-local batching for coalescing synchronous dispatches within one update tick.
193pub fn batch<R>(f: impl FnOnce() -> R) -> R {
194    BATCH_QUEUE.with(|q| q.borrow_mut().clear());
195    let result = f();
196    BATCH_QUEUE.with(|q| {
197        let mut queue = q.borrow_mut();
198        while let Some(job) = queue.pop() {
199            job();
200        }
201    });
202    result
203}
204
205pub fn defer_batch(job: impl FnOnce() + 'static) {
206    BATCH_QUEUE.with(|q| q.borrow_mut().push(Box::new(job)));
207}
208
209pub type EnvFuture<M> = Pin<Box<dyn Future<Output = Result<M, crate::error::EffectError>> + Send>>;
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use rust_dependencies::{RngDep, SeededUuidGen, UuidDep};
215
216    #[test]
217    fn fake_clock_advances() {
218        let start = Instant::now();
219        let clock = FakeClock::new(start);
220        clock.advance(Duration::from_millis(100));
221        assert_eq!(clock.now(), start + Duration::from_millis(100));
222    }
223
224    #[test]
225    fn mock_http_stubs_responses() {
226        let http = MockHttp::new();
227        http.stub("https://example.com", "ok");
228        assert_eq!(http.get("https://example.com"), Some("ok".into()));
229    }
230
231    #[test]
232    fn test_env_is_deterministic() {
233        let env = Environment::test();
234        let uuid1 = env.require::<UuidDep>().unwrap().0.next();
235        let rng1 = env.require::<RngDep>().unwrap().0.next_u64();
236        let env2 = Environment::test();
237        let uuid2 = env2.require::<UuidDep>().unwrap().0.next();
238        let rng2 = env2.require::<RngDep>().unwrap().0.next_u64();
239        assert_eq!(uuid1, uuid2);
240        assert_eq!(rng1, rng2);
241    }
242
243    #[test]
244    fn scoped_override_replaces_dependency() {
245        let env = Environment::test();
246        let base = env.require::<UuidDep>().unwrap().0.next();
247        let scoped = env.scoped_with(Environment::new().with(UuidDep(Arc::new(SeededUuidGen::new(
248            99,
249        )))));
250        let overridden = scoped.require::<UuidDep>().unwrap().0.next();
251        assert_ne!(base, overridden);
252        assert_eq!(overridden, SeededUuidGen::new(99).next());
253    }
254}