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