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(¶ms_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}