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