Skip to main content

json_eval_rs/parse_schema/
common.rs

1use crate::jsoneval::path_utils;
2use crate::jsoneval::table_metadata::ColumnMetadata;
3/// Shared utilities for schema parsing (used by both legacy and parsed implementations)
4use indexmap::IndexSet;
5use serde_json::Value;
6
7/// Collect $ref dependencies from a JSON value recursively
8pub fn collect_refs(value: &Value, refs: &mut IndexSet<String>) {
9    match value {
10        Value::Object(map) => {
11            if let Some(path) = map.get("$ref").and_then(Value::as_str) {
12                refs.insert(path_utils::normalize_to_json_pointer(path));
13            }
14            if let Some(path) = map.get("ref").and_then(Value::as_str) {
15                refs.insert(path_utils::normalize_to_json_pointer(path));
16            }
17            if let Some(var_val) = map.get("var") {
18                match var_val {
19                    Value::String(s) => {
20                        refs.insert(s.clone());
21                    }
22                    Value::Array(arr) => {
23                        if let Some(path) = arr.get(0).and_then(Value::as_str) {
24                            refs.insert(path.to_string());
25                        }
26                    }
27                    _ => {}
28                }
29            }
30            for val in map.values() {
31                collect_refs(val, refs);
32            }
33        }
34        Value::Array(arr) => {
35            for val in arr {
36                collect_refs(val, refs);
37            }
38        }
39        _ => {}
40    }
41}
42
43/// Check if a value contains any actionable schema keys recursively (with depth limit for arrays)
44/// used to skip large pure-data arrays during schema walking
45#[inline]
46pub fn has_actionable_keys(value: &Value) -> bool {
47    match value {
48        Value::Object(map) => {
49            if map.contains_key("$evaluation")
50                || map.contains_key("$table")
51                || map.contains_key("dependents")
52                || map.contains_key("$layout")
53            {
54                return true;
55            }
56
57            // Check for conditional hidden/disabled fields
58            if let Some(Value::Object(condition)) = map.get("condition") {
59                if condition.contains_key("hidden") || condition.contains_key("disabled") {
60                    return true;
61                }
62            }
63
64            // Check for rules object
65            if map.contains_key("rules") {
66                return true;
67            }
68
69            // Check for type="array" with items (subforms)
70            if let Some(Value::String(type_str)) = map.get("type") {
71                if type_str == "array" && map.contains_key("items") {
72                    return true;
73                }
74            }
75
76            // Check for options with URL templates
77            if let Some(Value::String(url)) = map.get("url") {
78                if url.contains('{') && url.contains('}') {
79                    return true;
80                }
81            }
82
83            map.values().any(has_actionable_keys)
84        }
85        Value::Array(arr) => arr.iter().take(5).any(has_actionable_keys),
86        _ => false,
87    }
88}
89
90/// Compute forward/normal column partitions with transitive closure
91///
92/// This function identifies which columns have forward references (dependencies on later columns)
93/// and separates them from normal columns for proper evaluation order.
94pub fn compute_column_partitions(columns: &[ColumnMetadata]) -> (Vec<usize>, Vec<usize>) {
95    use std::collections::HashSet;
96
97    // Build set of all forward-referencing column names (direct + transitive)
98    let mut fwd_cols = HashSet::new();
99    for col in columns {
100        if col.has_forward_ref {
101            fwd_cols.insert(col.name.as_ref());
102        }
103    }
104
105    // Transitive closure: any column that depends on forward columns is also forward
106    loop {
107        let mut changed = false;
108        for col in columns {
109            if !fwd_cols.contains(col.name.as_ref()) {
110                // Check if this column depends on any forward column
111                for dep in col.dependencies.iter() {
112                    // Strip $ prefix from dependency name for comparison
113                    let dep_name = dep.trim_start_matches('$');
114                    if fwd_cols.contains(dep_name) {
115                        fwd_cols.insert(col.name.as_ref());
116                        changed = true;
117                        break;
118                    }
119                }
120            }
121        }
122        // Stop when no more changes
123        if !changed {
124            break;
125        }
126    }
127
128    // Separate into forward and normal indices
129    let mut forward_indices = Vec::new();
130    let mut normal_indices = Vec::new();
131
132    for (idx, col) in columns.iter().enumerate() {
133        if fwd_cols.contains(col.name.as_ref()) {
134            forward_indices.push(idx);
135        } else {
136            normal_indices.push(idx);
137        }
138    }
139
140    (forward_indices, normal_indices)
141}