query_flow/
loading.rs

1//! Loading state for async resource handling.
2
3use crate::QueryError;
4
5/// Represents the loading state of an async resource.
6///
7/// Use this as your `Query::Output` type when the query needs to load
8/// data asynchronously (e.g., file I/O, network requests).
9///
10/// # Example
11///
12/// ```ignore
13/// struct LoadFile { path: PathBuf }
14///
15/// impl Query for LoadFile {
16///     type CacheKey = PathBuf;
17///     type Output = LoadingState<String>;
18///
19///     fn query(&self, ctx: &mut QueryContext) -> Result<Self::Output, QueryError> {
20///         // Check if already loaded
21///         if let Some(content) = ctx.get_loaded(&self.path) {
22///             return Ok(LoadingState::Ready(content));
23///         }
24///
25///         // Spawn background loader
26///         ctx.spawn_loader(self.cache_key(), async {
27///             tokio::fs::read_to_string(&self.path).await.unwrap()
28///         });
29///
30///         Err(QueryError::Suspend)
31///     }
32/// }
33/// ```
34#[derive(Debug, Clone, PartialEq, Eq, Default)]
35pub enum LoadingState<T> {
36    /// Resource is still loading.
37    #[default]
38    Loading,
39    /// Resource is ready with the given value.
40    Ready(T),
41}
42
43impl<T> LoadingState<T> {
44    /// Check if the resource is still loading.
45    pub fn is_loading(&self) -> bool {
46        matches!(self, LoadingState::Loading)
47    }
48
49    /// Check if the resource is ready.
50    pub fn is_ready(&self) -> bool {
51        matches!(self, LoadingState::Ready(_))
52    }
53
54    /// Get the value if ready, None if loading.
55    pub fn get(&self) -> Option<&T> {
56        match self {
57            LoadingState::Loading => None,
58            LoadingState::Ready(t) => Some(t),
59        }
60    }
61
62    /// Get the value if ready, None if loading (consuming version).
63    pub fn into_inner(self) -> Option<T> {
64        match self {
65            LoadingState::Loading => None,
66            LoadingState::Ready(t) => Some(t),
67        }
68    }
69
70    /// Convert to Result - Loading becomes Err(QueryError::Suspend).
71    ///
72    /// Use this with the `?` operator to propagate loading state upward.
73    ///
74    /// # Example
75    ///
76    /// ```ignore
77    /// fn query(&self, ctx: &mut QueryContext) -> Result<MyOutput, QueryError> {
78    ///     let data = ctx.query(LoadData { id: self.id })?.suspend()?;
79    ///     // `data` is guaranteed to be ready here
80    ///     Ok(process(data))
81    /// }
82    /// ```
83    pub fn suspend(self) -> Result<T, QueryError> {
84        match self {
85            LoadingState::Loading => Err(QueryError::Suspend),
86            LoadingState::Ready(t) => Ok(t),
87        }
88    }
89
90    /// Reference version of suspend.
91    pub fn suspend_ref(&self) -> Result<&T, QueryError> {
92        match self {
93            LoadingState::Loading => Err(QueryError::Suspend),
94            LoadingState::Ready(t) => Ok(t),
95        }
96    }
97
98    /// Map the inner value if ready.
99    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> LoadingState<U> {
100        match self {
101            LoadingState::Loading => LoadingState::Loading,
102            LoadingState::Ready(t) => LoadingState::Ready(f(t)),
103        }
104    }
105
106    /// Flat map the inner value if ready.
107    pub fn and_then<U, F: FnOnce(T) -> LoadingState<U>>(self, f: F) -> LoadingState<U> {
108        match self {
109            LoadingState::Loading => LoadingState::Loading,
110            LoadingState::Ready(t) => f(t),
111        }
112    }
113}
114
115impl<T> From<T> for LoadingState<T> {
116    fn from(value: T) -> Self {
117        LoadingState::Ready(value)
118    }
119}
120
121impl<T> From<Option<T>> for LoadingState<T> {
122    fn from(opt: Option<T>) -> Self {
123        match opt {
124            Some(t) => LoadingState::Ready(t),
125            None => LoadingState::Loading,
126        }
127    }
128}