Skip to main content

json_eval_rs/jsoneval/
evaluate.rs

1use super::JSONEval;
2use crate::jsoneval::json_parser;
3use crate::jsoneval::path_utils;
4use crate::jsoneval::table_evaluate;
5use crate::jsoneval::cancellation::CancellationToken;
6use crate::utils::clean_float_noise_scalar;
7use crate::time_block;
8
9use serde_json::Value;
10
11
12
13impl JSONEval {
14    /// Evaluate the schema with the given data and context.
15    ///
16    /// # Arguments
17    ///
18    /// * `data` - The data to evaluate.
19    /// * `context` - The context to evaluate.
20    ///
21    /// # Returns
22    ///
23    /// A `Result` indicating success or an error message.
24    pub fn evaluate(
25        &mut self,
26        data: &str,
27        context: Option<&str>,
28        paths: Option<&[String]>,
29        token: Option<&CancellationToken>,
30    ) -> Result<(), String> {
31        if let Some(t) = token {
32            if t.is_cancelled() {
33                return Err("Cancelled".to_string());
34            }
35        }
36        time_block!("evaluate() [total]", { 
37            // Use SIMD-accelerated JSON parsing
38            // Parse and update data/context
39            let data_value = time_block!("  parse data", { json_parser::parse_json_str(data)? });
40            let context_value = time_block!("  parse context", {
41                if let Some(ctx) = context {
42                    json_parser::parse_json_str(ctx)?
43                } else {
44                    Value::Object(serde_json::Map::new())
45                }
46            });
47            self.evaluate_internal_with_new_data(data_value, context_value, paths, token)
48        })
49    }
50
51    /// Internal helper to evaluate with all data/context provided as Values
52    fn evaluate_internal_with_new_data(
53        &mut self,
54        data: Value,
55        context: Value,
56        paths: Option<&[String]>,
57        token: Option<&CancellationToken>,
58    ) -> Result<(), String> {
59        time_block!("  evaluate_internal_with_new_data", {
60            // Store data, context and replace in eval_data (clone once instead of twice)
61            self.data = data.clone();
62            self.context = context.clone();
63            time_block!("  replace_data_and_context", {
64                self.eval_data.replace_data_and_context(data, context);
65            });
66
67            // Call internal evaluate (uses existing data if not provided)
68            self.evaluate_internal(paths, token)
69        })
70    }
71
72    /// Internal evaluate that can be called when data is already set
73    /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
74    pub(crate) fn evaluate_internal(&mut self, paths: Option<&[String]>, token: Option<&CancellationToken>) -> Result<(), String> {
75        if let Some(t) = token {
76            if t.is_cancelled() {
77                return Err("Cancelled".to_string());
78            }
79        }
80        time_block!("  evaluate_internal() [total]", {
81            // Acquire lock for synchronous execution
82            let _lock = self.eval_lock.lock().unwrap();
83
84 
85            // Normalize paths to schema pointers for correct filtering
86            let normalized_paths_storage; // Keep alive
87            let normalized_paths = if let Some(p_list) = paths {
88                normalized_paths_storage = p_list
89                    .iter()
90                    .flat_map(|p| {
91                        let normalized = if p.starts_with("#/") {
92                            // Case 1: JSON Schema path (e.g. #/properties/foo) - keep as is
93                            p.to_string()
94                        } else if p.starts_with('/') {
95                            // Case 2: Rust Pointer path (e.g. /properties/foo) - ensure # prefix
96                            format!("#{}", p)
97                        } else {
98                            // Case 3: Dot notation (e.g. properties.foo) - replace dots with slashes and add prefix
99                            format!("#/{}", p.replace('.', "/"))
100                        };
101
102                        vec![normalized]
103                    })
104                    .collect::<Vec<_>>();
105                Some(normalized_paths_storage.as_slice())
106            } else {
107                None
108            };
109
110            // Borrow sorted_evaluations via Arc (avoid deep-cloning Vec<Vec<String>>)
111            let eval_batches = self.sorted_evaluations.clone();
112            
113            // Track cache misses across batches to prevent false hits from large skipped arrays
114            // Use persisted missed_keys from JSONEval
115
116            // Process each batch - sequentially
117            // Batches are processed sequentially to maintain dependency order
118            // Process value evaluations (simple computed fields)
119            // These are independent of rule batches and should always run
120            let eval_data_values = self.eval_data.clone();
121            time_block!("      evaluate values", {
122                for eval_key in self.value_evaluations.iter() {
123                    if let Some(t) = token {
124                        if t.is_cancelled() {
125                            return Err("Cancelled".to_string());
126                        }
127                    }
128                    // Skip if has dependencies (will be handled in sorted batches)
129                    if let Some(deps) = self.dependencies.get(eval_key) {
130                        if !deps.is_empty() {
131                            continue;
132                        }
133                    }
134
135                    // Filter items if paths are provided
136                    if let Some(filter_paths) = normalized_paths {
137                        if !filter_paths.is_empty()
138                            && !filter_paths.iter().any(|p| {
139                                eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())
140                            })
141                        {
142                            continue;
143                        }
144                    }
145
146                    let pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
147
148                    // Cache miss - evaluate
149                    if let Some(logic_id) = self.evaluations.get(eval_key) {
150                        if let Ok(val) = self.engine.run(logic_id, eval_data_values.data()) {
151                             let cleaned_val = clean_float_noise_scalar(val);
152
153                             if let Some(pointer_value) =
154                                 self.evaluated_schema.pointer_mut(&pointer_path)
155                             {
156                                 *pointer_value = cleaned_val;
157                             }
158                        }
159                    }
160                }
161            });
162
163            time_block!("    process batches", {
164                for batch in eval_batches.iter() {
165                    if let Some(t) = token {
166                        if t.is_cancelled() {
167                            return Err("Cancelled".to_string());
168                        }
169                    }
170                    // Skip empty batches
171                    if batch.is_empty() {
172                        continue;
173                    }
174
175                    // Check if we can skip this entire batch optimization
176                    if let Some(filter_paths) = normalized_paths {
177                        if !filter_paths.is_empty() {
178                            let batch_has_match = batch.iter().any(|eval_key| {
179                                filter_paths.iter().any(|p| {
180                                    eval_key.starts_with(p.as_str())
181                                        || (p.starts_with(eval_key.as_str())
182                                            && !eval_key.contains("/$params/"))
183                                })
184                            });
185                            if !batch_has_match {
186                                continue;
187                            }
188                        }
189                    }
190
191                    // Sequential execution
192                    let eval_data_snapshot = self.eval_data.clone();
193
194                    for eval_key in batch {
195                        if let Some(t) = token {
196                            if t.is_cancelled() {
197                                return Err("Cancelled".to_string());
198                            }
199                        }
200                        // Filter individual items if paths are provided
201                        if let Some(filter_paths) = normalized_paths {
202                            if !filter_paths.is_empty()
203                                && !filter_paths.iter().any(|p| {
204                                    eval_key.starts_with(p.as_str())
205                                        || (p.starts_with(eval_key.as_str())
206                                            && !eval_key.contains("/$params/"))
207                                })
208                            {
209                                continue;
210                            }
211                        }
212
213                        let pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
214
215                        // Cache miss - evaluate
216                        let is_table = self.table_metadata.contains_key(eval_key);
217
218                        if is_table {
219                            if let Ok(rows) =
220                                table_evaluate::evaluate_table(self, eval_key, &eval_data_snapshot, token)
221                            {
222                                let value = Value::Array(rows);
223                                self.eval_data.set(&pointer_path, value.clone());
224                                if let Some(schema_value) =
225                                    self.evaluated_schema.pointer_mut(&pointer_path)
226                                {
227                                    *schema_value = value;
228                                }
229                            }
230                        } else {
231                            if let Some(logic_id) = self.evaluations.get(eval_key) {
232                                if let Ok(val) =
233                                    self.engine.run(logic_id, eval_data_snapshot.data())
234                                {
235                                    let cleaned_val = clean_float_noise_scalar(val);
236                                    self.eval_data.set(&pointer_path, cleaned_val.clone());
237                                    if let Some(schema_value) =
238                                        self.evaluated_schema.pointer_mut(&pointer_path)
239                                    {
240                                        *schema_value = cleaned_val;
241                                    }
242                                }
243                            }
244                        }
245                    }
246                }
247            });
248
249            // Drop lock before calling evaluate_others
250            drop(_lock);
251
252            self.evaluate_others(paths, token);
253
254            Ok(())
255        })
256    }
257
258    pub(crate) fn evaluate_others(&mut self, paths: Option<&[String]>, token: Option<&CancellationToken>) {
259        if let Some(t) = token {
260            if t.is_cancelled() {
261                return;
262            }
263        }
264        time_block!("    evaluate_others()", {
265            // Step 1: Evaluate "rules" and "others" categories with caching
266            // Rules are evaluated here so their values are available in evaluated_schema
267            let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
268            if combined_count > 0 {
269                time_block!("      evaluate rules+others", {
270                    let eval_data_snapshot = self.eval_data.clone();
271
272                    let normalized_paths: Option<Vec<String>> = paths.map(|p_list| {
273                        p_list
274                            .iter()
275                            .flat_map(|p| {
276                                let ptr = path_utils::dot_notation_to_schema_pointer(p);
277                                // Also support version with /properties/ prefix for root match
278                                let with_props = if ptr.starts_with("#/") {
279                                    format!("#/properties/{}", &ptr[2..])
280                                } else {
281                                    ptr.clone()
282                                };
283                                vec![ptr, with_props]
284                            })
285                            .collect()
286                    });
287
288                    // Sequential evaluation
289                    let combined_evals: Vec<&String> = self
290                        .rules_evaluations
291                        .iter()
292                        .chain(self.others_evaluations.iter())
293                        .collect();
294
295                    for eval_key in combined_evals {
296                        if let Some(t) = token {
297                            if t.is_cancelled() {
298                                return;
299                            }
300                        }
301                        // Filter items if paths are provided
302                        if let Some(filter_paths) = normalized_paths.as_ref() {
303                            if !filter_paths.is_empty()
304                                && !filter_paths.iter().any(|p| {
305                                    eval_key.starts_with(p.as_str())
306                                        || (p.starts_with(eval_key.as_str())
307                                            && !eval_key.contains("/$params/"))
308                                })
309                            {
310                                continue;
311                            }
312                        }
313
314                        let pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
315
316                        // Cache miss - evaluate
317                        if let Some(logic_id) = self.evaluations.get(eval_key) {
318                            if let Ok(val) =
319                                self.engine.run(logic_id, eval_data_snapshot.data())
320                            {
321                                let cleaned_val = clean_float_noise_scalar(val);
322
323                                if let Some(pointer_value) =
324                                    self.evaluated_schema.pointer_mut(&pointer_path)
325                                {
326                                    if !pointer_path.starts_with("$")
327                                        && pointer_path.contains("/rules/")
328                                        && !pointer_path.ends_with("/value")
329                                    {
330                                        match pointer_value.as_object_mut() {
331                                            Some(pointer_obj) => {
332                                                pointer_obj.remove("$evaluation");
333                                                pointer_obj
334                                                    .insert("value".to_string(), cleaned_val);
335                                            }
336                                            None => continue,
337                                        }
338                                    } else {
339                                        *pointer_value = cleaned_val;
340                                    }
341                                }
342                            }
343                        }
344                    }
345                });
346            }
347        });
348
349        // Step 2: Evaluate options URL templates (handles {variable} patterns)
350        time_block!("      evaluate_options_templates", {
351            self.evaluate_options_templates(paths);
352        });
353
354        // Step 3: Resolve layout logic (metadata injection, hidden propagation)
355        time_block!("      resolve_layout", {
356            let _ = self.resolve_layout(false);
357        });
358    }
359
360    /// Evaluate options URL templates (handles {variable} patterns)
361    fn evaluate_options_templates(&mut self, paths: Option<&[String]>) {
362        // Use pre-collected options templates from parsing (Arc clone is cheap)
363        let templates_to_eval = self.options_templates.clone();
364
365        // Evaluate each template
366        for (path, template_str, params_path) in templates_to_eval.iter() {
367            // Filter items if paths are provided
368            // 'path' here is the schema path to the field (dot notation or similar, need to check)
369            // It seems to be schema pointer based on usage in other methods
370            if let Some(filter_paths) = paths {
371                if !filter_paths.is_empty()
372                    && !filter_paths
373                        .iter()
374                        .any(|p| path.starts_with(p.as_str()) || p.starts_with(path.as_str()))
375                {
376                    continue;
377                }
378            }
379
380            if let Some(params) = self.evaluated_schema.pointer(&params_path) {
381                if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
382                    if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
383                        *target = Value::String(evaluated);
384                    }
385                }
386            }
387        }
388    }
389
390    /// Evaluate a template string like "api/users/{id}" with params
391    fn evaluate_template(&self, template: &str, params: &Value) -> Result<String, String> {
392        let mut result = template.to_string();
393
394        // Simple template evaluation: replace {key} with params.key
395        if let Value::Object(params_map) = params {
396            for (key, value) in params_map {
397                let placeholder = format!("{{{}}}", key);
398                if let Some(str_val) = value.as_str() {
399                    result = result.replace(&placeholder, str_val);
400                } else {
401                    // Convert non-string values to strings
402                    result = result.replace(&placeholder, &value.to_string());
403                }
404            }
405        }
406
407        Ok(result)
408    }
409}