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