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_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#[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 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 if map.contains_key("rules") {
71 return true;
72 }
73
74 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 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
95pub fn compute_column_partitions(columns: &[ColumnMetadata]) -> (Vec<usize>, Vec<usize>) {
100 use std::collections::HashSet;
101
102 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 loop {
112 let mut changed = false;
113 for col in columns {
114 if !fwd_cols.contains(col.name.as_ref()) {
115 for dep in col.dependencies.iter() {
117 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 if !changed {
129 break;
130 }
131 }
132
133 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 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 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 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 let refs: IndexSet<String> = refs
197 .into_iter()
198 .filter_map(|dep| {
199 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 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 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 if map.contains_key("rules") && !path.is_empty() && !path.starts_with("#/$") {
245 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 if let Some(Value::String(url)) = map.get("url") {
261 if url.contains('{') && url.contains('}') {
263 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 if let Some(Value::String(type_str)) = map.get("type") {
275 if type_str == "array" {
276 if let Some(items) = map.get("items") {
277 subforms.push((path.to_string(), map.clone(), items.clone()));
279 }
280 }
281 }
282
283 if let Some(Value::Object(condition)) = map.get("condition") {
285 if condition.contains_key("hidden") {
287 conditional_hidden_fields.push(path.to_string());
288 }
289 if condition.contains_key("disabled") && map.contains_key("value") {
291 conditional_readonly_fields.push(path.to_string());
292 }
293 }
294
295 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 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 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 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 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 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 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 Ok(for (key, val) in map {
372 if key == "$evaluation"
374 || key == "dependents"
375 || (key == "items" && map.get("type").and_then(Value::as_str) == Some("array"))
376 {
377 continue;
378 }
379
380 let next_path = if path == "#" {
381 format!("#/{key}")
382 } else {
383 format!("{path}/{key}")
384 };
385
386 let is_excluded_special_path = next_path.contains("/$layout/")
389 || next_path.contains("/$items/")
390 || next_path.contains("/$options/")
391 || next_path.contains("/$dependents/")
392 || next_path.contains("/$rules/");
393
394 if key == "value" && !is_excluded_special_path {
395 value_fields.push(next_path.clone());
396 }
397
398 walk_schema(
400 val,
401 &next_path,
402 engine,
403 evaluations,
404 tables,
405 deps,
406 value_fields,
407 layout_paths,
408 dependents,
409 options_templates,
410 subforms,
411 fields_with_rules,
412 conditional_hidden_fields,
413 conditional_readonly_fields,
414 )?;
415 })
416 }
417 Value::Array(arr) => {
418 let is_layout_array = path.contains("$layout") || path.contains("elements");
422 if !is_layout_array && arr.len() > 10 && !has_actionable_keys(value) {
423 return Ok(());
424 }
425 Ok(for (index, item) in arr.iter().enumerate() {
426 let next_path = if path == "#" {
427 format!("#/{index}")
428 } else {
429 format!("{path}/{index}")
430 };
431 walk_schema(
432 item,
433 &next_path,
434 engine,
435 evaluations,
436 tables,
437 deps,
438 value_fields,
439 layout_paths,
440 dependents,
441 options_templates,
442 subforms,
443 fields_with_rules,
444 conditional_hidden_fields,
445 conditional_readonly_fields,
446 )?;
447 })
448 }
449 _ => Ok(()),
450 }
451}
452pub fn collect_table_dependencies(
453 tables: &IndexMap<String, Value>,
454 dependencies: &mut IndexMap<String, IndexSet<String>>,
455) {
456 for (table_key, _) in tables.iter() {
457 let mut table_deps = IndexSet::new();
458
459 let table_data_prefix = path_utils::normalize_to_json_pointer(table_key)
460 .replace("/properties/", "/")
461 .trim_start_matches('#')
462 .to_string();
463 let table_data_prefix_slash = format!("{}/", table_data_prefix);
464
465 for (eval_key, deps) in dependencies.iter() {
466 let is_child = eval_key.len() > table_key.len()
467 && eval_key.starts_with(table_key.as_str())
468 && eval_key.as_bytes().get(table_key.len()) == Some(&b'/');
469
470 if is_child {
471 if eval_key.contains("/$datas/") {
472 continue;
473 }
474
475 for dep in deps {
476 let dep_data_path = path_utils::normalize_to_json_pointer(dep)
477 .replace("/properties/", "/")
478 .trim_start_matches('#')
479 .to_string();
480
481 if dep_data_path == table_data_prefix
482 || dep_data_path.starts_with(&table_data_prefix_slash)
483 {
484 continue;
485 }
486 let is_params_dep = dep.contains("$params");
487 let is_inline_system = !is_params_dep
488 && !dep.contains("$context")
489 && (dep.starts_with("/$") || dep.starts_with('$'));
490 if is_inline_system {
491 continue;
492 }
493 table_deps.insert(dep.clone());
494 }
495 }
496 }
497
498 if !table_deps.is_empty() {
499 dependencies.insert(table_key.clone(), table_deps);
500 }
501 }
502}
503
504pub fn categorize_evaluations(
505 sorted_evaluations: &[Vec<String>],
506 evaluations: &IndexMap<String, crate::LogicId>,
507 tables: &IndexMap<String, Value>,
508) -> (Vec<String>, Vec<String>) {
509 let batched_keys: IndexSet<String> = sorted_evaluations.iter().flatten().cloned().collect();
510
511 let mut rules_evaluations = Vec::new();
512 let mut others_evaluations = Vec::new();
513
514 for eval_key in evaluations.keys() {
515 if batched_keys.contains(eval_key) {
516 continue;
517 }
518
519 if tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
520 continue;
521 }
522
523 if eval_key.contains("/$params/") {
524 continue;
525 }
526
527 if eval_key.contains("/rules/") {
528 rules_evaluations.push(eval_key.clone());
529 } else if !eval_key.contains("/dependents/") {
530 others_evaluations.push(eval_key.clone());
531 }
532 }
533
534 (rules_evaluations, others_evaluations)
535}
536
537pub fn process_value_fields(
538 value_fields: Vec<String>,
539 tables: &IndexMap<String, Value>,
540) -> Vec<String> {
541 let mut value_evaluations = Vec::new();
542
543 for path in value_fields {
544 if value_evaluations.contains(&path) {
545 continue;
546 }
547
548 if path.contains("/$params/") || tables.iter().any(|(key, _)| path.starts_with(key)) {
549 continue;
550 }
551
552 value_evaluations.push(path);
553 }
554
555 value_evaluations
556}
557
558pub fn compile_table_metadata(
559 evaluations: &IndexMap<String, crate::LogicId>,
560 engine: &crate::RLogic,
561 eval_key: &str,
562 table: &Value,
563) -> Result<TableMetadata, String> {
564 let rows = table
565 .get("rows")
566 .and_then(|v| v.as_array())
567 .ok_or("table missing rows")?;
568 let empty_datas = Vec::new();
569 let datas = table
570 .get("datas")
571 .and_then(|v| v.as_array())
572 .unwrap_or(&empty_datas);
573
574 let mut data_plans = Vec::with_capacity(datas.len());
576 for (idx, entry) in datas.iter().enumerate() {
577 let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
578 continue;
579 };
580 let logic_path = format!("{eval_key}/$datas/{idx}/data");
581 let logic = evaluations.get(&logic_path).copied();
582 let literal = entry.get("data").map(|v| Arc::new(v.clone()));
583 data_plans.push((Arc::from(name), logic, literal));
584 }
585
586 let mut row_plans = Vec::with_capacity(rows.len());
588 for (row_idx, row_val) in rows.iter().enumerate() {
589 let Some(row_obj) = row_val.as_object() else {
590 continue;
591 };
592
593 if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
594 if repeat_arr.len() == 3 {
595 let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
596 let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
597 let start_logic = evaluations.get(&start_logic_path).copied();
598 let end_logic = evaluations.get(&end_logic_path).copied();
599
600 let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
601 let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
602
603 if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
604 let mut columns = Vec::with_capacity(template.len());
605 for (col_name, col_val) in template {
606 let col_eval_path =
607 format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
608 let logic = evaluations.get(&col_eval_path).copied();
609 let literal = if logic.is_none() {
610 Some(col_val.clone())
611 } else {
612 None
613 };
614
615 let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
617 let deps = engine
618 .get_referenced_vars(&logic_id)
619 .unwrap_or_default()
620 .into_iter()
621 .filter(|v| {
622 v.starts_with('$') && v != "$iteration" && v != "$threshold"
623 })
624 .collect();
625 let has_fwd = engine.has_forward_reference(&logic_id);
626 (deps, has_fwd)
627 } else {
628 (Vec::new(), false)
629 };
630
631 columns.push(ColumnMetadata::new(
632 col_name,
633 logic,
634 literal,
635 dependencies,
636 has_forward_ref,
637 ));
638 }
639
640 let (forward_cols, normal_cols) = compute_column_partitions(&columns);
642
643 row_plans.push(RowMetadata::Repeat {
644 start: RepeatBoundMetadata {
645 logic: start_logic,
646 literal: start_literal,
647 },
648 end: RepeatBoundMetadata {
649 logic: end_logic,
650 literal: end_literal,
651 },
652 columns: columns.into(),
653 forward_cols: forward_cols.into(),
654 normal_cols: normal_cols.into(),
655 });
656 continue;
657 }
658 }
659 }
660
661 let mut columns = Vec::with_capacity(row_obj.len());
663 for (col_name, col_val) in row_obj {
664 if col_name == "$repeat" {
665 continue;
666 }
667 let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
668 let logic = evaluations.get(&col_eval_path).copied();
669 let literal = if logic.is_none() {
670 Some(col_val.clone())
671 } else {
672 None
673 };
674
675 let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
677 let deps = engine
678 .get_referenced_vars(&logic_id)
679 .unwrap_or_default()
680 .into_iter()
681 .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
682 .collect();
683 let has_fwd = engine.has_forward_reference(&logic_id);
684 (deps, has_fwd)
685 } else {
686 (Vec::new(), false)
687 };
688
689 columns.push(ColumnMetadata::new(
690 col_name,
691 logic,
692 literal,
693 dependencies,
694 has_forward_ref,
695 ));
696 }
697 row_plans.push(RowMetadata::Static {
698 columns: columns.into(),
699 });
700 }
701
702 let skip_logic = evaluations.get(&format!("{eval_key}/$skip")).copied();
704 let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
705 let clear_logic = evaluations.get(&format!("{eval_key}/$clear")).copied();
706 let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
707
708 Ok(TableMetadata {
709 data_plans: data_plans.into(),
710 row_plans: row_plans.into(),
711 skip_logic,
712 skip_literal,
713 clear_logic,
714 clear_literal,
715 })
716}
717pub fn build_reffed_by(
718 dependencies: &IndexMap<String, IndexSet<String>>,
719) -> IndexMap<String, Vec<String>> {
720 let mut reffed_by: IndexMap<String, Vec<String>> = IndexMap::new();
721
722 for (eval_path, deps) in dependencies.iter() {
723 if eval_path.ends_with("/condition/hidden") {
724 let subject_path = eval_path[..eval_path.len() - 17].to_string();
725
726 for dep in deps {
727 let normalized_dep = path_utils::normalize_to_json_pointer(dep)
728 .replace("/properties/", "/")
729 .trim_start_matches('#')
730 .to_string();
731
732 let dep_key = if normalized_dep.starts_with('/') {
733 normalized_dep
734 } else {
735 format!("/{}", normalized_dep)
736 };
737
738 reffed_by
739 .entry(dep_key)
740 .or_insert_with(Vec::new)
741 .push(subject_path.clone());
742 }
743 }
744 }
745
746 reffed_by
747}
748
749pub fn build_dep_formula_triggers(
750 dependents_evaluations: &IndexMap<String, Vec<crate::DependentItem>>,
751 evaluations: &IndexMap<String, crate::LogicId>,
752 engine: &crate::RLogic,
753) -> IndexMap<String, Vec<(String, usize)>> {
754 let mut triggers: IndexMap<String, Vec<(String, usize)>> = IndexMap::new();
755
756 for (source_path, dep_items) in dependents_evaluations.iter() {
757 for (dep_idx, dep_item) in dep_items.iter().enumerate() {
758 let formula_keys: Vec<String> = [
759 dep_item
760 .value
761 .as_ref()
762 .and_then(|v| v.as_str())
763 .map(|s| s.to_string()),
764 dep_item
765 .clear
766 .as_ref()
767 .and_then(|v| v.as_str())
768 .map(|s| s.to_string()),
769 ]
770 .into_iter()
771 .flatten()
772 .filter(|k| k.contains("/dependents/"))
773 .collect();
774
775 for formula_key in formula_keys {
776 let logic_id = match evaluations.get(&formula_key).copied() {
777 Some(id) => id,
778 None => continue,
779 };
780
781 let refs = engine.get_referenced_vars(&logic_id).unwrap_or_default();
782
783 for dep_ref in refs {
784 let normalized = path_utils::normalize_to_json_pointer(&dep_ref)
785 .replace("/properties/", "/")
786 .trim_start_matches('#')
787 .to_string();
788 let dep_key = if normalized.starts_with('/') {
789 normalized
790 } else {
791 format!("/{}", normalized)
792 };
793
794 if dep_key.starts_with("/$") {
795 continue;
796 }
797
798 if dep_key.matches('/').count() <= 1 {
799 continue;
800 }
801
802 let source_data = path_utils::normalize_to_json_pointer(source_path)
803 .replace("/properties/", "/")
804 .trim_start_matches('#')
805 .to_string();
806 let source_data_key = if source_data.starts_with('/') {
807 source_data
808 } else {
809 format!("/{}", source_data)
810 };
811 if dep_key == source_data_key {
812 continue;
813 }
814
815 let target_data = path_utils::normalize_to_json_pointer(&dep_item.ref_path)
816 .replace("/properties/", "/")
817 .trim_start_matches('#')
818 .to_string();
819 let target_data_key = if target_data.starts_with('/') {
820 target_data
821 } else {
822 format!("/{}", target_data)
823 };
824 if dep_key == target_data_key {
825 continue;
826 }
827
828 let pair = (source_path.clone(), dep_idx);
829 let entry = triggers.entry(dep_key).or_insert_with(Vec::new);
830 if !entry.contains(&pair) {
831 entry.push(pair);
832 }
833 }
834 }
835 }
836 }
837
838 for sources in triggers.values_mut() {
839 sources.sort();
840 }
842
843 triggers
844}