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