1use std::time::Instant;
8
9#[derive(Debug, Clone, Default)]
10pub enum Loadable<T> {
11 #[default]
12 NotAsked,
13 Loading,
14 Loaded(T, Instant),
15 Failed(String),
16}
17
18impl<T> Loadable<T> {
19 pub fn value(&self) -> Option<&T> {
20 match self {
21 Loadable::Loaded(v, _) => Some(v),
22 _ => None,
23 }
24 }
25
26 pub fn value_mut(&mut self) -> Option<&mut T> {
29 match self {
30 Loadable::Loaded(v, _) => Some(v),
31 _ => None,
32 }
33 }
34
35 pub fn is_loading(&self) -> bool {
36 matches!(self, Loadable::Loading)
37 }
38
39 pub fn error(&self) -> Option<&str> {
40 match self {
41 Loadable::Failed(e) => Some(e),
42 _ => None,
43 }
44 }
45
46 pub fn age(&self) -> Option<std::time::Duration> {
48 match self {
49 Loadable::Loaded(_, at) => Some(at.elapsed()),
50 _ => None,
51 }
52 }
53
54 pub fn loaded(value: T) -> Self {
55 Loadable::Loaded(value, Instant::now())
56 }
57
58 pub fn begin_refresh(&mut self) {
61 if !matches!(self, Loadable::Loaded(..)) {
62 *self = Loadable::Loading;
63 }
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn accessors_report_the_right_state() {
73 let n: Loadable<u8> = Loadable::NotAsked;
74 assert!(n.value().is_none() && n.error().is_none() && !n.is_loading());
75
76 let l = Loadable::loaded(7u8);
77 assert_eq!(l.value(), Some(&7));
78 assert!(l.age().is_some());
79
80 let f: Loadable<u8> = Loadable::Failed("boom".into());
81 assert_eq!(f.error(), Some("boom"));
82 }
83
84 #[test]
85 fn loaded_data_can_be_grown_in_place() {
86 let mut l = Loadable::loaded(vec![1u8]);
89 let at = l.age();
90 l.value_mut().unwrap().push(2);
91 assert_eq!(l.value(), Some(&vec![1, 2]));
92 assert!(at.is_some() && l.age().is_some());
93
94 let mut n: Loadable<Vec<u8>> = Loadable::NotAsked;
95 assert!(n.value_mut().is_none());
96 }
97
98 #[test]
99 fn refreshing_keeps_existing_data_on_screen() {
100 let mut l = Loadable::loaded(7u8);
101 l.begin_refresh();
102 assert_eq!(l.value(), Some(&7), "a refresh must not blank the view");
103
104 let mut e: Loadable<u8> = Loadable::Failed("boom".into());
105 e.begin_refresh();
106 assert!(e.is_loading());
107 }
108}