1use crate::jsoneval::path_utils;
2use crate::jsoneval::table_metadata::{
3 ColumnMetadata, RepeatBoundMetadata, RowMetadata, TableMetadata,
4};
5use crate::{LogicId, RLogic};
6use indexmap::{IndexMap, IndexSet};
8use serde_json::Map;
9use serde_json::Value;
10use std::sync::Arc;
11
12pub fn collect_layout_field_refs(value: &Value, refs: &mut IndexSet<String>) {
17 fn collect_elements(elements: &Value, refs: &mut IndexSet<String>) {
18 let Some(elements) = elements.as_array() else {
19 return;
20 };
21 for element in elements {
22 let Some(map) = element.as_object() else {
23 continue;
24 };
25 if let Some(reference) = map.get("$ref").and_then(Value::as_str) {
26 let pointer = path_utils::normalize_to_json_pointer(reference);
27 refs.insert(pointer.trim_start_matches('#').to_string());
28 }
29 if let Some(children) = map.get("elements") {
30 collect_elements(children, refs);
31 }
32 }
33 }
34
35 match value {
36 Value::Object(map) => {
37 if let Some(elements) = map
38 .get("$layout")
39 .and_then(Value::as_object)
40 .and_then(|layout| layout.get("elements"))
41 {
42 collect_elements(elements, refs);
43 }
44 for child in map.values() {
45 collect_layout_field_refs(child, refs);
46 }
47 }
48 Value::Array(values) => {
49 for child in values {
50 collect_layout_field_refs(child, refs);
51 }
52 }
53 _ => {}
54 }
55}
56
57pub fn collect_refs(value: &Value, refs: &mut IndexSet<String>) {
59 match value {
60 Value::Object(map) => {
61 if let Some(path) = map.get("$ref").and_then(Value::as_str) {
62 refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
63 }
64 if let Some(path) = map.get("ref").and_then(Value::as_str) {
65 refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
66 }
67 if let Some(var_val) = map.get("var") {
68 match var_val {
69 Value::String(s) => {
70 refs.insert(s.clone());
71 }
72 Value::Array(arr) => {
73 if let Some(path) = arr.get(0).and_then(Value::as_str) {
74 refs.insert(path.to_string());
75 }
76 }
77 _ => {}
78 }
79 }
80 for val in map.values() {
81 collect_refs(val, refs);
82 }
83 }
84 Value::Array(arr) => {
85 for val in arr {
86 collect_refs(val, refs);
87 }
88 }
89 _ => {}
90 }
91}
92
93#[inline]
96pub fn has_actionable_keys(value: &Value) -> bool {
97 match value {
98 Value::Object(map) => {
99 if map.contains_key("$evaluation")
100 || map.contains_key("$table")
101 || map.contains_key("dependents")
102 || map.contains_key("$layout")
103 {
104 return true;
105 }
106
107 if let Some(Value::Object(condition)) = map.get("condition") {
109 if condition.contains_key("hidden") || condition.contains_key("disabled") {
110 return true;
111 }
112 }
113
114 if map.contains_key("rules") {
116 return true;
117 }
118
119 if let Some(Value::String(type_str)) = map.get("type") {
121 if type_str == "array" && map.contains_key("items") {
122 return true;
123 }
124 }
125
126 if let Some(Value::String(url)) = map.get("url") {
128 if url.contains('{') && url.contains('}') {
129 return true;
130 }
131 }
132
133 map.values().any(has_actionable_keys)
134 }
135 Value::Array(arr) => arr.iter().take(5).any(has_actionable_keys),
136 _ => false,
137 }
138}
139
140pub fn compute_column_partitions(columns: &[ColumnMetadata]) -> (Vec<usize>, Vec<usize>) {
145 use std::collections::HashSet;
146
147 let mut fwd_cols = HashSet::new();
149 for col in columns {
150 if col.has_forward_ref {
151 fwd_cols.insert(col.name.as_ref());
152 }
153 }
154
155 loop {
157 let mut changed = false;
158 for col in columns {
159 if !fwd_cols.contains(col.name.as_ref()) {
160 for dep in col.dependencies.iter() {
162 let dep_name = dep.trim_start_matches('$');
164 if fwd_cols.contains(dep_name) {
165 fwd_cols.insert(col.name.as_ref());
166 changed = true;
167 break;
168 }
169 }
170 }
171 }
172 if !changed {
174 break;
175 }
176 }
177
178 let mut forward_indices = Vec::new();
180 let mut normal_indices = Vec::new();
181
182 for (idx, col) in columns.iter().enumerate() {
183 if fwd_cols.contains(col.name.as_ref()) {
184 forward_indices.push(idx);
185 } else {
186 normal_indices.push(idx);
187 }
188 }
189
190 let forward_sorted = toposort_column_indices(columns, forward_indices);
191 let normal_sorted = toposort_column_indices(columns, normal_indices);
192
193 (forward_sorted, normal_sorted)
194}
195
196fn toposort_column_indices(columns: &[ColumnMetadata], indices: Vec<usize>) -> Vec<usize> {
199 if indices.len() <= 1 {
200 return indices;
201 }
202
203 let n = indices.len();
204 let mut name_to_sub_idx = std::collections::HashMap::with_capacity(n);
205 for (sub_idx, &col_idx) in indices.iter().enumerate() {
206 name_to_sub_idx.insert(columns[col_idx].name.as_ref(), sub_idx);
207 }
208
209 let mut in_degree = vec![0usize; n];
210 let mut adj = vec![Vec::new(); n];
211
212 for (u, &col_idx) in indices.iter().enumerate() {
213 for dep in columns[col_idx].dependencies.iter() {
214 if dep.starts_with('$') {
215 let dep_name = dep.trim_start_matches('$');
216 if let Some(&v) = name_to_sub_idx.get(dep_name) {
217 if v != u {
218 adj[v].push(u);
220 in_degree[u] += 1;
221 }
222 }
223 }
224 }
225 }
226
227 let mut queue = std::collections::VecDeque::new();
228 for (i, °) in in_degree.iter().enumerate() {
229 if deg == 0 {
230 queue.push_back(i);
231 }
232 }
233
234 let mut sorted = Vec::with_capacity(n);
235 let mut visited = vec![false; n];
236
237 while let Some(v) = queue.pop_front() {
238 visited[v] = true;
239 sorted.push(indices[v]);
240 for &u in &adj[v] {
241 in_degree[u] -= 1;
242 if in_degree[u] == 0 {
243 queue.push_back(u);
244 }
245 }
246 }
247
248 if sorted.len() < n {
250 for (i, &was_visited) in visited.iter().enumerate() {
251 if !was_visited {
252 sorted.push(indices[i]);
253 }
254 }
255 }
256
257 sorted
258}
259
260pub fn walk_schema(
261 value: &Value,
262 path: &str,
263 engine: &mut RLogic,
264 evaluations: &mut IndexMap<String, LogicId>,
265 tables: &mut IndexMap<String, Value>,
266 deps: &mut IndexMap<String, IndexSet<String>>,
267 value_fields: &mut Vec<String>,
268 layout_paths: &mut Vec<String>,
269 dependents: &mut IndexMap<String, Vec<crate::DependentItem>>,
270 options_templates: &mut Vec<(String, String, String)>,
271 subforms: &mut Vec<(String, serde_json::Map<String, Value>, Value)>,
272 fields_with_rules: &mut Vec<String>,
273 conditional_hidden_fields: &mut Vec<String>,
274 conditional_readonly_fields: &mut Vec<String>,
275) -> Result<(), String> {
276 match value {
277 Value::Object(map) => {
278 if let Some(evaluation) = map.get("$evaluation") {
280 let key = path.to_string();
281 let logic_value = evaluation.get("logic").unwrap_or(evaluation);
282 let logic_id = engine
283 .compile(logic_value)
284 .map_err(|e| format!("failed to compile evaluation at {key}: {e}"))?;
285 evaluations.insert(key.clone(), logic_id);
286
287 let mut refs: IndexSet<String> = engine
289 .get_referenced_vars(&logic_id)
290 .unwrap_or_default()
291 .into_iter()
292 .map(|dep| path_utils::canonicalize_schema_path(&dep).into_owned())
293 .filter(|dep| {
294 dep.matches('/').count() > 1 || dep.starts_with("/$")
299 })
300 .collect();
301 let mut extra_refs = IndexSet::new();
302 collect_refs(logic_value, &mut extra_refs);
303 if !extra_refs.is_empty() {
304 refs.extend(extra_refs.into_iter());
305 }
306
307 let refs: IndexSet<String> = refs
309 .into_iter()
310 .filter_map(|dep| {
311 if let Some(table_idx) = dep.find("/$table/") {
313 let table_path = &dep[..table_idx];
314 Some(table_path.to_string())
315 } else {
316 Some(dep.to_string())
317 }
318 })
319 .collect();
320
321 if !refs.is_empty() {
322 deps.insert(key.clone(), refs);
323 }
324 }
325
326 if let Some(table) = map.get("$table") {
328 let key = path.to_string();
329
330 let rows = table.clone();
331 let datas = map
332 .get("$datas")
333 .cloned()
334 .unwrap_or_else(|| Value::Array(vec![]));
335 let skip = map.get("$skip").cloned().unwrap_or(Value::Bool(false));
336 let clear = map.get("$clear").cloned().unwrap_or(Value::Bool(false));
337
338 let mut table_entry = Map::new();
339 table_entry.insert("rows".to_string(), rows);
340 table_entry.insert("datas".to_string(), datas);
341 table_entry.insert("skip".to_string(), skip);
342 table_entry.insert("clear".to_string(), clear);
343
344 tables.insert(key, Value::Object(table_entry));
345 }
346
347 if let Some(layout_obj) = map.get("$layout") {
349 if let Some(Value::Array(_)) = layout_obj.get("elements") {
350 let layout_elements_path = format!("{}/$layout/elements", path);
351 layout_paths.push(layout_elements_path);
352 }
353 }
354
355 if map.contains_key("rules") && !path.is_empty() && !path.starts_with("#/$") {
357 let field_path = path
360 .trim_start_matches('#')
361 .replace("/properties/", ".")
362 .trim_start_matches('/')
363 .trim_start_matches('.')
364 .to_string();
365
366 if !field_path.is_empty() && !field_path.starts_with("$") {
367 fields_with_rules.push(field_path);
368 }
369 }
370
371 if let Some(Value::String(url)) = map.get("url") {
373 if url.contains('{') && url.contains('}') {
375 let url_path = path_utils::normalize_to_json_pointer(&format!("{}/url", path))
377 .into_owned();
378 let params_path =
379 path_utils::normalize_to_json_pointer(&format!("{}/params", path))
380 .into_owned();
381 options_templates.push((url_path, url.clone(), params_path));
382 }
383 }
384
385 if let Some(Value::String(type_str)) = map.get("type") {
387 if type_str == "array" {
388 if let Some(items) = map.get("items") {
389 subforms.push((path.to_string(), map.clone(), items.clone()));
391 }
392 }
393 }
394
395 if let Some(Value::Object(condition)) = map.get("condition") {
397 if condition.contains_key("hidden") {
399 conditional_hidden_fields.push(path.to_string());
400 }
401 if condition.contains_key("disabled") && map.contains_key("value") {
403 conditional_readonly_fields.push(path.to_string());
404 }
405 }
406
407 if let Some(Value::Array(dependents_arr)) = map.get("dependents") {
409 let mut dependent_items = Vec::new();
410
411 for (dep_idx, dep_item) in dependents_arr.iter().enumerate() {
412 if let Value::Object(dep_obj) = dep_item {
413 if let Some(Value::String(ref_path)) = dep_obj.get("$ref") {
414 let clear_val = if let Some(clear) = dep_obj.get("clear") {
416 if let Value::Object(clear_obj) = clear {
417 if clear_obj.contains_key("$evaluation") {
418 let clear_eval = clear_obj.get("$evaluation").unwrap();
420 let clear_key =
421 format!("{}/dependents/{}/clear", path, dep_idx);
422 let logic_id = engine.compile(clear_eval).map_err(|e| {
423 format!(
424 "Failed to compile dependent clear at {}: {}",
425 clear_key, e
426 )
427 })?;
428 evaluations.insert(clear_key.clone(), logic_id);
429 Some(Value::String(clear_key))
431 } else {
432 Some(clear.clone())
433 }
434 } else {
435 Some(clear.clone())
436 }
437 } else {
438 None
439 };
440
441 let value_val = if let Some(value) = dep_obj.get("value") {
443 if let Value::Object(value_obj) = value {
444 if value_obj.contains_key("$evaluation") {
445 let value_eval = value_obj.get("$evaluation").unwrap();
447 let value_key =
448 format!("{}/dependents/{}/value", path, dep_idx);
449 let logic_id = engine.compile(value_eval).map_err(|e| {
450 format!(
451 "Failed to compile dependent value at {}: {}",
452 value_key, e
453 )
454 })?;
455 evaluations.insert(value_key.clone(), logic_id);
456 Some(Value::String(value_key))
458 } else {
459 Some(value.clone())
460 }
461 } else {
462 Some(value.clone())
463 }
464 } else {
465 None
466 };
467
468 dependent_items.push(crate::DependentItem {
469 ref_path: ref_path.clone(),
470 clear: clear_val,
471 value: value_val,
472 });
473 }
474 }
475 }
476
477 if !dependent_items.is_empty() {
478 dependents.insert(path.to_string(), dependent_items);
479 }
480 }
481
482 Ok(for (key, val) in map {
484 if key == "$evaluation"
486 || key == "dependents"
487 || (key == "items" && map.get("type").and_then(Value::as_str) == Some("array"))
488 {
489 continue;
490 }
491
492 let next_path = if path == "#" {
493 format!("#/{key}")
494 } else {
495 format!("{path}/{key}")
496 };
497
498 let is_excluded_special_path = next_path.contains("/$layout/")
501 || next_path.contains("/$items/")
502 || next_path.contains("/$options/")
503 || next_path.contains("/$dependents/")
504 || next_path.contains("/$rules/");
505
506 if key == "value" && !is_excluded_special_path {
507 value_fields.push(next_path.clone());
508 }
509
510 walk_schema(
512 val,
513 &next_path,
514 engine,
515 evaluations,
516 tables,
517 deps,
518 value_fields,
519 layout_paths,
520 dependents,
521 options_templates,
522 subforms,
523 fields_with_rules,
524 conditional_hidden_fields,
525 conditional_readonly_fields,
526 )?;
527 })
528 }
529 Value::Array(arr) => {
530 let is_layout_array = path.contains("$layout") || path.contains("elements");
534 if !is_layout_array && arr.len() > 10 && !has_actionable_keys(value) {
535 return Ok(());
536 }
537 Ok(for (index, item) in arr.iter().enumerate() {
538 let next_path = if path == "#" {
539 format!("#/{index}")
540 } else {
541 format!("{path}/{index}")
542 };
543 walk_schema(
544 item,
545 &next_path,
546 engine,
547 evaluations,
548 tables,
549 deps,
550 value_fields,
551 layout_paths,
552 dependents,
553 options_templates,
554 subforms,
555 fields_with_rules,
556 conditional_hidden_fields,
557 conditional_readonly_fields,
558 )?;
559 })
560 }
561 _ => Ok(()),
562 }
563}
564pub fn collect_table_dependencies(
565 tables: &IndexMap<String, Value>,
566 dependencies: &mut IndexMap<String, IndexSet<String>>,
567) {
568 for (table_key, _) in tables.iter() {
569 let mut table_deps = IndexSet::new();
570
571 let table_data_prefix = path_utils::normalize_to_json_pointer(table_key)
572 .replace("/properties/", "/")
573 .trim_start_matches('#')
574 .to_string();
575 let table_data_prefix_slash = format!("{}/", table_data_prefix);
576
577 for (eval_key, deps) in dependencies.iter() {
578 let is_child = eval_key.len() > table_key.len()
579 && eval_key.starts_with(table_key.as_str())
580 && eval_key.as_bytes().get(table_key.len()) == Some(&b'/');
581
582 if is_child {
583 if eval_key.contains("/$datas/") {
584 continue;
585 }
586
587 for dep in deps {
588 let dep_data_path = path_utils::normalize_to_json_pointer(dep)
589 .replace("/properties/", "/")
590 .trim_start_matches('#')
591 .to_string();
592
593 if dep_data_path == table_data_prefix
594 || dep_data_path.starts_with(&table_data_prefix_slash)
595 {
596 continue;
597 }
598 let is_params_dep = dep.contains("$params");
599 let is_inline_system = !is_params_dep
600 && !dep.contains("$context")
601 && (dep.starts_with("/$") || dep.starts_with('$'));
602 if is_inline_system {
603 continue;
604 }
605 table_deps.insert(dep.clone());
606 }
607 }
608 }
609
610 if !table_deps.is_empty() {
611 dependencies.insert(table_key.clone(), table_deps);
612 }
613 }
614}
615
616pub fn categorize_evaluations(
617 sorted_evaluations: &[Vec<String>],
618 evaluations: &IndexMap<String, crate::LogicId>,
619 tables: &IndexMap<String, Value>,
620) -> (Vec<String>, Vec<String>) {
621 let batched_keys: IndexSet<String> = sorted_evaluations.iter().flatten().cloned().collect();
622
623 let mut rules_evaluations = Vec::new();
624 let mut others_evaluations = Vec::new();
625
626 for eval_key in evaluations.keys() {
627 if batched_keys.contains(eval_key) {
628 continue;
629 }
630
631 if tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
632 continue;
633 }
634
635 if eval_key.contains("/$params/") {
636 continue;
637 }
638
639 if eval_key.contains("/rules/") {
640 rules_evaluations.push(eval_key.clone());
641 } else if !eval_key.contains("/dependents/") {
642 others_evaluations.push(eval_key.clone());
643 }
644 }
645
646 (rules_evaluations, others_evaluations)
647}
648
649pub fn process_value_fields(
650 value_fields: Vec<String>,
651 tables: &IndexMap<String, Value>,
652) -> Vec<String> {
653 let mut value_evaluations = Vec::new();
654
655 for path in value_fields {
656 if value_evaluations.contains(&path) {
657 continue;
658 }
659
660 if path.contains("/$params/") || tables.iter().any(|(key, _)| path.starts_with(key)) {
661 continue;
662 }
663
664 value_evaluations.push(path);
665 }
666
667 value_evaluations
668}
669
670pub fn compile_table_metadata(
671 evaluations: &IndexMap<String, crate::LogicId>,
672 engine: &crate::RLogic,
673 eval_key: &str,
674 table: &Value,
675) -> Result<TableMetadata, String> {
676 let rows = table
677 .get("rows")
678 .and_then(|v| v.as_array())
679 .ok_or("table missing rows")?;
680 let empty_datas = Vec::new();
681 let datas = table
682 .get("datas")
683 .and_then(|v| v.as_array())
684 .unwrap_or(&empty_datas);
685
686 let mut data_plans = Vec::with_capacity(datas.len());
688 for (idx, entry) in datas.iter().enumerate() {
689 let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
690 continue;
691 };
692 let logic_path = format!("{eval_key}/$datas/{idx}/data");
693 let logic = evaluations.get(&logic_path).copied();
694 let literal = entry.get("data").map(|v| Arc::new(v.clone()));
695 data_plans.push((Arc::from(name), logic, literal));
696 }
697
698 let mut row_plans = Vec::with_capacity(rows.len());
700 for (row_idx, row_val) in rows.iter().enumerate() {
701 let Some(row_obj) = row_val.as_object() else {
702 continue;
703 };
704
705 if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
706 if repeat_arr.len() == 3 {
707 let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
708 let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
709 let start_logic = evaluations.get(&start_logic_path).copied();
710 let end_logic = evaluations.get(&end_logic_path).copied();
711
712 let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
713 let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
714
715 if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
716 let mut columns = Vec::with_capacity(template.len());
717 for (col_name, col_val) in template {
718 let col_eval_path =
719 format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
720 let logic = evaluations.get(&col_eval_path).copied();
721 let literal = if logic.is_none() {
722 Some(col_val.clone())
723 } else {
724 None
725 };
726
727 let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
729 let deps = engine
730 .get_referenced_vars(&logic_id)
731 .unwrap_or_default()
732 .into_iter()
733 .filter(|v| {
734 v.starts_with('$') && v != "$iteration" && v != "$threshold"
735 })
736 .collect();
737 let has_fwd = engine.has_forward_reference(&logic_id);
738 (deps, has_fwd)
739 } else {
740 (Vec::new(), false)
741 };
742
743 columns.push(ColumnMetadata::new(
744 col_name,
745 logic,
746 literal,
747 dependencies,
748 has_forward_ref,
749 ));
750 }
751
752 let (forward_cols, normal_cols) = compute_column_partitions(&columns);
754
755 row_plans.push(RowMetadata::Repeat {
756 start: RepeatBoundMetadata {
757 logic: start_logic,
758 literal: start_literal,
759 },
760 end: RepeatBoundMetadata {
761 logic: end_logic,
762 literal: end_literal,
763 },
764 columns: columns.into(),
765 forward_cols: forward_cols.into(),
766 normal_cols: normal_cols.into(),
767 });
768 continue;
769 }
770 }
771 }
772
773 let mut columns = Vec::with_capacity(row_obj.len());
775 for (col_name, col_val) in row_obj {
776 if col_name == "$repeat" {
777 continue;
778 }
779 let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
780 let logic = evaluations.get(&col_eval_path).copied();
781 let literal = if logic.is_none() {
782 Some(col_val.clone())
783 } else {
784 None
785 };
786
787 let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
789 let deps = engine
790 .get_referenced_vars(&logic_id)
791 .unwrap_or_default()
792 .into_iter()
793 .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
794 .collect();
795 let has_fwd = engine.has_forward_reference(&logic_id);
796 (deps, has_fwd)
797 } else {
798 (Vec::new(), false)
799 };
800
801 columns.push(ColumnMetadata::new(
802 col_name,
803 logic,
804 literal,
805 dependencies,
806 has_forward_ref,
807 ));
808 }
809 row_plans.push(RowMetadata::Static {
810 columns: columns.into(),
811 });
812 }
813
814 let skip_logic = evaluations.get(&format!("{eval_key}/$skip")).copied();
816 let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
817 let clear_logic = evaluations.get(&format!("{eval_key}/$clear")).copied();
818 let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
819
820 Ok(TableMetadata {
821 data_plans: data_plans.into(),
822 row_plans: row_plans.into(),
823 skip_logic,
824 skip_literal,
825 clear_logic,
826 clear_literal,
827 })
828}
829pub fn build_reffed_by(
830 dependencies: &IndexMap<String, IndexSet<String>>,
831) -> IndexMap<String, Vec<String>> {
832 let mut reffed_by: IndexMap<String, Vec<String>> = IndexMap::new();
833
834 for (eval_path, deps) in dependencies.iter() {
835 if eval_path.ends_with("/condition/hidden") {
836 let subject_path = eval_path[..eval_path.len() - 17].to_string();
837
838 for dep in deps {
839 let normalized_dep = path_utils::normalize_to_json_pointer(dep)
840 .replace("/properties/", "/")
841 .trim_start_matches('#')
842 .to_string();
843
844 let dep_key = if normalized_dep.starts_with('/') {
845 normalized_dep
846 } else {
847 format!("/{}", normalized_dep)
848 };
849
850 reffed_by
851 .entry(dep_key)
852 .or_insert_with(Vec::new)
853 .push(subject_path.clone());
854 }
855 }
856 }
857
858 reffed_by
859}
860
861pub fn build_dep_formula_triggers(
862 dependents_evaluations: &IndexMap<String, Vec<crate::DependentItem>>,
863 evaluations: &IndexMap<String, crate::LogicId>,
864 engine: &crate::RLogic,
865) -> IndexMap<String, Vec<(String, usize)>> {
866 let mut triggers: IndexMap<String, Vec<(String, usize)>> = IndexMap::new();
867
868 for (source_path, dep_items) in dependents_evaluations.iter() {
869 for (dep_idx, dep_item) in dep_items.iter().enumerate() {
870 let formula_keys: Vec<String> = [
871 dep_item
872 .value
873 .as_ref()
874 .and_then(|v| v.as_str())
875 .map(|s| s.to_string()),
876 dep_item
877 .clear
878 .as_ref()
879 .and_then(|v| v.as_str())
880 .map(|s| s.to_string()),
881 ]
882 .into_iter()
883 .flatten()
884 .filter(|k| k.contains("/dependents/"))
885 .collect();
886
887 for formula_key in formula_keys {
888 let logic_id = match evaluations.get(&formula_key).copied() {
889 Some(id) => id,
890 None => continue,
891 };
892
893 let refs = engine.get_referenced_vars(&logic_id).unwrap_or_default();
894
895 for dep_ref in refs {
896 let normalized = path_utils::normalize_to_json_pointer(&dep_ref)
897 .replace("/properties/", "/")
898 .trim_start_matches('#')
899 .to_string();
900 let dep_key = if normalized.starts_with('/') {
901 normalized
902 } else {
903 format!("/{}", normalized)
904 };
905
906 if dep_key.starts_with("/$") {
907 continue;
908 }
909
910 if dep_key.matches('/').count() <= 1 {
911 continue;
912 }
913
914 let source_data = path_utils::normalize_to_json_pointer(source_path)
915 .replace("/properties/", "/")
916 .trim_start_matches('#')
917 .to_string();
918 let source_data_key = if source_data.starts_with('/') {
919 source_data
920 } else {
921 format!("/{}", source_data)
922 };
923 if dep_key == source_data_key {
924 continue;
925 }
926
927 let target_data = path_utils::normalize_to_json_pointer(&dep_item.ref_path)
928 .replace("/properties/", "/")
929 .trim_start_matches('#')
930 .to_string();
931 let target_data_key = if target_data.starts_with('/') {
932 target_data
933 } else {
934 format!("/{}", target_data)
935 };
936 if dep_key == target_data_key {
937 continue;
938 }
939
940 let pair = (source_path.clone(), dep_idx);
941 let entry = triggers.entry(dep_key).or_insert_with(Vec::new);
942 if !entry.contains(&pair) {
943 entry.push(pair);
944 }
945 }
946 }
947 }
948 }
949
950 for sources in triggers.values_mut() {
951 sources.sort();
952 }
954
955 triggers
956}