Skip to main content

teaql_core/
eval.rs

1use serde::{Deserialize, Serialize};
2
3/// The load state metadata hidden inside an entity.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
5pub enum LoadState {
6    #[default]
7    NotLoaded,
8    Partial(std::collections::HashSet<String>),
9    /// Compact generated-entity representation. Known fields borrow their generated static
10    /// names; only genuinely dynamic projection aliases need to own a string.
11    PartialCompact(smallvec::SmallVec<[std::borrow::Cow<'static, str>; 8]>),
12    FullyLoaded,
13}
14
15impl LoadState {
16    pub fn is_loaded(&self, field_or_relation: &str) -> bool {
17        match self {
18            LoadState::NotLoaded => false,
19            LoadState::FullyLoaded => true,
20            LoadState::Partial(set) => set.contains(field_or_relation),
21            LoadState::PartialCompact(fields) => fields
22                .iter()
23                .any(|field| field.as_ref() == field_or_relation),
24        }
25    }
26}
27
28/// A wrapper type for Expression API evaluation results.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub enum EvalResult<T> {
31    /// Value is successfully loaded and present.
32    Value(T),
33    /// Value is loaded but it is legitimately Null.
34    Null,
35    /// Value is not loaded, trapping the evaluation path.
36    NotLoaded {
37        failed_node: String,
38        attempted_path: String,
39    },
40}
41
42impl<T> EvalResult<T> {
43    pub fn and_then<U, F: FnOnce(T) -> EvalResult<U>>(
44        self,
45        field_name: &str,
46        f: F,
47    ) -> EvalResult<U> {
48        match self {
49            EvalResult::Value(val) => match f(val) {
50                EvalResult::NotLoaded {
51                    failed_node,
52                    attempted_path,
53                } => {
54                    let new_path = match (attempted_path == field_name, attempted_path.is_empty()) {
55                        (true, _) => attempted_path,
56                        (_, true) => field_name.to_string(),
57                        _ => format!("{}.{}", field_name, attempted_path),
58                    };
59                    EvalResult::NotLoaded {
60                        failed_node,
61                        attempted_path: new_path,
62                    }
63                }
64                other => other,
65            },
66            EvalResult::Null => EvalResult::Null,
67            EvalResult::NotLoaded {
68                failed_node,
69                attempted_path,
70            } => EvalResult::NotLoaded {
71                failed_node,
72                attempted_path,
73            },
74        }
75    }
76
77    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> EvalResult<U> {
78        match self {
79            EvalResult::Value(val) => EvalResult::Value(f(val)),
80            EvalResult::Null => EvalResult::Null,
81            EvalResult::NotLoaded {
82                failed_node,
83                attempted_path,
84            } => EvalResult::NotLoaded {
85                failed_node,
86                attempted_path,
87            },
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    struct Company {
97        pub name: Option<String>,
98        pub __load_state: LoadState,
99    }
100
101    impl Company {
102        fn eval_name(&self) -> EvalResult<&str> {
103            if !self.__load_state.is_loaded("name") {
104                return EvalResult::NotLoaded {
105                    failed_node: "name".to_string(),
106                    attempted_path: "name".to_string(),
107                };
108            }
109            match &self.name {
110                Some(n) => EvalResult::Value(n.as_str()),
111                None => EvalResult::Null,
112            }
113        }
114    }
115
116    struct Platform {
117        pub company: Option<Box<Company>>,
118        pub __load_state: LoadState,
119    }
120
121    impl Platform {
122        fn eval_company(&self) -> EvalResult<&Company> {
123            if !self.__load_state.is_loaded("company") {
124                return EvalResult::NotLoaded {
125                    failed_node: "company".to_string(),
126                    attempted_path: "company".to_string(),
127                };
128            }
129            match &self.company {
130                Some(c) => EvalResult::Value(c.as_ref()),
131                None => EvalResult::Null,
132            }
133        }
134    }
135
136    struct User {
137        pub platform: Option<Box<Platform>>,
138        pub __load_state: LoadState,
139    }
140
141    impl User {
142        fn eval_platform(&self) -> EvalResult<&Platform> {
143            if !self.__load_state.is_loaded("platform") {
144                return EvalResult::NotLoaded {
145                    failed_node: "platform".to_string(),
146                    attempted_path: "platform".to_string(),
147                };
148            }
149            match &self.platform {
150                Some(p) => EvalResult::Value(p.as_ref()),
151                None => EvalResult::Null,
152            }
153        }
154    }
155
156    #[test]
157    fn test_eval_tracking_chain_perfect_path() {
158        // Build the mocked entity graph:
159        // User -> Platform -> Company
160        // But we simulate a logic bug: Company is NOT fully loaded, its "name" is missing!
161
162        let company = Company {
163            name: None,
164            // Company only partially loaded (doesn't include "name")
165            __load_state: LoadState::NotLoaded,
166        };
167
168        let platform = Platform {
169            company: Some(Box::new(company)),
170            // Platform is fully loaded
171            __load_state: LoadState::FullyLoaded,
172        };
173
174        let user = User {
175            platform: Some(Box::new(platform)),
176            // User is fully loaded
177            __load_state: LoadState::FullyLoaded,
178        };
179
180        // Let's evaluate the expression: user.platform.company.name
181        let result = user.eval_platform().and_then("platform", |p| {
182            p.eval_company().and_then("company", |c| c.eval_name())
183        });
184
185        // We expect it to fail exactly at "name" and bubble up the path!
186        match &result {
187            EvalResult::NotLoaded { attempted_path, .. } => {
188                assert_eq!(attempted_path, "platform.company.name");
189                println!("\n\n>>> 【系统捕获到未加载异常】 <<<\n{:#?}\n\n", result);
190            }
191            _ => panic!("Expected NotLoaded but got {:?}", result),
192        }
193    }
194
195    #[test]
196    fn test_eval_tracking_chain_middle_break() {
197        // If the platform exists, but company itself wasn't loaded
198        let platform = Platform {
199            company: None,                      // No data
200            __load_state: LoadState::NotLoaded, // Missing loaded state for company
201        };
202
203        let user = User {
204            platform: Some(Box::new(platform)),
205            __load_state: LoadState::FullyLoaded,
206        };
207
208        let result = user.eval_platform().and_then("platform", |p| {
209            p.eval_company().and_then("company", |c| c.eval_name())
210        });
211
212        match result {
213            EvalResult::NotLoaded { attempted_path, .. } => {
214                assert_eq!(attempted_path, "platform.company");
215                println!(
216                    "Success! Intercepted middle missing path: {}",
217                    attempted_path
218                );
219            }
220            _ => panic!("Expected NotLoaded"),
221        }
222    }
223
224    #[test]
225    fn test_eval_tracking_chain_normal_null() {
226        // If the platform exists, company is fully loaded, but its name is truly empty (NULL in DB)
227        let company = Company {
228            name: None, // Real database null
229            __load_state: LoadState::FullyLoaded,
230        };
231
232        let platform = Platform {
233            company: Some(Box::new(company)),
234            __load_state: LoadState::FullyLoaded,
235        };
236
237        let user = User {
238            platform: Some(Box::new(platform)),
239            __load_state: LoadState::FullyLoaded,
240        };
241
242        let result = user.eval_platform().and_then("platform", |p| {
243            p.eval_company().and_then("company", |c| c.eval_name())
244        });
245
246        match result {
247            EvalResult::Null => {
248                println!("Success! Legitimately empty (Null), not an error.");
249            }
250            _ => panic!("Expected Null"),
251        }
252    }
253}