Skip to main content

json_eval_rs/parse_schema/
common.rs

1use crate::jsoneval::path_utils;
2use crate::jsoneval::table_metadata::{
3    ColumnMetadata, RepeatBoundMetadata, RowMetadata, TableMetadata,
4};
5use crate::{LogicId, RLogic};
6/// Shared utilities for schema parsing (used by both legacy and parsed implementations)
7use indexmap::{IndexMap, IndexSet};
8use serde_json::Map;
9use serde_json::Value;
10use std::sync::Arc;
11
12/// Collect $ref dependencies from a JSON value recursively
13pub fn collect_refs(value: &Value, refs: &mut IndexSet<String>) {
14    match value {
15        Value::Object(map) => {
16            if let Some(path) = map.get("$ref").and_then(Value::as_str) {
17                refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
18            }
19            if let Some(path) = map.get("ref").and_then(Value::as_str) {
20                refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
21            }
22            if let Some(var_val) = map.get("var") {
23                match var_val {
24                    Value::String(s) => {
25                        refs.insert(s.clone());
26                    }
27                    Value::Array(arr) => {
28                        if let Some(path) = arr.get(0).and_then(Value::as_str) {
29                            refs.insert(path.to_string());
30                        }
31                    }
32                    _ => {}
33                }
34            }
35            for val in map.values() {
36                collect_refs(val, refs);
37            }
38        }
39        Value::Array(arr) => {
40            for val in arr {
41                collect_refs(val, refs);
42            }
43        }
44        _ => {}
45    }
46}
47
48/// Check if a value contains any actionable schema keys recursively (with depth limit for arrays)
49/// used to skip large pure-data arrays during schema walking
50#[inline]
51pub fn has_actionable_keys(value: &Value) -> bool {
52    match value {
53        Value::Object(map) => {
54            if map.contains_key("$evaluation")
55                || map.contains_key("$table")
56                || map.contains_key("dependents")
57                || map.contains_key("$layout")
58            {
59                return true;
60            }
61
62            // Check for conditional hidden/disabled fields
63            if let Some(Value::Object(condition)) = map.get("condition") {
64                if condition.contains_key("hidden") || condition.contains_key("disabled") {
65                    return true;
66                }
67            }
68
69            // Check for rules object
70            if map.contains_key("rules") {
71                return true;
72            }
73
74            // Check for type="array" with items (subforms)
75            if let Some(Value::String(type_str)) = map.get("type") {
76                if type_str == "array" && map.contains_key("items") {
77                    return true;
78                }
79            }
80
81            // Check for options with URL templates
82            if let Some(Value::String(url)) = map.get("url") {
83                if url.contains('{') && url.contains('}') {
84                    return true;
85                }
86            }
87
88            map.values().any(has_actionable_keys)
89        }
90        Value::Array(arr) => arr.iter().take(5).any(has_actionable_keys),
91        _ => false,
92    }
93}
94
95/// Compute forward/normal column partitions with transitive closure
96///
97/// This function identifies which columns have forward references (dependencies on later columns)
98/// and separates them from normal columns for proper evaluation order.
99pub fn compute_column_partitions(columns: &[ColumnMetadata]) -> (Vec<usize>, Vec<usize>) {
100    use std::collections::HashSet;
101
102    // Build set of all forward-referencing column names (direct + transitive)
103    let mut fwd_cols = HashSet::new();
104    for col in columns {
105        if col.has_forward_ref {
106            fwd_cols.insert(col.name.as_ref());
107        }
108    }
109
110    // Transitive closure: any column that depends on forward columns is also forward
111    loop {
112        let mut changed = false;
113        for col in columns {
114            if !fwd_cols.contains(col.name.as_ref()) {
115                // Check if this column depends on any forward column
116                for dep in col.dependencies.iter() {
117                    // Strip $ prefix from dependency name for comparison
118                    let dep_name = dep.trim_start_matches('$');
119                    if fwd_cols.contains(dep_name) {
120                        fwd_cols.insert(col.name.as_ref());
121                        changed = true;
122                        break;
123                    }
124                }
125            }
126        }
127        // Stop when no more changes
128        if !changed {
129            break;
130        }
131    }
132
133    // Separate into forward and normal indices
134    let mut forward_indices = Vec::new();
135    let mut normal_indices = Vec::new();
136
137    for (idx, col) in columns.iter().enumerate() {
138        if fwd_cols.contains(col.name.as_ref()) {
139            forward_indices.push(idx);
140        } else {
141            normal_indices.push(idx);
142        }
143    }
144
145    (forward_indices, normal_indices)
146}
147
148pub fn walk_schema(
149    value: &Value,
150    path: &str,
151    engine: &mut RLogic,
152    evaluations: &mut IndexMap<String, LogicId>,
153    tables: &mut IndexMap<String, Value>,
154    deps: &mut IndexMap<String, IndexSet<String>>,
155    value_fields: &mut Vec<String>,
156    layout_paths: &mut Vec<String>,
157    dependents: &mut IndexMap<String, Vec<crate::DependentItem>>,
158    options_templates: &mut Vec<(String, String, String)>,
159    subforms: &mut Vec<(String, serde_json::Map<String, Value>, Value)>,
160    fields_with_rules: &mut Vec<String>,
161    conditional_hidden_fields: &mut Vec<String>,
162    conditional_readonly_fields: &mut Vec<String>,
163) -> Result<(), String> {
164    match value {
165        Value::Object(map) => {
166            // Check for $evaluation
167            if let Some(evaluation) = map.get("$evaluation") {
168                let key = path.to_string();
169                let logic_value = evaluation.get("logic").unwrap_or(evaluation);
170                let logic_id = engine
171                    .compile(logic_value)
172                    .map_err(|e| format!("failed to compile evaluation at {key}: {e}"))?;
173                evaluations.insert(key.clone(), logic_id);
174
175                // Collect dependencies with smart table inheritance
176                let mut refs: IndexSet<String> = engine
177                    .get_referenced_vars(&logic_id)
178                    .unwrap_or_default()
179                    .into_iter()
180                    .map(|dep| path_utils::canonicalize_schema_path(&dep).into_owned())
181                    .filter(|dep| {
182                        // Filter out simple column references (e.g., "/INSAGE_YEAR", "/PREM_PP")
183                        // These are FINDINDEX/MATCH column names, not actual data dependencies
184                        // Real dependencies have multiple path segments (e.g., "/illustration/properties/...")
185                        // Update: allow top-level fields only if they are system paths or deeper paths
186                        dep.matches('/').count() > 1 || dep.starts_with("/$")
187                    })
188                    .collect();
189                let mut extra_refs = IndexSet::new();
190                collect_refs(logic_value, &mut extra_refs);
191                if !extra_refs.is_empty() {
192                    refs.extend(extra_refs.into_iter());
193                }
194
195                // For table dependencies, inherit parent table path instead of individual rows
196                let refs: IndexSet<String> = refs
197                    .into_iter()
198                    .filter_map(|dep| {
199                        // If dependency is a table row (contains /$table/), inherit table path
200                        if let Some(table_idx) = dep.find("/$table/") {
201                            let table_path = &dep[..table_idx];
202                            Some(table_path.to_string())
203                        } else {
204                            Some(dep.to_string())
205                        }
206                    })
207                    .collect();
208
209                if !refs.is_empty() {
210                    deps.insert(key.clone(), refs);
211                }
212            }
213
214            // Check for $table
215            if let Some(table) = map.get("$table") {
216                let key = path.to_string();
217
218                let rows = table.clone();
219                let datas = map
220                    .get("$datas")
221                    .cloned()
222                    .unwrap_or_else(|| Value::Array(vec![]));
223                let skip = map.get("$skip").cloned().unwrap_or(Value::Bool(false));
224                let clear = map.get("$clear").cloned().unwrap_or(Value::Bool(false));
225
226                let mut table_entry = Map::new();
227                table_entry.insert("rows".to_string(), rows);
228                table_entry.insert("datas".to_string(), datas);
229                table_entry.insert("skip".to_string(), skip);
230                table_entry.insert("clear".to_string(), clear);
231
232                tables.insert(key, Value::Object(table_entry));
233            }
234
235            // Check for $layout with elements
236            if let Some(layout_obj) = map.get("$layout") {
237                if let Some(Value::Array(_)) = layout_obj.get("elements") {
238                    let layout_elements_path = format!("{}/$layout/elements", path);
239                    layout_paths.push(layout_elements_path);
240                }
241            }
242
243            // Check for rules object - collect field path for efficient validation
244            if map.contains_key("rules") && !path.is_empty() && !path.starts_with("#/$") {
245                // Convert JSON pointer path to dotted notation for validation
246                // E.g., "#/properties/form/properties/name" -> "form.name"
247                let field_path = path
248                    .trim_start_matches('#')
249                    .replace("/properties/", ".")
250                    .trim_start_matches('/')
251                    .trim_start_matches('.')
252                    .to_string();
253
254                if !field_path.is_empty() && !field_path.starts_with("$") {
255                    fields_with_rules.push(field_path);
256                }
257            }
258
259            // Check for options with URL templates
260            if let Some(Value::String(url)) = map.get("url") {
261                // Check if URL contains template pattern {variable}
262                if url.contains('{') && url.contains('}') {
263                    // Convert to JSON pointer format for evaluated_schema access
264                    let url_path = path_utils::normalize_to_json_pointer(&format!("{}/url", path))
265                        .into_owned();
266                    let params_path =
267                        path_utils::normalize_to_json_pointer(&format!("{}/params", path))
268                            .into_owned();
269                    options_templates.push((url_path, url.clone(), params_path));
270                }
271            }
272
273            // Check for array fields with items (subforms)
274            if let Some(Value::String(type_str)) = map.get("type") {
275                if type_str == "array" {
276                    if let Some(items) = map.get("items") {
277                        // Store subform info for later creation (after walk completes)
278                        subforms.push((path.to_string(), map.clone(), items.clone()));
279                        // Don't recurse into items - it will be processed as a separate subform
280                        return Ok(());
281                    }
282                }
283            }
284
285            // Check for conditional hidden/disabled fields
286            if let Some(Value::Object(condition)) = map.get("condition") {
287                // Hidden
288                if condition.contains_key("hidden") {
289                    conditional_hidden_fields.push(path.to_string());
290                }
291                // Disabled (Read Only) - only relevant if it has a value enforce
292                if condition.contains_key("disabled") && map.contains_key("value") {
293                    conditional_readonly_fields.push(path.to_string());
294                }
295            }
296
297            // Check for dependents array
298            if let Some(Value::Array(dependents_arr)) = map.get("dependents") {
299                let mut dependent_items = Vec::new();
300
301                for (dep_idx, dep_item) in dependents_arr.iter().enumerate() {
302                    if let Value::Object(dep_obj) = dep_item {
303                        if let Some(Value::String(ref_path)) = dep_obj.get("$ref") {
304                            // Process clear - compile if it's an $evaluation
305                            let clear_val = if let Some(clear) = dep_obj.get("clear") {
306                                if let Value::Object(clear_obj) = clear {
307                                    if clear_obj.contains_key("$evaluation") {
308                                        // Compile and store the evaluation
309                                        let clear_eval = clear_obj.get("$evaluation").unwrap();
310                                        let clear_key =
311                                            format!("{}/dependents/{}/clear", path, dep_idx);
312                                        let logic_id = engine.compile(clear_eval).map_err(|e| {
313                                            format!(
314                                                "Failed to compile dependent clear at {}: {}",
315                                                clear_key, e
316                                            )
317                                        })?;
318                                        evaluations.insert(clear_key.clone(), logic_id);
319                                        // Replace with eval key reference
320                                        Some(Value::String(clear_key))
321                                    } else {
322                                        Some(clear.clone())
323                                    }
324                                } else {
325                                    Some(clear.clone())
326                                }
327                            } else {
328                                None
329                            };
330
331                            // Process value - compile if it's an $evaluation
332                            let value_val = if let Some(value) = dep_obj.get("value") {
333                                if let Value::Object(value_obj) = value {
334                                    if value_obj.contains_key("$evaluation") {
335                                        // Compile and store the evaluation
336                                        let value_eval = value_obj.get("$evaluation").unwrap();
337                                        let value_key =
338                                            format!("{}/dependents/{}/value", path, dep_idx);
339                                        let logic_id = engine.compile(value_eval).map_err(|e| {
340                                            format!(
341                                                "Failed to compile dependent value at {}: {}",
342                                                value_key, e
343                                            )
344                                        })?;
345                                        evaluations.insert(value_key.clone(), logic_id);
346                                        // Replace with eval key reference
347                                        Some(Value::String(value_key))
348                                    } else {
349                                        Some(value.clone())
350                                    }
351                                } else {
352                                    Some(value.clone())
353                                }
354                            } else {
355                                None
356                            };
357
358                            dependent_items.push(crate::DependentItem {
359                                ref_path: ref_path.clone(),
360                                clear: clear_val,
361                                value: value_val,
362                            });
363                        }
364                    }
365                }
366
367                if !dependent_items.is_empty() {
368                    dependents.insert(path.to_string(), dependent_items);
369                }
370            }
371
372            // Recurse into children
373            Ok(for (key, val) in map {
374                // Skip special evaluation and dependents keys from recursion (already processed above)
375                if key == "$evaluation" || key == "dependents" {
376                    continue;
377                }
378
379                let next_path = if path == "#" {
380                    format!("#/{key}")
381                } else {
382                    format!("{path}/{key}")
383                };
384
385                // Check if this is a "value" field
386                // Allow $params but exclude other special $ paths like $layout, $items, etc.
387                let is_excluded_special_path = next_path.contains("/$layout/")
388                    || next_path.contains("/$items/")
389                    || next_path.contains("/$options/")
390                    || next_path.contains("/$dependents/")
391                    || next_path.contains("/$rules/");
392
393                if key == "value" && !is_excluded_special_path {
394                    value_fields.push(next_path.clone());
395                }
396
397                // Recurse into all children (including $ keys like $table, $datas, etc.)
398                walk_schema(
399                    val,
400                    &next_path,
401                    engine,
402                    evaluations,
403                    tables,
404                    deps,
405                    value_fields,
406                    layout_paths,
407                    dependents,
408                    options_templates,
409                    subforms,
410                    fields_with_rules,
411                    conditional_hidden_fields,
412                    conditional_readonly_fields,
413                )?;
414            })
415        }
416        Value::Array(arr) => {
417            // Skip large arrays that contain no actionable schema keys.
418            // This avoids recursively walking pure-data arrays (e.g., table rows in $params).
419            if arr.len() > 10 && !has_actionable_keys(value) {
420                return Ok(());
421            }
422            Ok(for (index, item) in arr.iter().enumerate() {
423                let next_path = if path == "#" {
424                    format!("#/{index}")
425                } else {
426                    format!("{path}/{index}")
427                };
428                walk_schema(
429                    item,
430                    &next_path,
431                    engine,
432                    evaluations,
433                    tables,
434                    deps,
435                    value_fields,
436                    layout_paths,
437                    dependents,
438                    options_templates,
439                    subforms,
440                    fields_with_rules,
441                    conditional_hidden_fields,
442                    conditional_readonly_fields,
443                )?;
444            })
445        }
446        _ => Ok(()),
447    }
448}
449pub fn collect_table_dependencies(
450    tables: &IndexMap<String, Value>,
451    dependencies: &mut IndexMap<String, IndexSet<String>>,
452) {
453    for (table_key, _) in tables.iter() {
454        let mut table_deps = IndexSet::new();
455
456        let table_data_prefix = path_utils::normalize_to_json_pointer(table_key)
457            .replace("/properties/", "/")
458            .trim_start_matches('#')
459            .to_string();
460        let table_data_prefix_slash = format!("{}/", table_data_prefix);
461
462        for (eval_key, deps) in dependencies.iter() {
463            let is_child = eval_key.len() > table_key.len()
464                && eval_key.starts_with(table_key.as_str())
465                && eval_key.as_bytes().get(table_key.len()) == Some(&b'/');
466
467            if is_child {
468                if eval_key.contains("/$datas/") {
469                    continue;
470                }
471
472                for dep in deps {
473                    let dep_data_path = path_utils::normalize_to_json_pointer(dep)
474                        .replace("/properties/", "/")
475                        .trim_start_matches('#')
476                        .to_string();
477
478                    if dep_data_path == table_data_prefix
479                        || dep_data_path.starts_with(&table_data_prefix_slash)
480                    {
481                        continue;
482                    }
483                    let is_params_dep = dep.contains("$params");
484                    let is_inline_system = !is_params_dep
485                        && !dep.contains("$context")
486                        && (dep.starts_with("/$") || dep.starts_with('$'));
487                    if is_inline_system {
488                        continue;
489                    }
490                    table_deps.insert(dep.clone());
491                }
492            }
493        }
494
495        if !table_deps.is_empty() {
496            dependencies.insert(table_key.clone(), table_deps);
497        }
498    }
499}
500
501pub fn categorize_evaluations(
502    sorted_evaluations: &[Vec<String>],
503    evaluations: &IndexMap<String, crate::LogicId>,
504    tables: &IndexMap<String, Value>,
505) -> (Vec<String>, Vec<String>) {
506    let batched_keys: IndexSet<String> = sorted_evaluations.iter().flatten().cloned().collect();
507
508    let mut rules_evaluations = Vec::new();
509    let mut others_evaluations = Vec::new();
510
511    for eval_key in evaluations.keys() {
512        if batched_keys.contains(eval_key) {
513            continue;
514        }
515
516        if tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
517            continue;
518        }
519
520        if eval_key.contains("/$params/") {
521            continue;
522        }
523
524        if eval_key.contains("/rules/") {
525            rules_evaluations.push(eval_key.clone());
526        } else if !eval_key.contains("/dependents/") {
527            others_evaluations.push(eval_key.clone());
528        }
529    }
530
531    (rules_evaluations, others_evaluations)
532}
533
534pub fn process_value_fields(
535    value_fields: Vec<String>,
536    tables: &IndexMap<String, Value>,
537) -> Vec<String> {
538    let mut value_evaluations = Vec::new();
539
540    for path in value_fields {
541        if value_evaluations.contains(&path) {
542            continue;
543        }
544
545        if path.contains("/$params/") || tables.iter().any(|(key, _)| path.starts_with(key)) {
546            continue;
547        }
548
549        value_evaluations.push(path);
550    }
551
552    value_evaluations
553}
554
555pub fn compile_table_metadata(
556    evaluations: &IndexMap<String, crate::LogicId>,
557    engine: &crate::RLogic,
558    eval_key: &str,
559    table: &Value,
560) -> Result<TableMetadata, String> {
561    let rows = table
562        .get("rows")
563        .and_then(|v| v.as_array())
564        .ok_or("table missing rows")?;
565    let empty_datas = Vec::new();
566    let datas = table
567        .get("datas")
568        .and_then(|v| v.as_array())
569        .unwrap_or(&empty_datas);
570
571    // Pre-compile data plans with Arc sharing
572    let mut data_plans = Vec::with_capacity(datas.len());
573    for (idx, entry) in datas.iter().enumerate() {
574        let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
575            continue;
576        };
577        let logic_path = format!("{eval_key}/$datas/{idx}/data");
578        let logic = evaluations.get(&logic_path).copied();
579        let literal = entry.get("data").map(|v| Arc::new(v.clone()));
580        data_plans.push((Arc::from(name), logic, literal));
581    }
582
583    // Pre-compile row plans with dependency analysis
584    let mut row_plans = Vec::with_capacity(rows.len());
585    for (row_idx, row_val) in rows.iter().enumerate() {
586        let Some(row_obj) = row_val.as_object() else {
587            continue;
588        };
589
590        if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
591            if repeat_arr.len() == 3 {
592                let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
593                let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
594                let start_logic = evaluations.get(&start_logic_path).copied();
595                let end_logic = evaluations.get(&end_logic_path).copied();
596
597                let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
598                let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
599
600                if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
601                    let mut columns = Vec::with_capacity(template.len());
602                    for (col_name, col_val) in template {
603                        let col_eval_path =
604                            format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
605                        let logic = evaluations.get(&col_eval_path).copied();
606                        let literal = if logic.is_none() {
607                            Some(col_val.clone())
608                        } else {
609                            None
610                        };
611
612                        // Extract dependencies ONCE at parse time (not during evaluation)
613                        let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
614                            let deps = engine
615                                .get_referenced_vars(&logic_id)
616                                .unwrap_or_default()
617                                .into_iter()
618                                .filter(|v| {
619                                    v.starts_with('$') && v != "$iteration" && v != "$threshold"
620                                })
621                                .collect();
622                            let has_fwd = engine.has_forward_reference(&logic_id);
623                            (deps, has_fwd)
624                        } else {
625                            (Vec::new(), false)
626                        };
627
628                        columns.push(ColumnMetadata::new(
629                            col_name,
630                            logic,
631                            literal,
632                            dependencies,
633                            has_forward_ref,
634                        ));
635                    }
636
637                    // Pre-compute forward column propagation (transitive closure)
638                    let (forward_cols, normal_cols) = compute_column_partitions(&columns);
639
640                    row_plans.push(RowMetadata::Repeat {
641                        start: RepeatBoundMetadata {
642                            logic: start_logic,
643                            literal: start_literal,
644                        },
645                        end: RepeatBoundMetadata {
646                            logic: end_logic,
647                            literal: end_literal,
648                        },
649                        columns: columns.into(),
650                        forward_cols: forward_cols.into(),
651                        normal_cols: normal_cols.into(),
652                    });
653                    continue;
654                }
655            }
656        }
657
658        // Static row
659        let mut columns = Vec::with_capacity(row_obj.len());
660        for (col_name, col_val) in row_obj {
661            if col_name == "$repeat" {
662                continue;
663            }
664            let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
665            let logic = evaluations.get(&col_eval_path).copied();
666            let literal = if logic.is_none() {
667                Some(col_val.clone())
668            } else {
669                None
670            };
671
672            // Extract dependencies ONCE at parse time
673            let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
674                let deps = engine
675                    .get_referenced_vars(&logic_id)
676                    .unwrap_or_default()
677                    .into_iter()
678                    .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
679                    .collect();
680                let has_fwd = engine.has_forward_reference(&logic_id);
681                (deps, has_fwd)
682            } else {
683                (Vec::new(), false)
684            };
685
686            columns.push(ColumnMetadata::new(
687                col_name,
688                logic,
689                literal,
690                dependencies,
691                has_forward_ref,
692            ));
693        }
694        row_plans.push(RowMetadata::Static {
695            columns: columns.into(),
696        });
697    }
698
699    // Pre-compile skip/clear logic
700    let skip_logic = evaluations.get(&format!("{eval_key}/$skip")).copied();
701    let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
702    let clear_logic = evaluations.get(&format!("{eval_key}/$clear")).copied();
703    let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
704
705    Ok(TableMetadata {
706        data_plans: data_plans.into(),
707        row_plans: row_plans.into(),
708        skip_logic,
709        skip_literal,
710        clear_logic,
711        clear_literal,
712    })
713}
714pub fn build_reffed_by(
715    dependencies: &IndexMap<String, IndexSet<String>>,
716) -> IndexMap<String, Vec<String>> {
717    let mut reffed_by: IndexMap<String, Vec<String>> = IndexMap::new();
718
719    for (eval_path, deps) in dependencies.iter() {
720        if eval_path.ends_with("/condition/hidden") {
721            let subject_path = eval_path[..eval_path.len() - 17].to_string();
722
723            for dep in deps {
724                let normalized_dep = path_utils::normalize_to_json_pointer(dep)
725                    .replace("/properties/", "/")
726                    .trim_start_matches('#')
727                    .to_string();
728
729                let dep_key = if normalized_dep.starts_with('/') {
730                    normalized_dep
731                } else {
732                    format!("/{}", normalized_dep)
733                };
734
735                reffed_by
736                    .entry(dep_key)
737                    .or_insert_with(Vec::new)
738                    .push(subject_path.clone());
739            }
740        }
741    }
742
743    reffed_by
744}
745
746pub fn build_dep_formula_triggers(
747    dependents_evaluations: &IndexMap<String, Vec<crate::DependentItem>>,
748    evaluations: &IndexMap<String, crate::LogicId>,
749    engine: &crate::RLogic,
750) -> IndexMap<String, Vec<(String, usize)>> {
751    let mut triggers: IndexMap<String, Vec<(String, usize)>> = IndexMap::new();
752
753    for (source_path, dep_items) in dependents_evaluations.iter() {
754        for (dep_idx, dep_item) in dep_items.iter().enumerate() {
755            let formula_keys: Vec<String> = [
756                dep_item
757                    .value
758                    .as_ref()
759                    .and_then(|v| v.as_str())
760                    .map(|s| s.to_string()),
761                dep_item
762                    .clear
763                    .as_ref()
764                    .and_then(|v| v.as_str())
765                    .map(|s| s.to_string()),
766            ]
767            .into_iter()
768            .flatten()
769            .filter(|k| k.contains("/dependents/"))
770            .collect();
771
772            for formula_key in formula_keys {
773                let logic_id = match evaluations.get(&formula_key).copied() {
774                    Some(id) => id,
775                    None => continue,
776                };
777
778                let refs = engine.get_referenced_vars(&logic_id).unwrap_or_default();
779
780                for dep_ref in refs {
781                    let normalized = path_utils::normalize_to_json_pointer(&dep_ref)
782                        .replace("/properties/", "/")
783                        .trim_start_matches('#')
784                        .to_string();
785                    let dep_key = if normalized.starts_with('/') {
786                        normalized
787                    } else {
788                        format!("/{}", normalized)
789                    };
790
791                    if dep_key.starts_with("/$") {
792                        continue;
793                    }
794
795                    if dep_key.matches('/').count() <= 1 {
796                        continue;
797                    }
798
799                    let source_data = path_utils::normalize_to_json_pointer(source_path)
800                        .replace("/properties/", "/")
801                        .trim_start_matches('#')
802                        .to_string();
803                    let source_data_key = if source_data.starts_with('/') {
804                        source_data
805                    } else {
806                        format!("/{}", source_data)
807                    };
808                    if dep_key == source_data_key {
809                        continue;
810                    }
811
812                    let target_data = path_utils::normalize_to_json_pointer(&dep_item.ref_path)
813                        .replace("/properties/", "/")
814                        .trim_start_matches('#')
815                        .to_string();
816                    let target_data_key = if target_data.starts_with('/') {
817                        target_data
818                    } else {
819                        format!("/{}", target_data)
820                    };
821                    if dep_key == target_data_key {
822                        continue;
823                    }
824
825                    let pair = (source_path.clone(), dep_idx);
826                    let entry = triggers.entry(dep_key).or_insert_with(Vec::new);
827                    if !entry.contains(&pair) {
828                        entry.push(pair);
829                    }
830                }
831            }
832        }
833    }
834
835    for sources in triggers.values_mut() {
836        sources.sort();
837        // Since we check contains above and we sort, it's pretty clean.
838    }
839
840    triggers
841}