Skip to main content

rust_elm/
sub.rs

1use std::time::Duration;
2
3/// Subscription descriptions — interpreted in [`crate::subscription`] / [`crate::runtime`] (requires `runtime` feature).
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub enum Sub<M> {
6    None,
7    Tick {
8        id: u64,
9        every: Duration,
10        produce: fn() -> M,
11    },
12    Stream {
13        id: u64,
14        name: &'static str,
15        every: Duration,
16        produce: fn() -> M,
17    },
18    WebSocket {
19        id: u64,
20        url: &'static str,
21        every: Duration,
22        produce: fn() -> M,
23    },
24    MapMsg {
25        inner: Box<Sub<()>>,
26        map: fn(()) -> M,
27    },
28    Batch(Vec<Sub<M>>),
29}
30
31impl<M> Sub<M> {
32    pub fn none() -> Self {
33        Self::None
34    }
35
36    pub fn tick(id: u64, every: Duration, produce: fn() -> M) -> Self {
37        Self::Tick { id, every, produce }
38    }
39
40    pub fn stream(id: u64, name: &'static str, every: Duration, produce: fn() -> M) -> Self {
41        Self::Stream {
42            id,
43            name,
44            every,
45            produce,
46        }
47    }
48
49    pub fn websocket(id: u64, url: &'static str, every: Duration, produce: fn() -> M) -> Self {
50        Self::WebSocket {
51            id,
52            url,
53            every,
54            produce,
55        }
56    }
57
58    /// Wrap a `Sub<()>` and map each firing into `M` (Elm `Sub.map`).
59    pub fn map_msg(inner: Sub<()>, map: fn(()) -> M) -> Self {
60        Self::MapMsg {
61            inner: Box::new(inner),
62            map,
63        }
64    }
65
66    /// Map batched subscription outputs. For leaf ticks/streams, use explicit `produce` fns or
67    /// [`Self::map_msg`] at the target action type.
68    pub fn map<N>(self, _f: fn(M) -> N) -> Sub<N> {
69        match self {
70            Self::None => Sub::None,
71            Self::Batch(items) => Sub::Batch(items.into_iter().map(|s| s.map(_f)).collect()),
72            Self::MapMsg { .. } | Self::Tick { .. } | Self::Stream { .. } | Self::WebSocket { .. } => {
73                Sub::None
74            }
75        }
76    }
77
78    pub fn batch(subs: impl IntoIterator<Item = Sub<M>>) -> Self {
79        let subs: Vec<_> = subs.into_iter().collect();
80        if subs.is_empty() {
81            Self::None
82        } else if subs.len() == 1 {
83            subs.into_iter().next().unwrap()
84        } else {
85            Self::Batch(subs)
86        }
87    }
88
89    pub fn id(&self) -> Option<u64> {
90        match self {
91            Self::None => None,
92            Self::Tick { id, .. } | Self::Stream { id, .. } | Self::WebSocket { id, .. } => {
93                Some(*id)
94            }
95            Self::MapMsg { inner, .. } => inner.id(),
96            Self::Batch(items) => items.first().and_then(Sub::id),
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    fn zero() -> i32 {
106        0
107    }
108
109    fn unit() {}
110
111    fn ping(_: ()) -> i32 {
112        7
113    }
114
115    #[test]
116    fn tick_has_stable_id() {
117        let sub = Sub::<i32>::tick(42, Duration::from_secs(1), zero);
118        assert_eq!(sub.id(), Some(42));
119    }
120
121    #[test]
122    fn map_msg_wraps_unit_tick() {
123        let sub = Sub::map_msg(Sub::tick(1, Duration::from_millis(1), unit), ping);
124        assert_eq!(sub.id(), Some(1));
125        let Sub::MapMsg { map, .. } = sub else {
126            panic!("expected map_msg");
127        };
128        assert_eq!(map(()), 7);
129    }
130}