Skip to main content

tui_dispatch_core/
resource.rs

1//! DataResource: typed async data lifecycle
2//!
3//! A type representing the lifecycle of async-loaded data. Use this instead of
4//! scattering `loading: bool` and `error: Option<String>` across your state.
5//!
6//! # Example
7//!
8//! ```
9//! use tui_dispatch_core::DataResource;
10//!
11//! // In state
12//! struct AppState {
13//!     weather: DataResource<WeatherData>,
14//! }
15//!
16//! # #[derive(Clone)]
17//! # struct WeatherData;
18//!
19//! // In reducer
20//! # let mut state = AppState { weather: DataResource::Empty };
21//! # enum Action { FetchWeather, WeatherDidLoad(WeatherData), WeatherDidFail(String) }
22//! # let action = Action::FetchWeather;
23//! match action {
24//!     Action::FetchWeather => {
25//!         state.weather = DataResource::Loading;
26//!         // return effect to fetch
27//!     }
28//!     Action::WeatherDidLoad(data) => {
29//!         state.weather = DataResource::Loaded(data);
30//!     }
31//!     Action::WeatherDidFail(err) => {
32//!         state.weather = DataResource::Failed(err);
33//!     }
34//! }
35//!
36//! // In render
37//! match &state.weather {
38//!     DataResource::Empty => { /* show placeholder */ }
39//!     DataResource::Loading => { /* show spinner */ }
40//!     DataResource::Loaded(data) => { /* render data */ }
41//!     DataResource::Failed(err) => { /* show error */ }
42//! }
43//! ```
44
45use serde::{Deserialize, Serialize};
46
47/// Represents the lifecycle of async-loaded data.
48///
49/// This type captures the four states data can be in:
50/// - `Empty`: No data requested yet
51/// - `Loading`: Data is being fetched
52/// - `Loaded(T)`: Data successfully loaded
53/// - `Failed(String)`: Loading failed with error message
54#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
55#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
56pub enum DataResource<T> {
57    /// No data requested yet
58    #[default]
59    Empty,
60    /// Data is being fetched
61    Loading,
62    /// Data successfully loaded
63    Loaded(T),
64    /// Loading failed with error message
65    Failed(String),
66}
67
68impl<T> DataResource<T> {
69    /// Returns `true` if this is `Empty`.
70    pub fn is_empty(&self) -> bool {
71        matches!(self, Self::Empty)
72    }
73
74    /// Returns `true` if this is `Loading`.
75    pub fn is_loading(&self) -> bool {
76        matches!(self, Self::Loading)
77    }
78
79    /// Returns `true` if this is `Loaded(_)`.
80    pub fn is_loaded(&self) -> bool {
81        matches!(self, Self::Loaded(_))
82    }
83
84    /// Returns `true` if this is `Failed(_)`.
85    pub fn is_failed(&self) -> bool {
86        matches!(self, Self::Failed(_))
87    }
88
89    /// Returns a reference to the loaded data, or `None` if not loaded.
90    pub fn data(&self) -> Option<&T> {
91        match self {
92            Self::Loaded(t) => Some(t),
93            _ => None,
94        }
95    }
96
97    /// Returns a mutable reference to the loaded data, or `None` if not loaded.
98    pub fn data_mut(&mut self) -> Option<&mut T> {
99        match self {
100            Self::Loaded(t) => Some(t),
101            _ => None,
102        }
103    }
104
105    /// Returns the error message if failed, or `None` otherwise.
106    pub fn error(&self) -> Option<&str> {
107        match self {
108            Self::Failed(e) => Some(e),
109            _ => None,
110        }
111    }
112
113    /// Maps the loaded value using the provided function.
114    ///
115    /// If `Loaded(t)`, returns `Loaded(f(t))`. Otherwise returns the same state.
116    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> DataResource<U> {
117        match self {
118            Self::Empty => DataResource::Empty,
119            Self::Loading => DataResource::Loading,
120            Self::Loaded(t) => DataResource::Loaded(f(t)),
121            Self::Failed(e) => DataResource::Failed(e),
122        }
123    }
124
125    /// Maps a reference to the loaded value.
126    pub fn map_ref<U>(&self, f: impl FnOnce(&T) -> U) -> DataResource<U> {
127        match self {
128            Self::Empty => DataResource::Empty,
129            Self::Loading => DataResource::Loading,
130            Self::Loaded(t) => DataResource::Loaded(f(t)),
131            Self::Failed(e) => DataResource::Failed(e.clone()),
132        }
133    }
134
135    /// Applies a function that returns a `DataResource` to the loaded value.
136    ///
137    /// Useful for chaining dependent async operations.
138    pub fn and_then<U>(self, f: impl FnOnce(T) -> DataResource<U>) -> DataResource<U> {
139        match self {
140            Self::Empty => DataResource::Empty,
141            Self::Loading => DataResource::Loading,
142            Self::Loaded(t) => f(t),
143            Self::Failed(e) => DataResource::Failed(e),
144        }
145    }
146
147    /// Returns the loaded value or a default.
148    pub fn unwrap_or(self, default: T) -> T {
149        match self {
150            Self::Loaded(t) => t,
151            _ => default,
152        }
153    }
154
155    /// Returns the loaded value or computes a default.
156    pub fn unwrap_or_else(self, f: impl FnOnce() -> T) -> T {
157        match self {
158            Self::Loaded(t) => t,
159            _ => f(),
160        }
161    }
162
163    /// Converts from `&DataResource<T>` to `DataResource<&T>`.
164    pub fn as_ref(&self) -> DataResource<&T> {
165        match self {
166            Self::Empty => DataResource::Empty,
167            Self::Loading => DataResource::Loading,
168            Self::Loaded(t) => DataResource::Loaded(t),
169            Self::Failed(e) => DataResource::Failed(e.clone()),
170        }
171    }
172
173    /// Returns `true` if there's either data or an error (not empty or loading).
174    pub fn is_settled(&self) -> bool {
175        matches!(self, Self::Loaded(_) | Self::Failed(_))
176    }
177
178    /// Returns `true` if there's no data yet (empty or loading).
179    pub fn is_pending(&self) -> bool {
180        matches!(self, Self::Empty | Self::Loading)
181    }
182
183    /// Transitions to `Loading` state. Returns `true` if state actually changed.
184    ///
185    /// Useful in reducers to start a fetch:
186    /// ```
187    /// # use tui_dispatch_core::DataResource;
188    /// # let mut resource: DataResource<String> = DataResource::Empty;
189    /// if resource.start_loading() {
190    ///     // dispatch effect to fetch
191    /// }
192    /// ```
193    pub fn start_loading(&mut self) -> bool
194    where
195        T: Clone,
196    {
197        if self.is_loading() {
198            false
199        } else {
200            *self = Self::Loading;
201            true
202        }
203    }
204}
205
206impl<T: Clone> DataResource<T> {
207    /// Returns a clone of the loaded data, or `None` if not loaded.
208    pub fn cloned(&self) -> Option<T> {
209        self.data().cloned()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_default_is_empty() {
219        let resource: DataResource<String> = DataResource::default();
220        assert!(resource.is_empty());
221    }
222
223    #[test]
224    fn test_state_checks() {
225        let empty: DataResource<i32> = DataResource::Empty;
226        let loading: DataResource<i32> = DataResource::Loading;
227        let loaded: DataResource<i32> = DataResource::Loaded(42);
228        let failed: DataResource<i32> = DataResource::Failed("oops".to_string());
229
230        assert!(empty.is_empty());
231        assert!(!empty.is_loading());
232        assert!(empty.is_pending());
233        assert!(!empty.is_settled());
234
235        assert!(!loading.is_empty());
236        assert!(loading.is_loading());
237        assert!(loading.is_pending());
238        assert!(!loading.is_settled());
239
240        assert!(!loaded.is_empty());
241        assert!(!loaded.is_loading());
242        assert!(loaded.is_loaded());
243        assert!(!loaded.is_pending());
244        assert!(loaded.is_settled());
245
246        assert!(!failed.is_empty());
247        assert!(failed.is_failed());
248        assert!(!failed.is_pending());
249        assert!(failed.is_settled());
250    }
251
252    #[test]
253    fn test_data_accessors() {
254        let loaded: DataResource<i32> = DataResource::Loaded(42);
255        let failed: DataResource<i32> = DataResource::Failed("error".to_string());
256
257        assert_eq!(loaded.data(), Some(&42));
258        assert_eq!(failed.data(), None);
259        assert_eq!(failed.error(), Some("error"));
260        assert_eq!(loaded.error(), None);
261    }
262
263    #[test]
264    fn test_map() {
265        let loaded: DataResource<i32> = DataResource::Loaded(21);
266        let doubled = loaded.map(|x| x * 2);
267        assert_eq!(doubled.data(), Some(&42));
268
269        let loading: DataResource<i32> = DataResource::Loading;
270        let still_loading: DataResource<i32> = loading.map(|x| x * 2);
271        assert!(still_loading.is_loading());
272    }
273
274    #[test]
275    fn test_and_then() {
276        let loaded: DataResource<i32> = DataResource::Loaded(42);
277        let chained = loaded.and_then(|x| DataResource::Loaded(x.to_string()));
278        assert_eq!(chained.data(), Some(&"42".to_string()));
279
280        let failed: DataResource<i32> = DataResource::Failed("err".to_string());
281        let still_failed: DataResource<String> =
282            failed.and_then(|x| DataResource::Loaded(x.to_string()));
283        assert!(still_failed.is_failed());
284    }
285
286    #[test]
287    fn test_unwrap_or() {
288        let loaded: DataResource<i32> = DataResource::Loaded(42);
289        let empty: DataResource<i32> = DataResource::Empty;
290
291        assert_eq!(loaded.unwrap_or(0), 42);
292        assert_eq!(empty.unwrap_or(0), 0);
293    }
294
295    #[test]
296    fn test_start_loading() {
297        let mut resource: DataResource<i32> = DataResource::Empty;
298        assert!(resource.start_loading());
299        assert!(resource.is_loading());
300
301        // Already loading, should return false
302        assert!(!resource.start_loading());
303        assert!(resource.is_loading());
304    }
305
306    #[test]
307    fn test_serialize_deserialize() {
308        let loaded: DataResource<i32> = DataResource::Loaded(42);
309        let json = serde_json::to_string(&loaded).unwrap();
310        let restored: DataResource<i32> = serde_json::from_str(&json).unwrap();
311        assert_eq!(loaded, restored);
312
313        let failed: DataResource<i32> = DataResource::Failed("oops".to_string());
314        let json = serde_json::to_string(&failed).unwrap();
315        let restored: DataResource<i32> = serde_json::from_str(&json).unwrap();
316        assert_eq!(failed, restored);
317    }
318}