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}