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 (forward_indices, normal_indices)
191}
192
193pub fn walk_schema(
194 value: &Value,
195 path: &str,
196 engine: &mut RLogic,
197 evaluations: &mut IndexMap<String, LogicId>,
198 tables: &mut IndexMap<String, Value>,
199 deps: &mut IndexMap<String, IndexSet<String>>,
200 value_fields: &mut Vec<String>,
201 layout_paths: &mut Vec<String>,
202 dependents: &mut IndexMap<String, Vec<crate::DependentItem>>,
203 options_templates: &mut Vec<(String, String, String)>,
204 subforms: &mut Vec<(String, serde_json::Map<String, Value>, Value)>,
205 fields_with_rules: &mut Vec<String>,
206 conditional_hidden_fields: &mut Vec<String>,
207 conditional_readonly_fields: &mut Vec<String>,
208) -> Result<(), String> {
209 match value {
210 Value::Object(map) => {
211 if let Some(evaluation) = map.get("$evaluation") {
213 let key = path.to_string();
214 let logic_value = evaluation.get("logic").unwrap_or(evaluation);
215 let logic_id = engine
216 .compile(logic_value)
217 .map_err(|e| format!("failed to compile evaluation at {key}: {e}"))?;
218 evaluations.insert(key.clone(), logic_id);
219
220 let mut refs: IndexSet<String> = engine
222 .get_referenced_vars(&logic_id)
223 .unwrap_or_default()
224 .into_iter()
225 .map(|dep| path_utils::canonicalize_schema_path(&dep).into_owned())
226 .filter(|dep| {
227 dep.matches('/').count() > 1 || dep.starts_with("/$")
232 })
233 .collect();
234 let mut extra_refs = IndexSet::new();
235 collect_refs(logic_value, &mut extra_refs);
236 if !extra_refs.is_empty() {
237 refs.extend(extra_refs.into_iter());
238 }
239
240 let refs: IndexSet<String> = refs
242 .into_iter()
243 .filter_map(|dep| {
244 if let Some(table_idx) = dep.find("/$table/") {
246 let table_path = &dep[..table_idx];
247 Some(table_path.to_string())
248 } else {
249 Some(dep.to_string())
250 }
251 })
252 .collect();
253
254 if !refs.is_empty() {
255 deps.insert(key.clone(), refs);
256 }
257 }
258
259 if let Some(table) = map.get("$table") {
261 let key = path.to_string();
262
263 let rows = table.clone();
264 let datas = map
265 .get("$datas")
266 .cloned()
267 .unwrap_or_else(|| Value::Array(vec![]));
268 let skip = map.get("$skip").cloned().unwrap_or(Value::Bool(false));
269 let clear = map.get("$clear").cloned().unwrap_or(Value::Bool(false));
270
271 let mut table_entry = Map::new();
272 table_entry.insert("rows".to_string(), rows);
273 table_entry.insert("datas".to_string(), datas);
274 table_entry.insert("skip".to_string(), skip);
275 table_entry.insert("clear".to_string(), clear);
276
277 tables.insert(key, Value::Object(table_entry));
278 }
279
280 if let Some(layout_obj) = map.get("$layout") {
282 if let Some(Value::Array(_)) = layout_obj.get("elements") {
283 let layout_elements_path = format!("{}/$layout/elements", path);
284 layout_paths.push(layout_elements_path);
285 }
286 }
287
288 if map.contains_key("rules") && !path.is_empty() && !path.starts_with("#/$") {
290 let field_path = path
293 .trim_start_matches('#')
294 .replace("/properties/", ".")
295 .trim_start_matches('/')
296 .trim_start_matches('.')
297 .to_string();
298
299 if !field_path.is_empty() && !field_path.starts_with("$") {
300 fields_with_rules.push(field_path);
301 }
302 }
303
304 if let Some(Value::String(url)) = map.get("url") {
306 if url.contains('{') && url.contains('}') {
308 let url_path = path_utils::normalize_to_json_pointer(&format!("{}/url", path))
310 .into_owned();
311 let params_path =
312 path_utils::normalize_to_json_pointer(&format!("{}/params", path))
313 .into_owned();
314 options_templates.push((url_path, url.clone(), params_path));
315 }
316 }
317
318 if let Some(Value::String(type_str)) = map.get("type") {
320 if type_str == "array" {
321 if let Some(items) = map.get("items") {
322 subforms.push((path.to_string(), map.clone(), items.clone()));
324 }
325 }
326 }
327
328 if let Some(Value::Object(condition)) = map.get("condition") {
330 if condition.contains_key("hidden") {
332 conditional_hidden_fields.push(path.to_string());
333 }
334 if condition.contains_key("disabled") && map.contains_key("value") {
336 conditional_readonly_fields.push(path.to_string());
337 }
338 }
339
340 if let Some(Value::Array(dependents_arr)) = map.get("dependents") {
342 let mut dependent_items = Vec::new();
343
344 for (dep_idx, dep_item) in dependents_arr.iter().enumerate() {
345 if let Value::Object(dep_obj) = dep_item {
346 if let Some(Value::String(ref_path)) = dep_obj.get("$ref") {
347 let clear_val = if let Some(clear) = dep_obj.get("clear") {
349 if let Value::Object(clear_obj) = clear {
350 if clear_obj.contains_key("$evaluation") {
351 let clear_eval = clear_obj.get("$evaluation").unwrap();
353 let clear_key =
354 format!("{}/dependents/{}/clear", path, dep_idx);
355 let logic_id = engine.compile(clear_eval).map_err(|e| {
356 format!(
357 "Failed to compile dependent clear at {}: {}",
358 clear_key, e
359 )
360 })?;
361 evaluations.insert(clear_key.clone(), logic_id);
362 Some(Value::String(clear_key))
364 } else {
365 Some(clear.clone())
366 }
367 } else {
368 Some(clear.clone())
369 }
370 } else {
371 None
372 };
373
374 let value_val = if let Some(value) = dep_obj.get("value") {
376 if let Value::Object(value_obj) = value {
377 if value_obj.contains_key("$evaluation") {
378 let value_eval = value_obj.get("$evaluation").unwrap();
380 let value_key =
381 format!("{}/dependents/{}/value", path, dep_idx);
382 let logic_id = engine.compile(value_eval).map_err(|e| {
383 format!(
384 "Failed to compile dependent value at {}: {}",
385 value_key, e
386 )
387 })?;
388 evaluations.insert(value_key.clone(), logic_id);
389 Some(Value::String(value_key))
391 } else {
392 Some(value.clone())
393 }
394 } else {
395 Some(value.clone())
396 }
397 } else {
398 None
399 };
400
401 dependent_items.push(crate::DependentItem {
402 ref_path: ref_path.clone(),
403 clear: clear_val,
404 value: value_val,
405 });
406 }
407 }
408 }
409
410 if !dependent_items.is_empty() {
411 dependents.insert(path.to_string(), dependent_items);
412 }
413 }
414
415 Ok(for (key, val) in map {
417 if key == "$evaluation"
419 || key == "dependents"
420 || (key == "items" && map.get("type").and_then(Value::as_str) == Some("array"))
421 {
422 continue;
423 }
424
425 let next_path = if path == "#" {
426 format!("#/{key}")
427 } else {
428 format!("{path}/{key}")
429 };
430
431 let is_excluded_special_path = next_path.contains("/$layout/")
434 || next_path.contains("/$items/")
435 || next_path.contains("/$options/")
436 || next_path.contains("/$dependents/")
437 || next_path.contains("/$rules/");
438
439 if key == "value" && !is_excluded_special_path {
440 value_fields.push(next_path.clone());
441 }
442
443 walk_schema(
445 val,
446 &next_path,
447 engine,
448 evaluations,
449 tables,
450 deps,
451 value_fields,
452 layout_paths,
453 dependents,
454 options_templates,
455 subforms,
456 fields_with_rules,
457 conditional_hidden_fields,
458 conditional_readonly_fields,
459 )?;
460 })
461 }
462 Value::Array(arr) => {
463 let is_layout_array = path.contains("$layout") || path.contains("elements");
467 if !is_layout_array && arr.len() > 10 && !has_actionable_keys(value) {
468 return Ok(());
469 }
470 Ok(for (index, item) in arr.iter().enumerate() {
471 let next_path = if path == "#" {
472 format!("#/{index}")
473 } else {
474 format!("{path}/{index}")
475 };
476 walk_schema(
477 item,
478 &next_path,
479 engine,
480 evaluations,
481 tables,
482 deps,
483 value_fields,
484 layout_paths,
485 dependents,
486 options_templates,
487 subforms,
488 fields_with_rules,
489 conditional_hidden_fields,
490 conditional_readonly_fields,
491 )?;
492 })
493 }
494 _ => Ok(()),
495 }
496}
497pub fn collect_table_dependencies(
498 tables: &IndexMap<String, Value>,
499 dependencies: &mut IndexMap<String, IndexSet<String>>,
500) {
501 for (table_key, _) in tables.iter() {
502 let mut table_deps = IndexSet::new();
503
504 let table_data_prefix = path_utils::normalize_to_json_pointer(table_key)
505 .replace("/properties/", "/")
506 .trim_start_matches('#')
507 .to_string();
508 let table_data_prefix_slash = format!("{}/", table_data_prefix);
509
510 for (eval_key, deps) in dependencies.iter() {
511 let is_child = eval_key.len() > table_key.len()
512 && eval_key.starts_with(table_key.as_str())
513 && eval_key.as_bytes().get(table_key.len()) == Some(&b'/');
514
515 if is_child {
516 if eval_key.contains("/$datas/") {
517 continue;
518 }
519
520 for dep in deps {
521 let dep_data_path = path_utils::normalize_to_json_pointer(dep)
522 .replace("/properties/", "/")
523 .trim_start_matches('#')
524 .to_string();
525
526 if dep_data_path == table_data_prefix
527 || dep_data_path.starts_with(&table_data_prefix_slash)
528 {
529 continue;
530 }
531 let is_params_dep = dep.contains("$params");
532 let is_inline_system = !is_params_dep
533 && !dep.contains("$context")
534 && (dep.starts_with("/$") || dep.starts_with('$'));
535 if is_inline_system {
536 continue;
537 }
538 table_deps.insert(dep.clone());
539 }
540 }
541 }
542
543 if !table_deps.is_empty() {
544 dependencies.insert(table_key.clone(), table_deps);
545 }
546 }
547}
548
549pub fn categorize_evaluations(
550 sorted_evaluations: &[Vec<String>],
551 evaluations: &IndexMap<String, crate::LogicId>,
552 tables: &IndexMap<String, Value>,
553) -> (Vec<String>, Vec<String>) {
554 let batched_keys: IndexSet<String> = sorted_evaluations.iter().flatten().cloned().collect();
555
556 let mut rules_evaluations = Vec::new();
557 let mut others_evaluations = Vec::new();
558
559 for eval_key in evaluations.keys() {
560 if batched_keys.contains(eval_key) {
561 continue;
562 }
563
564 if tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
565 continue;
566 }
567
568 if eval_key.contains("/$params/") {
569 continue;
570 }
571
572 if eval_key.contains("/rules/") {
573 rules_evaluations.push(eval_key.clone());
574 } else if !eval_key.contains("/dependents/") {
575 others_evaluations.push(eval_key.clone());
576 }
577 }
578
579 (rules_evaluations, others_evaluations)
580}
581
582pub fn process_value_fields(
583 value_fields: Vec<String>,
584 tables: &IndexMap<String, Value>,
585) -> Vec<String> {
586 let mut value_evaluations = Vec::new();
587
588 for path in value_fields {
589 if value_evaluations.contains(&path) {
590 continue;
591 }
592
593 if path.contains("/$params/") || tables.iter().any(|(key, _)| path.starts_with(key)) {
594 continue;
595 }
596
597 value_evaluations.push(path);
598 }
599
600 value_evaluations
601}
602
603pub fn compile_table_metadata(
604 evaluations: &IndexMap<String, crate::LogicId>,
605 engine: &crate::RLogic,
606 eval_key: &str,
607 table: &Value,
608) -> Result<TableMetadata, String> {
609 let rows = table
610 .get("rows")
611 .and_then(|v| v.as_array())
612 .ok_or("table missing rows")?;
613 let empty_datas = Vec::new();
614 let datas = table
615 .get("datas")
616 .and_then(|v| v.as_array())
617 .unwrap_or(&empty_datas);
618
619 let mut data_plans = Vec::with_capacity(datas.len());
621 for (idx, entry) in datas.iter().enumerate() {
622 let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
623 continue;
624 };
625 let logic_path = format!("{eval_key}/$datas/{idx}/data");
626 let logic = evaluations.get(&logic_path).copied();
627 let literal = entry.get("data").map(|v| Arc::new(v.clone()));
628 data_plans.push((Arc::from(name), logic, literal));
629 }
630
631 let mut row_plans = Vec::with_capacity(rows.len());
633 for (row_idx, row_val) in rows.iter().enumerate() {
634 let Some(row_obj) = row_val.as_object() else {
635 continue;
636 };
637
638 if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
639 if repeat_arr.len() == 3 {
640 let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
641 let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
642 let start_logic = evaluations.get(&start_logic_path).copied();
643 let end_logic = evaluations.get(&end_logic_path).copied();
644
645 let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
646 let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
647
648 if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
649 let mut columns = Vec::with_capacity(template.len());
650 for (col_name, col_val) in template {
651 let col_eval_path =
652 format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
653 let logic = evaluations.get(&col_eval_path).copied();
654 let literal = if logic.is_none() {
655 Some(col_val.clone())
656 } else {
657 None
658 };
659
660 let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
662 let deps = engine
663 .get_referenced_vars(&logic_id)
664 .unwrap_or_default()
665 .into_iter()
666 .filter(|v| {
667 v.starts_with('$') && v != "$iteration" && v != "$threshold"
668 })
669 .collect();
670 let has_fwd = engine.has_forward_reference(&logic_id);
671 (deps, has_fwd)
672 } else {
673 (Vec::new(), false)
674 };
675
676 columns.push(ColumnMetadata::new(
677 col_name,
678 logic,
679 literal,
680 dependencies,
681 has_forward_ref,
682 ));
683 }
684
685 let (forward_cols, normal_cols) = compute_column_partitions(&columns);
687
688 row_plans.push(RowMetadata::Repeat {
689 start: RepeatBoundMetadata {
690 logic: start_logic,
691 literal: start_literal,
692 },
693 end: RepeatBoundMetadata {
694 logic: end_logic,
695 literal: end_literal,
696 },
697 columns: columns.into(),
698 forward_cols: forward_cols.into(),
699 normal_cols: normal_cols.into(),
700 });
701 continue;
702 }
703 }
704 }
705
706 let mut columns = Vec::with_capacity(row_obj.len());
708 for (col_name, col_val) in row_obj {
709 if col_name == "$repeat" {
710 continue;
711 }
712 let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
713 let logic = evaluations.get(&col_eval_path).copied();
714 let literal = if logic.is_none() {
715 Some(col_val.clone())
716 } else {
717 None
718 };
719
720 let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
722 let deps = engine
723 .get_referenced_vars(&logic_id)
724 .unwrap_or_default()
725 .into_iter()
726 .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
727 .collect();
728 let has_fwd = engine.has_forward_reference(&logic_id);
729 (deps, has_fwd)
730 } else {
731 (Vec::new(), false)
732 };
733
734 columns.push(ColumnMetadata::new(
735 col_name,
736 logic,
737 literal,
738 dependencies,
739 has_forward_ref,
740 ));
741 }
742 row_plans.push(RowMetadata::Static {
743 columns: columns.into(),
744 });
745 }
746
747 let skip_logic = evaluations.get(&format!("{eval_key}/$skip")).copied();
749 let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
750 let clear_logic = evaluations.get(&format!("{eval_key}/$clear")).copied();
751 let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
752
753 Ok(TableMetadata {
754 data_plans: data_plans.into(),
755 row_plans: row_plans.into(),
756 skip_logic,
757 skip_literal,
758 clear_logic,
759 clear_literal,
760 })
761}
762pub fn build_reffed_by(
763 dependencies: &IndexMap<String, IndexSet<String>>,
764) -> IndexMap<String, Vec<String>> {
765 let mut reffed_by: IndexMap<String, Vec<String>> = IndexMap::new();
766
767 for (eval_path, deps) in dependencies.iter() {
768 if eval_path.ends_with("/condition/hidden") {
769 let subject_path = eval_path[..eval_path.len() - 17].to_string();
770
771 for dep in deps {
772 let normalized_dep = path_utils::normalize_to_json_pointer(dep)
773 .replace("/properties/", "/")
774 .trim_start_matches('#')
775 .to_string();
776
777 let dep_key = if normalized_dep.starts_with('/') {
778 normalized_dep
779 } else {
780 format!("/{}", normalized_dep)
781 };
782
783 reffed_by
784 .entry(dep_key)
785 .or_insert_with(Vec::new)
786 .push(subject_path.clone());
787 }
788 }
789 }
790
791 reffed_by
792}
793
794pub fn build_dep_formula_triggers(
795 dependents_evaluations: &IndexMap<String, Vec<crate::DependentItem>>,
796 evaluations: &IndexMap<String, crate::LogicId>,
797 engine: &crate::RLogic,
798) -> IndexMap<String, Vec<(String, usize)>> {
799 let mut triggers: IndexMap<String, Vec<(String, usize)>> = IndexMap::new();
800
801 for (source_path, dep_items) in dependents_evaluations.iter() {
802 for (dep_idx, dep_item) in dep_items.iter().enumerate() {
803 let formula_keys: Vec<String> = [
804 dep_item
805 .value
806 .as_ref()
807 .and_then(|v| v.as_str())
808 .map(|s| s.to_string()),
809 dep_item
810 .clear
811 .as_ref()
812 .and_then(|v| v.as_str())
813 .map(|s| s.to_string()),
814 ]
815 .into_iter()
816 .flatten()
817 .filter(|k| k.contains("/dependents/"))
818 .collect();
819
820 for formula_key in formula_keys {
821 let logic_id = match evaluations.get(&formula_key).copied() {
822 Some(id) => id,
823 None => continue,
824 };
825
826 let refs = engine.get_referenced_vars(&logic_id).unwrap_or_default();
827
828 for dep_ref in refs {
829 let normalized = path_utils::normalize_to_json_pointer(&dep_ref)
830 .replace("/properties/", "/")
831 .trim_start_matches('#')
832 .to_string();
833 let dep_key = if normalized.starts_with('/') {
834 normalized
835 } else {
836 format!("/{}", normalized)
837 };
838
839 if dep_key.starts_with("/$") {
840 continue;
841 }
842
843 if dep_key.matches('/').count() <= 1 {
844 continue;
845 }
846
847 let source_data = path_utils::normalize_to_json_pointer(source_path)
848 .replace("/properties/", "/")
849 .trim_start_matches('#')
850 .to_string();
851 let source_data_key = if source_data.starts_with('/') {
852 source_data
853 } else {
854 format!("/{}", source_data)
855 };
856 if dep_key == source_data_key {
857 continue;
858 }
859
860 let target_data = path_utils::normalize_to_json_pointer(&dep_item.ref_path)
861 .replace("/properties/", "/")
862 .trim_start_matches('#')
863 .to_string();
864 let target_data_key = if target_data.starts_with('/') {
865 target_data
866 } else {
867 format!("/{}", target_data)
868 };
869 if dep_key == target_data_key {
870 continue;
871 }
872
873 let pair = (source_path.clone(), dep_idx);
874 let entry = triggers.entry(dep_key).or_insert_with(Vec::new);
875 if !entry.contains(&pair) {
876 entry.push(pair);
877 }
878 }
879 }
880 }
881 }
882
883 for sources in triggers.values_mut() {
884 sources.sort();
885 }
887
888 triggers
889}