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                    }
280                }
281            }
282
283            // Check for conditional hidden/disabled fields
284            if let Some(Value::Object(condition)) = map.get("condition") {
285                // Hidden
286                if condition.contains_key("hidden") {
287                    conditional_hidden_fields.push(path.to_string());
288                }
289                // Disabled (Read Only) - only relevant if it has a value enforce
290                if condition.contains_key("disabled") && map.contains_key("value") {
291                    conditional_readonly_fields.push(path.to_string());
292                }
293            }
294
295            // Check for dependents array
296            if let Some(Value::Array(dependents_arr)) = map.get("dependents") {
297                let mut dependent_items = Vec::new();
298
299                for (dep_idx, dep_item) in dependents_arr.iter().enumerate() {
300                    if let Value::Object(dep_obj) = dep_item {
301                        if let Some(Value::String(ref_path)) = dep_obj.get("$ref") {
302                            // Process clear - compile if it's an $evaluation
303                            let clear_val = if let Some(clear) = dep_obj.get("clear") {
304                                if let Value::Object(clear_obj) = clear {
305                                    if clear_obj.contains_key("$evaluation") {
306                                        // Compile and store the evaluation
307                                        let clear_eval = clear_obj.get("$evaluation").unwrap();
308                                        let clear_key =
309                                            format!("{}/dependents/{}/clear", path, dep_idx);
310                                        let logic_id = engine.compile(clear_eval).map_err(|e| {
311                                            format!(
312                                                "Failed to compile dependent clear at {}: {}",
313                                                clear_key, e
314                                            )
315                                        })?;
316                                        evaluations.insert(clear_key.clone(), logic_id);
317                                        // Replace with eval key reference
318                                        Some(Value::String(clear_key))
319                                    } else {
320                                        Some(clear.clone())
321                                    }
322                                } else {
323                                    Some(clear.clone())
324                                }
325                            } else {
326                                None
327                            };
328
329                            // Process value - compile if it's an $evaluation
330                            let value_val = if let Some(value) = dep_obj.get("value") {
331                                if let Value::Object(value_obj) = value {
332                                    if value_obj.contains_key("$evaluation") {
333                                        // Compile and store the evaluation
334                                        let value_eval = value_obj.get("$evaluation").unwrap();
335                                        let value_key =
336                                            format!("{}/dependents/{}/value", path, dep_idx);
337                                        let logic_id = engine.compile(value_eval).map_err(|e| {
338                                            format!(
339                                                "Failed to compile dependent value at {}: {}",
340                                                value_key, e
341                                            )
342                                        })?;
343                                        evaluations.insert(value_key.clone(), logic_id);
344                                        // Replace with eval key reference
345                                        Some(Value::String(value_key))
346                                    } else {
347                                        Some(value.clone())
348                                    }
349                                } else {
350                                    Some(value.clone())
351                                }
352                            } else {
353                                None
354                            };
355
356                            dependent_items.push(crate::DependentItem {
357                                ref_path: ref_path.clone(),
358                                clear: clear_val,
359                                value: value_val,
360                            });
361                        }
362                    }
363                }
364
365                if !dependent_items.is_empty() {
366                    dependents.insert(path.to_string(), dependent_items);
367                }
368            }
369
370            // Recurse into children
371            Ok(for (key, val) in map {
372                // Skip special evaluation and dependents keys from recursion (already processed above)
373                if key == "$evaluation" || key == "dependents" || (key == "items" && map.get("type").and_then(Value::as_str) == Some("array")) {
374                    continue;
375                }
376
377                let next_path = if path == "#" {
378                    format!("#/{key}")
379                } else {
380                    format!("{path}/{key}")
381                };
382
383                // Check if this is a "value" field
384                // Allow $params but exclude other special $ paths like $layout, $items, etc.
385                let is_excluded_special_path = next_path.contains("/$layout/")
386                    || next_path.contains("/$items/")
387                    || next_path.contains("/$options/")
388                    || next_path.contains("/$dependents/")
389                    || next_path.contains("/$rules/");
390
391                if key == "value" && !is_excluded_special_path {
392                    value_fields.push(next_path.clone());
393                }
394
395                // Recurse into all children (including $ keys like $table, $datas, etc.)
396                walk_schema(
397                    val,
398                    &next_path,
399                    engine,
400                    evaluations,
401                    tables,
402                    deps,
403                    value_fields,
404                    layout_paths,
405                    dependents,
406                    options_templates,
407                    subforms,
408                    fields_with_rules,
409                    conditional_hidden_fields,
410                    conditional_readonly_fields,
411                )?;
412            })
413        }
414        Value::Array(arr) => {
415            // Skip large arrays that contain no actionable schema keys.
416            // This avoids recursively walking pure-data arrays (e.g., table rows in $params).
417            if arr.len() > 10 && !has_actionable_keys(value) {
418                return Ok(());
419            }
420            Ok(for (index, item) in arr.iter().enumerate() {
421                let next_path = if path == "#" {
422                    format!("#/{index}")
423                } else {
424                    format!("{path}/{index}")
425                };
426                walk_schema(
427                    item,
428                    &next_path,
429                    engine,
430                    evaluations,
431                    tables,
432                    deps,
433                    value_fields,
434                    layout_paths,
435                    dependents,
436                    options_templates,
437                    subforms,
438                    fields_with_rules,
439                    conditional_hidden_fields,
440                    conditional_readonly_fields,
441                )?;
442            })
443        }
444        _ => Ok(()),
445    }
446}
447pub fn collect_table_dependencies(
448    tables: &IndexMap<String, Value>,
449    dependencies: &mut IndexMap<String, IndexSet<String>>,
450) {
451    for (table_key, _) in tables.iter() {
452        let mut table_deps = IndexSet::new();
453
454        let table_data_prefix = path_utils::normalize_to_json_pointer(table_key)
455            .replace("/properties/", "/")
456            .trim_start_matches('#')
457            .to_string();
458        let table_data_prefix_slash = format!("{}/", table_data_prefix);
459
460        for (eval_key, deps) in dependencies.iter() {
461            let is_child = eval_key.len() > table_key.len()
462                && eval_key.starts_with(table_key.as_str())
463                && eval_key.as_bytes().get(table_key.len()) == Some(&b'/');
464
465            if is_child {
466                if eval_key.contains("/$datas/") {
467                    continue;
468                }
469
470                for dep in deps {
471                    let dep_data_path = path_utils::normalize_to_json_pointer(dep)
472                        .replace("/properties/", "/")
473                        .trim_start_matches('#')
474                        .to_string();
475
476                    if dep_data_path == table_data_prefix
477                        || dep_data_path.starts_with(&table_data_prefix_slash)
478                    {
479                        continue;
480                    }
481                    let is_params_dep = dep.contains("$params");
482                    let is_inline_system = !is_params_dep
483                        && !dep.contains("$context")
484                        && (dep.starts_with("/$") || dep.starts_with('$'));
485                    if is_inline_system {
486                        continue;
487                    }
488                    table_deps.insert(dep.clone());
489                }
490            }
491        }
492
493        if !table_deps.is_empty() {
494            dependencies.insert(table_key.clone(), table_deps);
495        }
496    }
497}
498
499pub fn categorize_evaluations(
500    sorted_evaluations: &[Vec<String>],
501    evaluations: &IndexMap<String, crate::LogicId>,
502    tables: &IndexMap<String, Value>,
503) -> (Vec<String>, Vec<String>) {
504    let batched_keys: IndexSet<String> = sorted_evaluations.iter().flatten().cloned().collect();
505
506    let mut rules_evaluations = Vec::new();
507    let mut others_evaluations = Vec::new();
508
509    for eval_key in evaluations.keys() {
510        if batched_keys.contains(eval_key) {
511            continue;
512        }
513
514        if tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
515            continue;
516        }
517
518        if eval_key.contains("/$params/") {
519            continue;
520        }
521
522        if eval_key.contains("/rules/") {
523            rules_evaluations.push(eval_key.clone());
524        } else if !eval_key.contains("/dependents/") {
525            others_evaluations.push(eval_key.clone());
526        }
527    }
528
529    (rules_evaluations, others_evaluations)
530}
531
532pub fn process_value_fields(
533    value_fields: Vec<String>,
534    tables: &IndexMap<String, Value>,
535) -> Vec<String> {
536    let mut value_evaluations = Vec::new();
537
538    for path in value_fields {
539        if value_evaluations.contains(&path) {
540            continue;
541        }
542
543        if path.contains("/$params/") || tables.iter().any(|(key, _)| path.starts_with(key)) {
544            continue;
545        }
546
547        value_evaluations.push(path);
548    }
549
550    value_evaluations
551}
552
553pub fn compile_table_metadata(
554    evaluations: &IndexMap<String, crate::LogicId>,
555    engine: &crate::RLogic,
556    eval_key: &str,
557    table: &Value,
558) -> Result<TableMetadata, String> {
559    let rows = table
560        .get("rows")
561        .and_then(|v| v.as_array())
562        .ok_or("table missing rows")?;
563    let empty_datas = Vec::new();
564    let datas = table
565        .get("datas")
566        .and_then(|v| v.as_array())
567        .unwrap_or(&empty_datas);
568
569    // Pre-compile data plans with Arc sharing
570    let mut data_plans = Vec::with_capacity(datas.len());
571    for (idx, entry) in datas.iter().enumerate() {
572        let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
573            continue;
574        };
575        let logic_path = format!("{eval_key}/$datas/{idx}/data");
576        let logic = evaluations.get(&logic_path).copied();
577        let literal = entry.get("data").map(|v| Arc::new(v.clone()));
578        data_plans.push((Arc::from(name), logic, literal));
579    }
580
581    // Pre-compile row plans with dependency analysis
582    let mut row_plans = Vec::with_capacity(rows.len());
583    for (row_idx, row_val) in rows.iter().enumerate() {
584        let Some(row_obj) = row_val.as_object() else {
585            continue;
586        };
587
588        if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
589            if repeat_arr.len() == 3 {
590                let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
591                let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
592                let start_logic = evaluations.get(&start_logic_path).copied();
593                let end_logic = evaluations.get(&end_logic_path).copied();
594
595                let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
596                let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
597
598                if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
599                    let mut columns = Vec::with_capacity(template.len());
600                    for (col_name, col_val) in template {
601                        let col_eval_path =
602                            format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
603                        let logic = evaluations.get(&col_eval_path).copied();
604                        let literal = if logic.is_none() {
605                            Some(col_val.clone())
606                        } else {
607                            None
608                        };
609
610                        // Extract dependencies ONCE at parse time (not during evaluation)
611                        let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
612                            let deps = engine
613                                .get_referenced_vars(&logic_id)
614                                .unwrap_or_default()
615                                .into_iter()
616                                .filter(|v| {
617                                    v.starts_with('$') && v != "$iteration" && v != "$threshold"
618                                })
619                                .collect();
620                            let has_fwd = engine.has_forward_reference(&logic_id);
621                            (deps, has_fwd)
622                        } else {
623                            (Vec::new(), false)
624                        };
625
626                        columns.push(ColumnMetadata::new(
627                            col_name,
628                            logic,
629                            literal,
630                            dependencies,
631                            has_forward_ref,
632                        ));
633                    }
634
635                    // Pre-compute forward column propagation (transitive closure)
636                    let (forward_cols, normal_cols) = compute_column_partitions(&columns);
637
638                    row_plans.push(RowMetadata::Repeat {
639                        start: RepeatBoundMetadata {
640                            logic: start_logic,
641                            literal: start_literal,
642                        },
643                        end: RepeatBoundMetadata {
644                            logic: end_logic,
645                            literal: end_literal,
646                        },
647                        columns: columns.into(),
648                        forward_cols: forward_cols.into(),
649                        normal_cols: normal_cols.into(),
650                    });
651                    continue;
652                }
653            }
654        }
655
656        // Static row
657        let mut columns = Vec::with_capacity(row_obj.len());
658        for (col_name, col_val) in row_obj {
659            if col_name == "$repeat" {
660                continue;
661            }
662            let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
663            let logic = evaluations.get(&col_eval_path).copied();
664            let literal = if logic.is_none() {
665                Some(col_val.clone())
666            } else {
667                None
668            };
669
670            // Extract dependencies ONCE at parse time
671            let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
672                let deps = engine
673                    .get_referenced_vars(&logic_id)
674                    .unwrap_or_default()
675                    .into_iter()
676                    .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
677                    .collect();
678                let has_fwd = engine.has_forward_reference(&logic_id);
679                (deps, has_fwd)
680            } else {
681                (Vec::new(), false)
682            };
683
684            columns.push(ColumnMetadata::new(
685                col_name,
686                logic,
687                literal,
688                dependencies,
689                has_forward_ref,
690            ));
691        }
692        row_plans.push(RowMetadata::Static {
693            columns: columns.into(),
694        });
695    }
696
697    // Pre-compile skip/clear logic
698    let skip_logic = evaluations.get(&format!("{eval_key}/$skip")).copied();
699    let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
700    let clear_logic = evaluations.get(&format!("{eval_key}/$clear")).copied();
701    let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
702
703    Ok(TableMetadata {
704        data_plans: data_plans.into(),
705        row_plans: row_plans.into(),
706        skip_logic,
707        skip_literal,
708        clear_logic,
709        clear_literal,
710    })
711}
712pub fn build_reffed_by(
713    dependencies: &IndexMap<String, IndexSet<String>>,
714) -> IndexMap<String, Vec<String>> {
715    let mut reffed_by: IndexMap<String, Vec<String>> = IndexMap::new();
716
717    for (eval_path, deps) in dependencies.iter() {
718        if eval_path.ends_with("/condition/hidden") {
719            let subject_path = eval_path[..eval_path.len() - 17].to_string();
720
721            for dep in deps {
722                let normalized_dep = path_utils::normalize_to_json_pointer(dep)
723                    .replace("/properties/", "/")
724                    .trim_start_matches('#')
725                    .to_string();
726
727                let dep_key = if normalized_dep.starts_with('/') {
728                    normalized_dep
729                } else {
730                    format!("/{}", normalized_dep)
731                };
732
733                reffed_by
734                    .entry(dep_key)
735                    .or_insert_with(Vec::new)
736                    .push(subject_path.clone());
737            }
738        }
739    }
740
741    reffed_by
742}
743
744pub fn build_dep_formula_triggers(
745    dependents_evaluations: &IndexMap<String, Vec<crate::DependentItem>>,
746    evaluations: &IndexMap<String, crate::LogicId>,
747    engine: &crate::RLogic,
748) -> IndexMap<String, Vec<(String, usize)>> {
749    let mut triggers: IndexMap<String, Vec<(String, usize)>> = IndexMap::new();
750
751    for (source_path, dep_items) in dependents_evaluations.iter() {
752        for (dep_idx, dep_item) in dep_items.iter().enumerate() {
753            let formula_keys: Vec<String> = [
754                dep_item
755                    .value
756                    .as_ref()
757                    .and_then(|v| v.as_str())
758                    .map(|s| s.to_string()),
759                dep_item
760                    .clear
761                    .as_ref()
762                    .and_then(|v| v.as_str())
763                    .map(|s| s.to_string()),
764            ]
765            .into_iter()
766            .flatten()
767            .filter(|k| k.contains("/dependents/"))
768            .collect();
769
770            for formula_key in formula_keys {
771                let logic_id = match evaluations.get(&formula_key).copied() {
772                    Some(id) => id,
773                    None => continue,
774                };
775
776                let refs = engine.get_referenced_vars(&logic_id).unwrap_or_default();
777
778                for dep_ref in refs {
779                    let normalized = path_utils::normalize_to_json_pointer(&dep_ref)
780                        .replace("/properties/", "/")
781                        .trim_start_matches('#')
782                        .to_string();
783                    let dep_key = if normalized.starts_with('/') {
784                        normalized
785                    } else {
786                        format!("/{}", normalized)
787                    };
788
789                    if dep_key.starts_with("/$") {
790                        continue;
791                    }
792
793                    if dep_key.matches('/').count() <= 1 {
794                        continue;
795                    }
796
797                    let source_data = path_utils::normalize_to_json_pointer(source_path)
798                        .replace("/properties/", "/")
799                        .trim_start_matches('#')
800                        .to_string();
801                    let source_data_key = if source_data.starts_with('/') {
802                        source_data
803                    } else {
804                        format!("/{}", source_data)
805                    };
806                    if dep_key == source_data_key {
807                        continue;
808                    }
809
810                    let target_data = path_utils::normalize_to_json_pointer(&dep_item.ref_path)
811                        .replace("/properties/", "/")
812                        .trim_start_matches('#')
813                        .to_string();
814                    let target_data_key = if target_data.starts_with('/') {
815                        target_data
816                    } else {
817                        format!("/{}", target_data)
818                    };
819                    if dep_key == target_data_key {
820                        continue;
821                    }
822
823                    let pair = (source_path.clone(), dep_idx);
824                    let entry = triggers.entry(dep_key).or_insert_with(Vec::new);
825                    if !entry.contains(&pair) {
826                        entry.push(pair);
827                    }
828                }
829            }
830        }
831    }
832
833    for sources in triggers.values_mut() {
834        sources.sort();
835        // Since we check contains above and we sort, it's pretty clean.
836    }
837
838    triggers
839}