Skip to main content

frus_shell/
remote.rs

1//! [`RemoteData`] — the Elm idiom for a value **loaded asynchronously**.
2//!
3//! A value coming from the network, or from any [`crate::Command`] effect, goes
4//! through four clearly distinct states. Blending them into an `Option<Result<T, E>>`
5//! — or worse, two `loading`/`error` booleans that can drift apart — is a classic
6//! source of bugs. `RemoteData` makes the states **exclusive** and forces the `view`
7//! to handle each one:
8//!
9//! ```ignore
10//! use frus::{Command, RemoteData, Request};
11//!
12//! struct App { user: RemoteData<User> }         // E defaults to String
13//!
14//! fn update(&mut self, msg: Msg) -> Command<Msg> {
15//!     match msg {
16//!         Msg::Load => {
17//!             self.user = RemoteData::Loading;
18//!             return Command::perform_async(async {
19//!                 let res = Request::get(URL).send().await;
20//!                 Msg::Loaded(res.map_err(|e| e.to_string()))
21//!             });
22//!         }
23//!         // An effect's `Result` becomes a `RemoteData` directly.
24//!         Msg::Loaded(res) => self.user = RemoteData::from_result(res),
25//!     }
26//!     Command::none()
27//! }
28//! ```
29//!
30//! In the `view` the four cases are **folded** over, and the compiler guarantees none
31//! is forgotten.
32
33/// The four states of an asynchronously loaded value.
34///
35/// `E`, the error type, defaults to `String` — the common case after
36/// `FetchError::to_string()`.
37#[derive(Debug, Clone, PartialEq, Eq, Default)]
38pub enum RemoteData<T, E = String> {
39    /// Nothing has been asked for yet; the initial state.
40    #[default]
41    NotAsked,
42    /// A request is in flight.
43    Loading,
44    /// The value has arrived.
45    Success(T),
46    /// The request failed.
47    Failure(E),
48}
49
50impl<T, E> RemoteData<T, E> {
51    /// Builds from an effect's `Result`: `Ok` → [`Success`](RemoteData::Success),
52    /// `Err` → [`Failure`](RemoteData::Failure). The natural bridge inside `update`.
53    pub fn from_result(res: Result<T, E>) -> Self {
54        match res {
55            Ok(value) => RemoteData::Success(value),
56            Err(err) => RemoteData::Failure(err),
57        }
58    }
59
60    /// Is a request in flight?
61    pub fn is_loading(&self) -> bool {
62        matches!(self, RemoteData::Loading)
63    }
64
65    /// Has the value arrived?
66    pub fn is_success(&self) -> bool {
67        matches!(self, RemoteData::Success(_))
68    }
69
70    /// Did the request fail?
71    pub fn is_failure(&self) -> bool {
72        matches!(self, RemoteData::Failure(_))
73    }
74
75    /// The value if it has arrived, `None` otherwise.
76    pub fn value(&self) -> Option<&T> {
77        match self {
78            RemoteData::Success(value) => Some(value),
79            _ => None,
80        }
81    }
82
83    /// The error if the request failed, `None` otherwise.
84    pub fn error(&self) -> Option<&E> {
85        match self {
86            RemoteData::Failure(err) => Some(err),
87            _ => None,
88        }
89    }
90
91    /// Borrows the inside: `RemoteData<T, E>` → `RemoteData<&T, &E>`, so a `view` can
92    /// fold over it without consuming the state.
93    pub fn as_ref(&self) -> RemoteData<&T, &E> {
94        match self {
95            RemoteData::NotAsked => RemoteData::NotAsked,
96            RemoteData::Loading => RemoteData::Loading,
97            RemoteData::Success(value) => RemoteData::Success(value),
98            RemoteData::Failure(err) => RemoteData::Failure(err),
99        }
100    }
101
102    /// Transforms the value on success, leaving the other states untouched — decoding
103    /// a JSON body into a domain type, say.
104    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> RemoteData<U, E> {
105        match self {
106            RemoteData::NotAsked => RemoteData::NotAsked,
107            RemoteData::Loading => RemoteData::Loading,
108            RemoteData::Success(value) => RemoteData::Success(f(value)),
109            RemoteData::Failure(err) => RemoteData::Failure(err),
110        }
111    }
112
113    /// Transforms the error on failure, leaving the other states untouched.
114    pub fn map_err<F>(self, f: impl FnOnce(E) -> F) -> RemoteData<T, F> {
115        match self {
116            RemoteData::NotAsked => RemoteData::NotAsked,
117            RemoteData::Loading => RemoteData::Loading,
118            RemoteData::Success(value) => RemoteData::Success(value),
119            RemoteData::Failure(err) => RemoteData::Failure(f(err)),
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn default_is_not_asked() {
130        let data: RemoteData<i32> = RemoteData::default();
131        assert_eq!(data, RemoteData::NotAsked);
132    }
133
134    #[test]
135    fn from_result_bridges_ok_and_err() {
136        let ok: RemoteData<i32> = RemoteData::from_result(Ok(7));
137        assert_eq!(ok, RemoteData::Success(7));
138        let err: RemoteData<i32> = RemoteData::from_result(Err("boom".to_string()));
139        assert_eq!(err, RemoteData::Failure("boom".to_string()));
140    }
141
142    #[test]
143    fn predicates_and_accessors() {
144        let ok: RemoteData<i32> = RemoteData::Success(42);
145        assert!(ok.is_success() && !ok.is_loading() && !ok.is_failure());
146        assert_eq!(ok.value(), Some(&42));
147        assert_eq!(ok.error(), None);
148
149        let err: RemoteData<i32> = RemoteData::Failure("nope".to_string());
150        assert!(err.is_failure());
151        assert_eq!(err.value(), None);
152        assert_eq!(err.error(), Some(&"nope".to_string()));
153
154        let loading: RemoteData<i32> = RemoteData::Loading;
155        assert!(loading.is_loading());
156    }
157
158    #[test]
159    fn map_only_touches_success() {
160        let ok: RemoteData<i32> = RemoteData::Success(3);
161        assert_eq!(ok.map(|n| n * 2), RemoteData::Success(6));
162
163        let loading: RemoteData<i32> = RemoteData::Loading;
164        assert_eq!(loading.map(|n| n * 2), RemoteData::Loading);
165
166        let err: RemoteData<i32> = RemoteData::Failure("e".to_string());
167        assert_eq!(err.map(|n| n * 2), RemoteData::Failure("e".to_string()));
168    }
169
170    #[test]
171    fn map_err_only_touches_failure() {
172        let err: RemoteData<i32, u16> = RemoteData::Failure(404);
173        let mapped: RemoteData<i32, String> = err.map_err(|c| format!("HTTP {c}"));
174        assert_eq!(mapped, RemoteData::Failure("HTTP 404".to_string()));
175    }
176
177    #[test]
178    fn as_ref_borrows_without_moving() {
179        let ok: RemoteData<String> = RemoteData::Success("hi".to_string());
180        assert_eq!(ok.as_ref().value().map(|s| s.as_str()), Some("hi"));
181        // `ok` is still usable afterwards.
182        assert!(ok.is_success());
183    }
184}