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