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