json_eval_rs/jsoneval/
dependents.rs

1use super::JSONEval;
2use crate::jsoneval::json_parser;
3use crate::jsoneval::path_utils;
4use crate::rlogic::{LogicId, RLogic};
5use crate::jsoneval::types::DependentItem;
6use crate::utils::clean_float_noise;
7use crate::EvalData;
8
9use indexmap::{IndexMap, IndexSet};
10use serde_json::Value;
11
12
13impl JSONEval {
14    /// Evaluate fields that depend on a changed path
15    /// This processes all dependent fields transitively when a source field changes
16    pub fn evaluate_dependents(
17        &mut self,
18        changed_paths: &[String],
19        data: Option<&str>,
20        context: Option<&str>,
21        re_evaluate: bool,
22    ) -> Result<Value, String> {
23        // Acquire lock for synchronous execution
24        let _lock = self.eval_lock.lock().unwrap();
25
26        // Update data if provided
27        if let Some(data_str) = data {
28            // Save old data for comparison
29            let old_data = self.eval_data.clone_data_without(&["$params"]);
30
31            let data_value = json_parser::parse_json_str(data_str)?;
32            let context_value = if let Some(ctx) = context {
33                json_parser::parse_json_str(ctx)?
34            } else {
35                Value::Object(serde_json::Map::new())
36            };
37            self.eval_data
38                .replace_data_and_context(data_value.clone(), context_value);
39
40            // Selectively purge cache entries that depend on changed data
41            // Only purge if values actually changed
42            // Convert changed_paths to data pointer format for cache purging
43            let data_paths: Vec<String> = changed_paths
44                .iter()
45                .map(|path| {
46                    // Robust normalization: normalize to schema pointer first, then strip schema-specific parts
47                    // This handles both "illustration.insured.name" and "#/illustration/properties/insured/properties/name"
48                    let schema_ptr = path_utils::dot_notation_to_schema_pointer(path);
49
50                    // Remove # prefix and /properties/ segments to get pure data location
51                    let normalized = schema_ptr
52                        .trim_start_matches('#')
53                        .replace("/properties/", "/");
54
55                    // Ensure it starts with / for data pointer
56                    if normalized.starts_with('/') {
57                        normalized
58                    } else {
59                        format!("/{}", normalized)
60                    }
61                })
62                .collect();
63            self.purge_cache_for_changed_data_with_comparison(&data_paths, &old_data, &data_value);
64        }
65
66        let mut result = Vec::new();
67        let mut processed = IndexSet::new();
68
69        // Normalize all changed paths and add to processing queue
70        // Converts: "illustration.insured.name" -> "#/illustration/properties/insured/properties/name"
71        let mut to_process: Vec<(String, bool)> = changed_paths
72            .iter()
73            .map(|path| (path_utils::dot_notation_to_schema_pointer(path), false))
74            .collect(); // (path, is_transitive)
75
76        // Process dependents recursively (always nested/transitive)
77        Self::process_dependents_queue(
78            &self.engine,
79            &self.evaluations,
80            &mut self.eval_data,
81            &self.dependents_evaluations,
82            &self.evaluated_schema,
83            &mut to_process,
84            &mut processed,
85            &mut result
86        )?;
87
88        // If re_evaluate is true, perform full evaluation with the mutated eval_data
89        // Then perform post-evaluation checks (ReadOnly, Hidden)
90        if re_evaluate {
91            // Drop lock for evaluate_internal
92            drop(_lock); 
93            self.evaluate_internal(None)?;
94            
95            // Re-acquire lock for ReadOnly/Hidden processing
96            let _lock = self.eval_lock.lock().unwrap();
97
98            // 1. Read-Only Pass
99            // Collect read-only fields - include ALL readonly values in the result
100            let mut readonly_changes = Vec::new();
101            let mut readonly_values = Vec::new();  // Track all readonly values (including unchanged)
102            
103            // OPTIMIZATION: Use conditional_readonly_fields cache instead of recursing whole schema
104            // self.collect_readonly_fixes(&self.evaluated_schema, "#", &mut readonly_changes);
105            for path in self.conditional_readonly_fields.iter() {
106                let normalized = path_utils::normalize_to_json_pointer(path);
107                if let Some(schema_element) = self.evaluated_schema.pointer(&normalized) {
108                    self.check_readonly_for_dependents(schema_element, path, &mut readonly_changes, &mut readonly_values);
109                }
110            }
111
112            // Apply fixes for changed values and add to queue
113            for (path, schema_value) in readonly_changes {
114                // Set data to match schema value
115                let data_path = path_utils::normalize_to_json_pointer(&path)
116                    .replace("/properties/", "/")
117                    .trim_start_matches('#')
118                    .to_string();
119                
120                self.eval_data.set(&data_path, schema_value.clone());
121                
122                // Add to process queue for changed values
123                to_process.push((path, true));
124            }
125            
126            // Add ALL readonly values to result (both changed and unchanged)
127            for (path, schema_value) in readonly_values {
128                let data_path = path_utils::normalize_to_json_pointer(&path)
129                    .replace("/properties/", "/")
130                    .trim_start_matches('#')
131                    .to_string();
132                
133                let mut change_obj = serde_json::Map::new();
134                change_obj.insert("$ref".to_string(), Value::String(path_utils::pointer_to_dot_notation(&data_path)));
135                change_obj.insert("$readonly".to_string(), Value::Bool(true));
136                change_obj.insert("value".to_string(), schema_value);
137                
138                result.push(Value::Object(change_obj));
139            }
140            
141            // Refund process queue for ReadOnly effects
142            if !to_process.is_empty() {
143                 Self::process_dependents_queue(
144                    &self.engine,
145                    &self.evaluations,
146                    &mut self.eval_data,
147                    &self.dependents_evaluations,
148                    &self.evaluated_schema,
149                    &mut to_process,
150                    &mut processed,
151                    &mut result
152                )?;
153            }
154
155            // 2. Recursive Hide Pass
156            // Collect hidden fields that have values
157            let mut hidden_fields = Vec::new();
158            // OPTIMIZATION: Use conditional_hidden_fields cache instead of recursing whole schema
159            // self.collect_hidden_fields(&self.evaluated_schema, "#", &mut hidden_fields);
160            for path in self.conditional_hidden_fields.iter() {
161                let normalized = path_utils::normalize_to_json_pointer(path);
162                 if let Some(schema_element) = self.evaluated_schema.pointer(&normalized) {
163                    self.check_hidden_field(schema_element, path, &mut hidden_fields);
164                }
165            }
166            
167            // Logic for recursive hiding (using reffed_by)
168            if !hidden_fields.is_empty() {
169                Self::recursive_hide_effect(
170                    &self.engine,
171                    &self.evaluations,
172                    &self.reffed_by,
173                    &mut self.eval_data,
174                    hidden_fields, 
175                    &mut to_process, 
176                    &mut result
177                );
178            }
179            
180            // Process queue for Hidden effects
181             if !to_process.is_empty() {
182                 Self::process_dependents_queue(
183                    &self.engine,
184                    &self.evaluations,
185                    &mut self.eval_data,
186                    &self.dependents_evaluations,
187                    &self.evaluated_schema,
188                    &mut to_process,
189                    &mut processed,
190                    &mut result
191                )?;
192            }
193        }
194
195        Ok(Value::Array(result))
196    }
197
198    /// Helper to evaluate a dependent value - uses pre-compiled eval keys for fast lookup
199    pub(crate) fn evaluate_dependent_value_static(
200        engine: &RLogic,
201        evaluations: &IndexMap<String, LogicId>,
202        eval_data: &EvalData,
203        value: &Value,
204        changed_field_value: &Value,
205        changed_field_ref_value: &Value,
206    ) -> Result<Value, String> {
207        match value {
208            // If it's a String, check if it's an eval key reference
209            Value::String(eval_key) => {
210                if let Some(logic_id) = evaluations.get(eval_key) {
211                    // It's a pre-compiled evaluation - run it with scoped context
212                    // Create internal context with $value and $refValue
213                    let mut internal_context = serde_json::Map::new();
214                    internal_context.insert("$value".to_string(), changed_field_value.clone());
215                    internal_context.insert("$refValue".to_string(), changed_field_ref_value.clone());
216                    let context_value = Value::Object(internal_context);
217
218                    let result = engine.run_with_context(logic_id, eval_data.data(), &context_value)
219                        .map_err(|e| format!("Failed to evaluate dependent logic '{}': {}", eval_key, e))?;
220                    Ok(result)
221                } else {
222                    // It's a regular string value
223                    Ok(value.clone())
224                }
225            }
226            // For backwards compatibility: compile $evaluation on-the-fly
227            // This shouldn't happen with properly parsed schemas
228            Value::Object(map) if map.contains_key("$evaluation") => {
229                Err("Dependent evaluation contains unparsed $evaluation - schema was not properly parsed".to_string())
230            }
231            // Primitive value - return as-is
232            _ => Ok(value.clone()),
233        }
234    }
235
236    /// Check if a single field is readonly and populate vectors for both changes and all values
237    pub(crate) fn check_readonly_for_dependents(
238        &self,
239        schema_element: &Value,
240        path: &str,
241        changes: &mut Vec<(String, Value)>,
242        all_values: &mut Vec<(String, Value)>,
243    ) {
244        match schema_element {
245            Value::Object(map) => {
246                // Check if field is disabled (ReadOnly)
247                let mut is_disabled = false;
248                if let Some(Value::Object(condition)) = map.get("condition") {
249                    if let Some(Value::Bool(d)) = condition.get("disabled") {
250                        is_disabled = *d;
251                    }
252                }
253
254                // Check skipReadOnlyValue config
255                 let mut skip_readonly = false;
256                if let Some(Value::Object(config)) = map.get("config") {
257                    if let Some(Value::Object(all)) = config.get("all") {
258                         if let Some(Value::Bool(skip)) = all.get("skipReadOnlyValue") {
259                             skip_readonly = *skip;
260                         }
261                    }
262                }
263
264                if is_disabled && !skip_readonly {
265                    if let Some(schema_value) = map.get("value") {
266                         let data_path = path_utils::normalize_to_json_pointer(path)
267                            .replace("/properties/", "/")
268                            .trim_start_matches('#')
269                            .to_string();
270                         
271                         let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
272                         
273                         // Add to all_values (include in dependents result regardless of change)
274                         all_values.push((path.to_string(), schema_value.clone()));
275                         
276                         // Only add to changes if value doesn't match
277                         if current_data != schema_value {
278                             changes.push((path.to_string(), schema_value.clone()));
279                         }
280                    }
281                }
282            }
283            _ => {}
284        }
285    }
286    
287    /// Recursively collect read-only fields that need updates (Legacy/Full-Scan)
288    #[allow(dead_code)]
289    pub(crate) fn collect_readonly_fixes(
290        &self,
291        schema_element: &Value,
292        path: &str,
293        changes: &mut Vec<(String, Value)>,
294    ) {
295        match schema_element {
296            Value::Object(map) => {
297                // Check if field is disabled (ReadOnly)
298                let mut is_disabled = false;
299                if let Some(Value::Object(condition)) = map.get("condition") {
300                    if let Some(Value::Bool(d)) = condition.get("disabled") {
301                        is_disabled = *d;
302                    }
303                }
304
305                // Check skipReadOnlyValue config
306                 let mut skip_readonly = false;
307                if let Some(Value::Object(config)) = map.get("config") {
308                    if let Some(Value::Object(all)) = config.get("all") {
309                         if let Some(Value::Bool(skip)) = all.get("skipReadOnlyValue") {
310                             skip_readonly = *skip;
311                         }
312                    }
313                }
314
315                if is_disabled && !skip_readonly {
316                    // Check if it's a value field (has "value" property or implicit via path?)
317                    // In JS: "const readOnlyValues = this.getSchemaValues();"
318                    // We only care if data != schema value
319                    if let Some(schema_value) = map.get("value") {
320                         let data_path = path_utils::normalize_to_json_pointer(path)
321                            .replace("/properties/", "/")
322                            .trim_start_matches('#')
323                            .to_string();
324                         
325                         let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
326                         
327                         if current_data != schema_value {
328                             changes.push((path.to_string(), schema_value.clone()));
329                         }
330                    }
331                }
332
333                // Recurse into properties
334                 if let Some(Value::Object(props)) = map.get("properties") {
335                    for (key, val) in props {
336                        let next_path = if path == "#" {
337                            format!("#/properties/{}", key)
338                        } else {
339                            format!("{}/properties/{}", path, key)
340                        };
341                        self.collect_readonly_fixes(val, &next_path, changes);
342                    }
343                }
344            }
345            _ => {}
346        }
347    }
348
349    /// Check if a single field is hidden and needs clearing (Optimized non-recursive)
350    pub(crate) fn check_hidden_field(
351        &self,
352        schema_element: &Value,
353        path: &str,
354        hidden_fields: &mut Vec<String>,
355    ) {
356         match schema_element {
357            Value::Object(map) => {
358                 // Check if field is hidden
359                let mut is_hidden = false;
360                if let Some(Value::Object(condition)) = map.get("condition") {
361                    if let Some(Value::Bool(h)) = condition.get("hidden") {
362                        is_hidden = *h;
363                    }
364                }
365
366                 // Check keepHiddenValue config
367                 let mut keep_hidden = false;
368                if let Some(Value::Object(config)) = map.get("config") {
369                    if let Some(Value::Object(all)) = config.get("all") {
370                         if let Some(Value::Bool(keep)) = all.get("keepHiddenValue") {
371                             keep_hidden = *keep;
372                         }
373                    }
374                }
375
376                if is_hidden && !keep_hidden {
377                     let data_path = path_utils::normalize_to_json_pointer(path)
378                        .replace("/properties/", "/")
379                        .trim_start_matches('#')
380                        .to_string();
381
382                     let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
383                     
384                     // If hidden and has non-empty value, add to list
385                     if current_data != &Value::Null && current_data != "" {
386                         hidden_fields.push(path.to_string());
387                     }
388                }
389            }
390             _ => {}
391         }
392    }
393
394    /// Recursively collect hidden fields that have values (candidates for clearing) (Legacy/Full-Scan)
395    #[allow(dead_code)]
396    pub(crate) fn collect_hidden_fields(
397        &self,
398        schema_element: &Value,
399        path: &str,
400        hidden_fields: &mut Vec<String>,
401    ) {
402         match schema_element {
403            Value::Object(map) => {
404                 // Check if field is hidden
405                let mut is_hidden = false;
406                if let Some(Value::Object(condition)) = map.get("condition") {
407                    if let Some(Value::Bool(h)) = condition.get("hidden") {
408                        is_hidden = *h;
409                    }
410                }
411
412                 // Check keepHiddenValue config
413                 let mut keep_hidden = false;
414                if let Some(Value::Object(config)) = map.get("config") {
415                    if let Some(Value::Object(all)) = config.get("all") {
416                         if let Some(Value::Bool(keep)) = all.get("keepHiddenValue") {
417                             keep_hidden = *keep;
418                         }
419                    }
420                }
421                
422                if is_hidden && !keep_hidden {
423                     let data_path = path_utils::normalize_to_json_pointer(path)
424                        .replace("/properties/", "/")
425                        .trim_start_matches('#')
426                        .to_string();
427
428                     let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
429                     
430                     // If hidden and has non-empty value, add to list
431                     if current_data != &Value::Null && current_data != "" {
432                         hidden_fields.push(path.to_string());
433                     }
434                }
435
436                // Recurse into children
437                for (key, val) in map {
438                    if key == "properties" {
439                        if let Value::Object(props) = val {
440                            for (p_key, p_val) in props {
441                                let next_path = if path == "#" {
442                                    format!("#/properties/{}", p_key)
443                                } else {
444                                    format!("{}/properties/{}", path, p_key)
445                                };
446                                self.collect_hidden_fields(p_val, &next_path, hidden_fields);
447                            }
448                        }
449                    } else if let Value::Object(_) = val {
450                        // Skip known metadata keys and explicitly handled keys
451                        if key == "condition" 
452                            || key == "config" 
453                            || key == "rules" 
454                            || key == "dependents" 
455                            || key == "hideLayout" 
456                            || key == "$layout" 
457                            || key == "$params" 
458                            || key == "definitions"
459                            || key == "$defs"
460                            || key.starts_with('$') 
461                        {
462                            continue;
463                        }
464                        
465                         let next_path = if path == "#" {
466                            format!("#/{}", key)
467                        } else {
468                            format!("{}/{}", path, key)
469                        };
470                        self.collect_hidden_fields(val, &next_path, hidden_fields);
471                    }
472                }
473            }
474            _ => {}
475        }
476    }
477
478    /// Perform recursive hiding effect using reffed_by graph
479    pub(crate) fn recursive_hide_effect(
480        engine: &RLogic,
481        evaluations: &IndexMap<String, LogicId>,
482        reffed_by: &IndexMap<String, Vec<String>>,
483        eval_data: &mut EvalData,
484        mut hidden_fields: Vec<String>,
485        queue: &mut Vec<(String, bool)>, 
486        result: &mut Vec<Value>
487    ) {
488        while let Some(hf) = hidden_fields.pop() {
489            let data_path = path_utils::normalize_to_json_pointer(&hf)
490                .replace("/properties/", "/")
491                .trim_start_matches('#')
492                .to_string();
493            
494            // clear data
495            eval_data.set(&data_path, Value::Null);
496            
497             // Create dependent object for result
498            let mut change_obj = serde_json::Map::new();
499            change_obj.insert("$ref".to_string(), Value::String(path_utils::pointer_to_dot_notation(&data_path)));
500            change_obj.insert("$hidden".to_string(), Value::Bool(true));
501            change_obj.insert("clear".to_string(), Value::Bool(true));
502            result.push(Value::Object(change_obj));
503            
504            // Add to queue for standard dependent processing
505            queue.push((hf.clone(), true));
506
507            // Check reffed_by to find other fields that might become hidden
508            if let Some(referencing_fields) = reffed_by.get(&data_path) {
509                for rb in referencing_fields {
510                    // Evaluate condition.hidden for rb
511                    // We need a way to run specific evaluation?
512                    // We can check if rb has a hidden evaluation in self.evaluations
513                    let hidden_eval_key = format!("{}/condition/hidden", rb);
514                    
515                    if let Some(logic_id) = evaluations.get(&hidden_eval_key) {
516                        // Run evaluation
517                        // Context: $value = current field (rb) value? No, $value usually refers to changed field in deps.
518                        // But here we are just re-evaluating the rule.
519                        // In JS logic: "const result = hiddenFn(runnerCtx);"
520                        // runnerCtx has the updated data (we just set hf to null).
521                        
522                         let rb_data_path = path_utils::normalize_to_json_pointer(rb)
523                                .replace("/properties/", "/")
524                                .trim_start_matches('#')
525                                .to_string();
526                         let rb_value = eval_data.data().pointer(&rb_data_path).cloned().unwrap_or(Value::Null);
527                         
528                         // We can use engine.run w/ eval_data
529                         if let Ok(Value::Bool(is_hidden)) = engine.run(
530                             logic_id, 
531                             eval_data.data()
532                         ) {
533                             if is_hidden {
534                                 // Check if rb is not already in hidden_fields and has value
535                                 // rb is &String, hidden_fields is Vec<String>
536                                 if !hidden_fields.contains(rb) {
537                                     let has_value = rb_value != Value::Null && rb_value != "";
538                                     if has_value {
539                                          hidden_fields.push(rb.clone());
540                                     }
541                                 }
542                             }
543                         }
544                    }
545                }
546            }
547        }
548    }
549
550    /// Process the dependents queue
551    /// This handles the transitive propagation of changes based on the dependents graph
552    pub(crate) fn process_dependents_queue(
553        engine: &RLogic,
554        evaluations: &IndexMap<String, LogicId>,
555        eval_data: &mut EvalData,
556        dependents_evaluations: &IndexMap<String, Vec<DependentItem>>,
557        evaluated_schema: &Value,
558        queue: &mut Vec<(String, bool)>,
559        processed: &mut IndexSet<String>,
560        result: &mut Vec<Value>,
561    ) -> Result<(), String> {
562        while let Some((current_path, is_transitive)) = queue.pop() {
563            if processed.contains(&current_path) {
564                continue;
565            }
566            processed.insert(current_path.clone());
567
568            // Get the value of the changed field for $value context
569            let current_data_path = path_utils::normalize_to_json_pointer(&current_path)
570                .replace("/properties/", "/")
571                .trim_start_matches('#')
572                .to_string();
573            let mut current_value = eval_data
574                .data()
575                .pointer(&current_data_path)
576                .cloned()
577                .unwrap_or(Value::Null);
578
579            // Find dependents for this path
580            if let Some(dependent_items) = dependents_evaluations.get(&current_path) {
581                for dep_item in dependent_items {
582                    let ref_path = &dep_item.ref_path;
583                    let pointer_path = path_utils::normalize_to_json_pointer(ref_path);
584                    // Data paths don't include /properties/, strip it for data access
585                    let data_path = pointer_path.replace("/properties/", "/");
586
587                    let current_ref_value = eval_data
588                        .data()
589                        .pointer(&data_path)
590                        .cloned()
591                        .unwrap_or(Value::Null);
592
593                    // Get field and parent field from schema
594                    let field = evaluated_schema.pointer(&pointer_path).cloned();
595
596                    // Get parent field - skip /properties/ to get actual parent object
597                    let parent_path = if let Some(last_slash) = pointer_path.rfind("/properties") {
598                        &pointer_path[..last_slash]
599                    } else {
600                        "/"
601                    };
602                    let mut parent_field = if parent_path.is_empty() || parent_path == "/" {
603                        evaluated_schema.clone()
604                    } else {
605                        evaluated_schema
606                            .pointer(parent_path)
607                            .cloned()
608                            .unwrap_or_else(|| Value::Object(serde_json::Map::new()))
609                    };
610
611                    // omit properties to minimize size of parent field
612                    if let Value::Object(ref mut map) = parent_field {
613                        map.remove("properties");
614                        map.remove("$layout");
615                    }
616
617                    let mut change_obj = serde_json::Map::new();
618                    change_obj.insert(
619                        "$ref".to_string(),
620                        Value::String(path_utils::pointer_to_dot_notation(&data_path)),
621                    );
622                    if let Some(f) = field {
623                        change_obj.insert("$field".to_string(), f);
624                    }
625                    change_obj.insert("$parentField".to_string(), parent_field);
626                    change_obj.insert("transitive".to_string(), Value::Bool(is_transitive));
627
628                    let mut add_transitive = false;
629                    let mut add_deps = false;
630                    // Process clear
631                    if let Some(clear_val) = &dep_item.clear {
632                        let clear_val_clone = clear_val.clone();
633                        let should_clear = Self::evaluate_dependent_value_static(
634                            engine,
635                            evaluations,
636                            eval_data,
637                            &clear_val_clone,
638                            &current_value,
639                            &current_ref_value,
640                        )?;
641                        let clear_bool = match should_clear {
642                            Value::Bool(b) => b,
643                            _ => false,
644                        };
645
646                        if clear_bool {
647                            // Clear the field
648                            if data_path == current_data_path {
649                                current_value = Value::Null;
650                            }
651                            eval_data.set(&data_path, Value::Null);
652                            change_obj.insert("clear".to_string(), Value::Bool(true));
653                            add_transitive = true;
654                            add_deps = true;
655                        }
656                    }
657
658                    // Process value
659                    if let Some(value_val) = &dep_item.value {
660                        let value_val_clone = value_val.clone();
661                        let computed_value = Self::evaluate_dependent_value_static(
662                            engine,
663                            evaluations,
664                            eval_data,
665                            &value_val_clone,
666                            &current_value,
667                            &current_ref_value,
668                        )?;
669                        let cleaned_val = clean_float_noise(computed_value.clone());
670
671                        if cleaned_val != current_ref_value && cleaned_val != Value::Null {
672                            // Set the value
673                            if data_path == current_data_path {
674                                current_value = cleaned_val.clone();
675                            }
676                            eval_data.set(&data_path, cleaned_val.clone());
677                            change_obj.insert("value".to_string(), cleaned_val);
678                            add_transitive = true;
679                            add_deps = true;
680                        }
681                    }
682
683                    // add only when has clear / value
684                    if add_deps {
685                        result.push(Value::Object(change_obj));
686                    }
687
688                    // Add this dependent to queue for transitive processing
689                    if add_transitive {
690                        queue.push((ref_path.clone(), true));
691                    }
692                }
693            }
694        }
695        Ok(())
696    }
697}