1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
//! Parsed Schema - Reusable parsing results for caching across multiple JSONEval instances
//!
//! This module separates the parsing results from the evaluation state, allowing
//! schemas to be parsed once and reused across multiple evaluations with different data/context.
use crate::{DependentItem, LogicId, RLogic, RLogicConfig, TableMetadata};
use indexmap::{IndexMap, IndexSet};
use serde_json::Value;
use std::sync::Arc;
/// Parsed schema containing all pre-compiled evaluation metadata.
/// This structure is separate from JSONEval to enable caching and reuse.
///
/// # Caching Strategy
///
/// Wrap ParsedSchema in Arc for sharing across threads and caching:
///
/// ```ignore
/// use std::sync::Arc;
///
/// // Parse once and wrap in Arc for caching
/// let parsed = Arc::new(ParsedSchema::parse(schema_str)?);
/// cache.insert(schema_key, parsed.clone());
///
/// // Reuse across multiple evaluations (Arc::clone is cheap)
/// let eval1 = JSONEval::with_parsed_schema(parsed.clone(), Some(context1), Some(data1))?;
/// let eval2 = JSONEval::with_parsed_schema(parsed.clone(), Some(context2), Some(data2))?;
/// ```
pub struct ParsedSchema {
/// The original schema Value (wrapped in Arc for efficient sharing)
pub schema: Arc<Value>,
/// RLogic engine with all compiled logic expressions (wrapped in Arc for sharing)
/// Multiple JSONEval instances created from the same ParsedSchema will share this engine
pub engine: Arc<RLogic>,
/// Map of evaluation keys to compiled logic IDs (wrapped in Arc for zero-copy sharing)
pub evaluations: Arc<IndexMap<String, LogicId>>,
/// Table definitions (rows, datas, skip, clear) (wrapped in Arc for zero-copy sharing)
pub tables: Arc<IndexMap<String, Value>>,
/// Pre-compiled table metadata (computed at parse time for zero-copy evaluation)
pub table_metadata: Arc<IndexMap<String, TableMetadata>>,
/// Dependencies map (evaluation key -> set of dependency paths) (wrapped in Arc for zero-copy sharing)
pub dependencies: Arc<IndexMap<String, IndexSet<String>>>,
/// Evaluations grouped into batches (wrapped in Arc for zero-copy sharing)
/// Each inner Vec contains evaluations that can run concurrently
pub sorted_evaluations: Arc<Vec<Vec<String>>>,
/// Evaluations categorized for result handling (wrapped in Arc for zero-copy sharing)
/// Dependents: map from source field to list of dependent items
pub dependents_evaluations: Arc<IndexMap<String, Vec<DependentItem>>>,
/// Rules: evaluations with "/rules/" in path (wrapped in Arc for zero-copy sharing)
pub rules_evaluations: Arc<Vec<String>>,
/// Fields with rules: dotted paths of all fields that have rules (wrapped in Arc for zero-copy sharing)
pub fields_with_rules: Arc<Vec<String>>,
/// Others: all other evaluations not in sorted_evaluations (wrapped in Arc for zero-copy sharing)
pub others_evaluations: Arc<Vec<String>>,
/// Value: evaluations ending with ".value" in path (wrapped in Arc for zero-copy sharing)
pub value_evaluations: Arc<Vec<String>>,
/// Cached layout paths (collected at parse time) (wrapped in Arc for zero-copy sharing)
pub layout_paths: Arc<Vec<String>>,
/// Cached root layout paths (layout paths not attached to another element) (wrapped in Arc)
pub root_layout_paths: Arc<Vec<String>>,
/// Schema field pointers referenced by any `$layout` element. Precomputed so
/// schema-value extraction can distinguish editable layout fields in O(1).
pub layout_field_refs: Arc<IndexSet<String>>,
/// Options URL templates (url_path, template_str, params_path) (wrapped in Arc for zero-copy sharing)
pub options_templates: Arc<Vec<(String, String, String)>>,
/// Subforms: cached ParsedSchema instances for array fields with items
/// Key is the schema path (e.g., "#/properties/items"), value is Arc<ParsedSchema> for cheap cloning
/// This allows subforms to be shared across multiple JSONEval instances efficiently
pub subforms: IndexMap<String, Arc<ParsedSchema>>,
/// Reverse dependency graph for hidden field logic (wrapped in Arc for zero-copy sharing)
/// Map from a field path (source) to list of fields (targets) that reference it in their hidden condition
/// Used for recursive hiding logic
pub reffed_by: Arc<IndexMap<String, Vec<String>>>,
/// Reverse map: data path → list of source field schema paths whose dependent
/// value/clear formulas reference that path (excluding $value/$refValue context vars).
/// When field X changes, source fields in dep_formula_triggers[X] are re-queued so
/// their downstream dependents are re-evaluated with the new context.
pub dep_formula_triggers: Arc<IndexMap<String, Vec<(String, usize)>>>,
/// Cached paths of fields that have hidden conditions (wrapped in Arc for zero-copy sharing)
pub conditional_hidden_fields: Arc<Vec<String>>,
/// Cached paths of fields that have disabled conditions and value property (wrapped in Arc for zero-copy sharing)
pub conditional_readonly_fields: Arc<Vec<String>>,
/// Extracted large static arrays from $params to avoid massive cloning (wrapped in Arc for zero-copy sharing)
pub static_arrays: Arc<IndexMap<String, Arc<Value>>>,
}
impl ParsedSchema {
/// Parse a schema string into a ParsedSchema structure
///
/// # Arguments
///
/// * `schema` - JSON schema string
///
/// # Returns
///
/// A Result containing the ParsedSchema or an error
pub fn parse(schema: &str) -> Result<Self, String> {
let schema_val: Value = serde_json::from_str(schema)
.map_err(|e| format!("Failed to parse schema JSON: {}", e))?;
Self::parse_value(schema_val)
}
/// Parse a schema Value into a ParsedSchema structure
///
/// # Arguments
///
/// * `schema_val` - JSON schema Value
///
/// # Returns
///
/// A Result containing the ParsedSchema or an error
pub fn parse_value(mut schema_val: Value) -> Result<Self, String> {
let engine_config = RLogicConfig::default();
// Pre-process: extract large static arrays from $params to prevent massive cloning
let static_arrays = if let Some(params) = schema_val
.get_mut("$params")
.and_then(|v| v.as_object_mut())
{
crate::jsoneval::static_arrays::extract_from_params(params)
} else {
IndexMap::new()
};
let static_arrays = Arc::new(static_arrays);
let engine = RLogic::with_config(engine_config);
engine.set_static_arrays(Arc::clone(&static_arrays));
let mut parsed = Self {
schema: Arc::new(schema_val),
engine: Arc::new(engine),
evaluations: Arc::new(IndexMap::new()),
tables: Arc::new(IndexMap::new()),
table_metadata: Arc::new(IndexMap::new()),
dependencies: Arc::new(IndexMap::new()),
sorted_evaluations: Arc::new(Vec::new()),
dependents_evaluations: Arc::new(IndexMap::new()),
rules_evaluations: Arc::new(Vec::new()),
fields_with_rules: Arc::new(Vec::new()),
others_evaluations: Arc::new(Vec::new()),
value_evaluations: Arc::new(Vec::new()),
layout_paths: Arc::new(Vec::new()),
root_layout_paths: Arc::new(Vec::new()),
layout_field_refs: Arc::new(IndexSet::new()),
options_templates: Arc::new(Vec::new()),
subforms: IndexMap::new(),
reffed_by: Arc::new(IndexMap::new()),
dep_formula_triggers: Arc::new(IndexMap::new()),
conditional_hidden_fields: Arc::new(Vec::new()),
conditional_readonly_fields: Arc::new(Vec::new()),
static_arrays,
};
// Parse the schema to populate all fields.
crate::parse_schema::parsed::parse_schema_into(&mut parsed)?;
// Nested parsed subforms copy `$params` after large arrays become markers.
// Give every nested engine the root store that owns those extracted arrays.
let static_arrays = Arc::clone(&parsed.static_arrays);
parsed.inherit_static_arrays(static_arrays)?;
Ok(parsed)
}
/// Rebind this parsed schema and every nested subform to a parent's extracted
/// static arrays. Subform schemas copy parent `$params` after extraction, so
/// their schema contains markers rather than source arrays.
pub(crate) fn inherit_static_arrays(
&mut self,
static_arrays: Arc<IndexMap<String, Arc<Value>>>,
) -> Result<(), String> {
Arc::get_mut(&mut self.engine)
.ok_or("Cannot rebind static arrays on a shared ParsedSchema engine")?
.set_static_arrays(Arc::clone(&static_arrays));
self.static_arrays = Arc::clone(&static_arrays);
for subform in self.subforms.values_mut() {
Arc::get_mut(subform)
.ok_or("Cannot rebind static arrays on a shared nested ParsedSchema")?
.inherit_static_arrays(Arc::clone(&static_arrays))?;
}
Ok(())
}
/// Parse a MessagePack-encoded schema into a ParsedSchema structure
///
/// # Arguments
///
/// * `schema_msgpack` - MessagePack-encoded schema bytes
///
/// # Returns
///
/// A Result containing the ParsedSchema or an error
pub fn parse_msgpack(schema_msgpack: &[u8]) -> Result<Self, String> {
let schema_val: Value = rmp_serde::from_slice(schema_msgpack)
.map_err(|e| format!("Failed to deserialize MessagePack schema: {}", e))?;
Self::parse_value(schema_val).map_err(|e| format!("Failed to parse schema: {}", e))
}
/// Get a reference to the original schema
pub fn schema(&self) -> &Value {
&*self.schema
}
}