json_eval_rs/lib.rs
1//! JSON Eval RS - High-performance JSON Logic evaluation library
2//!
3//! This library provides a complete implementation of JSON Logic with advanced features:
4//! - Pre-compilation of logic expressions for optimal performance
5//! - Mutation tracking via proxy-like data wrapper (EvalData)
6//! - All data mutations gated through EvalData for thread safety
7//! - Zero external logic dependencies (built from scratch)
8
9// Use mimalloc allocator on Windows for better performance
10#[cfg(windows)]
11#[global_allocator]
12static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
13
14pub mod rlogic;
15pub mod table_evaluate;
16pub mod table_metadata;
17pub mod topo_sort;
18pub mod parse_schema;
19
20pub mod parsed_schema;
21pub mod parsed_schema_cache;
22pub mod json_parser;
23pub mod path_utils;
24pub mod eval_data;
25pub mod eval_cache;
26pub mod subform_methods;
27
28// FFI module for C# and other languages
29#[cfg(feature = "ffi")]
30pub mod ffi;
31
32// WebAssembly module for JavaScript/TypeScript
33#[cfg(feature = "wasm")]
34pub mod wasm;
35
36// Re-export main types for convenience
37use indexmap::{IndexMap, IndexSet};
38pub use rlogic::{
39 CompiledLogic, CompiledLogicStore, Evaluator,
40 LogicId, RLogic, RLogicConfig,
41 CompiledLogicId, CompiledLogicStoreStats,
42};
43use serde::{Deserialize, Serialize};
44pub use table_metadata::TableMetadata;
45pub use path_utils::ArrayMetadata;
46pub use eval_data::EvalData;
47pub use eval_cache::{EvalCache, CacheKey, CacheStats};
48pub use parsed_schema::ParsedSchema;
49pub use parsed_schema_cache::{ParsedSchemaCache, ParsedSchemaCacheStats, PARSED_SCHEMA_CACHE};
50use serde::de::Error as _;
51
52/// Return format for path-based methods
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum ReturnFormat {
55 /// Nested object preserving the path hierarchy (default)
56 /// Example: { "user": { "profile": { "name": "John" } } }
57 #[default]
58 Nested,
59 /// Flat object with dotted keys
60 /// Example: { "user.profile.name": "John" }
61 Flat,
62 /// Array of values in the order of requested paths
63 /// Example: ["John"]
64 Array,
65}
66use serde_json::{Value};
67
68#[cfg(feature = "parallel")]
69use rayon::prelude::*;
70
71use std::mem;
72use std::sync::{Arc, Mutex};
73use std::time::Instant;
74use std::cell::RefCell;
75
76// Timing infrastructure
77thread_local! {
78 static TIMING_ENABLED: RefCell<bool> = RefCell::new(std::env::var("JSONEVAL_TIMING").is_ok());
79 static TIMING_DATA: RefCell<Vec<(String, std::time::Duration)>> = RefCell::new(Vec::new());
80}
81
82/// Check if timing is enabled
83#[inline]
84fn is_timing_enabled() -> bool {
85 TIMING_ENABLED.with(|enabled| *enabled.borrow())
86}
87
88/// Enable timing programmatically (in addition to JSONEVAL_TIMING environment variable)
89pub fn enable_timing() {
90 TIMING_ENABLED.with(|enabled| {
91 *enabled.borrow_mut() = true;
92 });
93}
94
95/// Disable timing
96pub fn disable_timing() {
97 TIMING_ENABLED.with(|enabled| {
98 *enabled.borrow_mut() = false;
99 });
100}
101
102/// Record timing data
103#[inline]
104fn record_timing(label: &str, duration: std::time::Duration) {
105 if is_timing_enabled() {
106 TIMING_DATA.with(|data| {
107 data.borrow_mut().push((label.to_string(), duration));
108 });
109 }
110}
111
112/// Print timing summary
113pub fn print_timing_summary() {
114 if !is_timing_enabled() {
115 return;
116 }
117
118 TIMING_DATA.with(|data| {
119 let timings = data.borrow();
120 if timings.is_empty() {
121 return;
122 }
123
124 eprintln!("\nš Timing Summary (JSONEVAL_TIMING enabled)");
125 eprintln!("{}", "=".repeat(60));
126
127 let mut total = std::time::Duration::ZERO;
128 for (label, duration) in timings.iter() {
129 eprintln!("{:40} {:>12?}", label, duration);
130 total += *duration;
131 }
132
133 eprintln!("{}", "=".repeat(60));
134 eprintln!("{:40} {:>12?}", "TOTAL", total);
135 eprintln!();
136 });
137}
138
139/// Clear timing data
140pub fn clear_timing_data() {
141 TIMING_DATA.with(|data| {
142 data.borrow_mut().clear();
143 });
144}
145
146/// Macro for timing a block of code
147macro_rules! time_block {
148 ($label:expr, $block:block) => {{
149 let _start = if is_timing_enabled() {
150 Some(Instant::now())
151 } else {
152 None
153 };
154 let result = $block;
155 if let Some(start) = _start {
156 record_timing($label, start.elapsed());
157 }
158 result
159 }};
160}
161
162/// Get the library version
163pub fn version() -> &'static str {
164 env!("CARGO_PKG_VERSION")
165}
166
167/// Clean floating point noise from JSON values
168/// Converts values very close to zero (< 1e-10) to exactly 0
169fn clean_float_noise(value: Value) -> Value {
170 const EPSILON: f64 = 1e-10;
171
172 match value {
173 Value::Number(n) => {
174 if let Some(f) = n.as_f64() {
175 if f.abs() < EPSILON {
176 // Clean near-zero values to exactly 0
177 Value::Number(serde_json::Number::from(0))
178 } else if f.fract().abs() < EPSILON {
179 // Clean whole numbers: 33.0 ā 33
180 Value::Number(serde_json::Number::from(f.round() as i64))
181 } else {
182 Value::Number(n)
183 }
184 } else {
185 Value::Number(n)
186 }
187 }
188 Value::Array(arr) => {
189 Value::Array(arr.into_iter().map(clean_float_noise).collect())
190 }
191 Value::Object(obj) => {
192 Value::Object(obj.into_iter().map(|(k, v)| (k, clean_float_noise(v))).collect())
193 }
194 _ => value,
195 }
196}
197
198/// Dependent item structure for transitive dependency tracking
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct DependentItem {
201 pub ref_path: String,
202 pub clear: Option<Value>, // Can be $evaluation or boolean
203 pub value: Option<Value>, // Can be $evaluation or primitive value
204}
205
206pub struct JSONEval {
207 pub schema: Arc<Value>,
208 pub engine: Arc<RLogic>,
209 /// Zero-copy Arc-wrapped collections (shared from ParsedSchema)
210 pub evaluations: Arc<IndexMap<String, LogicId>>,
211 pub tables: Arc<IndexMap<String, Value>>,
212 /// Pre-compiled table metadata (computed at parse time for zero-copy evaluation)
213 pub table_metadata: Arc<IndexMap<String, TableMetadata>>,
214 pub dependencies: Arc<IndexMap<String, IndexSet<String>>>,
215 /// Evaluations grouped into parallel-executable batches
216 /// Each inner Vec contains evaluations that can run concurrently
217 pub sorted_evaluations: Arc<Vec<Vec<String>>>,
218 /// Evaluations categorized for result handling
219 /// Dependents: map from source field to list of dependent items
220 pub dependents_evaluations: Arc<IndexMap<String, Vec<DependentItem>>>,
221 /// Rules: evaluations with "/rules/" in path
222 pub rules_evaluations: Arc<Vec<String>>,
223 /// Fields with rules: dotted paths of all fields that have rules (for efficient validation)
224 pub fields_with_rules: Arc<Vec<String>>,
225 /// Others: all other evaluations not in sorted_evaluations (for evaluated_schema output)
226 pub others_evaluations: Arc<Vec<String>>,
227 /// Value: evaluations ending with ".value" in path
228 pub value_evaluations: Arc<Vec<String>>,
229 /// Cached layout paths (collected at parse time)
230 pub layout_paths: Arc<Vec<String>>,
231 /// Options URL templates (url_path, template_str, params_path) collected at parse time
232 pub options_templates: Arc<Vec<(String, String, String)>>,
233 /// Subforms: isolated JSONEval instances for array fields with items
234 /// Key is the schema path (e.g., "#/riders"), value is the sub-JSONEval
235 pub subforms: IndexMap<String, Box<JSONEval>>,
236 pub context: Value,
237 pub data: Value,
238 pub evaluated_schema: Value,
239 pub eval_data: EvalData,
240 /// Evaluation cache with content-based hashing and zero-copy storage
241 pub eval_cache: EvalCache,
242 /// Flag to enable/disable evaluation caching
243 /// Set to false for web API usage where each request creates a new JSONEval instance
244 pub cache_enabled: bool,
245 /// Mutex for synchronous execution of evaluate and evaluate_dependents
246 eval_lock: Mutex<()>,
247 /// Cached MessagePack bytes for zero-copy schema retrieval
248 /// Stores original MessagePack if initialized from binary, cleared on schema mutations
249 cached_msgpack_schema: Option<Vec<u8>>,
250}
251
252impl Clone for JSONEval {
253 fn clone(&self) -> Self {
254 Self {
255 cache_enabled: self.cache_enabled,
256 schema: Arc::clone(&self.schema),
257 engine: Arc::clone(&self.engine),
258 evaluations: self.evaluations.clone(),
259 tables: self.tables.clone(),
260 table_metadata: self.table_metadata.clone(),
261 dependencies: self.dependencies.clone(),
262 sorted_evaluations: self.sorted_evaluations.clone(),
263 dependents_evaluations: self.dependents_evaluations.clone(),
264 rules_evaluations: self.rules_evaluations.clone(),
265 fields_with_rules: self.fields_with_rules.clone(),
266 others_evaluations: self.others_evaluations.clone(),
267 value_evaluations: self.value_evaluations.clone(),
268 layout_paths: self.layout_paths.clone(),
269 options_templates: self.options_templates.clone(),
270 subforms: self.subforms.clone(),
271 context: self.context.clone(),
272 data: self.data.clone(),
273 evaluated_schema: self.evaluated_schema.clone(),
274 eval_data: self.eval_data.clone(),
275 eval_cache: EvalCache::new(), // Create fresh cache for the clone
276 eval_lock: Mutex::new(()), // Create fresh mutex for the clone
277 cached_msgpack_schema: self.cached_msgpack_schema.clone(),
278 }
279 }
280}
281
282impl JSONEval {
283 pub fn new(
284 schema: &str,
285 context: Option<&str>,
286 data: Option<&str>,
287 ) -> Result<Self, serde_json::Error> {
288 time_block!("JSONEval::new() [total]", {
289 // Use serde_json for schema (needs arbitrary_precision) and SIMD for data (needs speed)
290 let schema_val: Value = time_block!(" parse schema JSON", {
291 serde_json::from_str(schema)?
292 });
293 let context: Value = time_block!(" parse context JSON", {
294 json_parser::parse_json_str(context.unwrap_or("{}")).map_err(serde_json::Error::custom)?
295 });
296 let data: Value = time_block!(" parse data JSON", {
297 json_parser::parse_json_str(data.unwrap_or("{}")).map_err(serde_json::Error::custom)?
298 });
299 let evaluated_schema = schema_val.clone();
300 // Use default config: tracking enabled
301 let engine_config = RLogicConfig::default();
302
303 let mut instance = time_block!(" create instance struct", {
304 Self {
305 schema: Arc::new(schema_val),
306 evaluations: Arc::new(IndexMap::new()),
307 tables: Arc::new(IndexMap::new()),
308 table_metadata: Arc::new(IndexMap::new()),
309 dependencies: Arc::new(IndexMap::new()),
310 sorted_evaluations: Arc::new(Vec::new()),
311 dependents_evaluations: Arc::new(IndexMap::new()),
312 rules_evaluations: Arc::new(Vec::new()),
313 fields_with_rules: Arc::new(Vec::new()),
314 others_evaluations: Arc::new(Vec::new()),
315 value_evaluations: Arc::new(Vec::new()),
316 layout_paths: Arc::new(Vec::new()),
317 options_templates: Arc::new(Vec::new()),
318 subforms: IndexMap::new(),
319 engine: Arc::new(RLogic::with_config(engine_config)),
320 context: context.clone(),
321 data: data.clone(),
322 evaluated_schema: evaluated_schema.clone(),
323 eval_data: EvalData::with_schema_data_context(&evaluated_schema, &data, &context),
324 eval_cache: EvalCache::new(),
325 cache_enabled: true, // Caching enabled by default
326 eval_lock: Mutex::new(()),
327 cached_msgpack_schema: None, // JSON initialization, no MessagePack cache
328 }
329 });
330 time_block!(" parse_schema", {
331 parse_schema::legacy::parse_schema(&mut instance).map_err(serde_json::Error::custom)?
332 });
333 Ok(instance)
334 })
335 }
336
337 /// Create a new JSONEval instance from MessagePack-encoded schema
338 ///
339 /// # Arguments
340 ///
341 /// * `schema_msgpack` - MessagePack-encoded schema bytes
342 /// * `context` - Optional JSON context string
343 /// * `data` - Optional JSON data string
344 ///
345 /// # Returns
346 ///
347 /// A Result containing the JSONEval instance or an error
348 pub fn new_from_msgpack(
349 schema_msgpack: &[u8],
350 context: Option<&str>,
351 data: Option<&str>,
352 ) -> Result<Self, String> {
353 // Store original MessagePack bytes for zero-copy retrieval
354 let cached_msgpack = schema_msgpack.to_vec();
355
356 // Deserialize MessagePack schema to Value
357 let schema_val: Value = rmp_serde::from_slice(schema_msgpack)
358 .map_err(|e| format!("Failed to deserialize MessagePack schema: {}", e))?;
359
360 let context: Value = json_parser::parse_json_str(context.unwrap_or("{}"))
361 .map_err(|e| format!("Failed to parse context: {}", e))?;
362 let data: Value = json_parser::parse_json_str(data.unwrap_or("{}"))
363 .map_err(|e| format!("Failed to parse data: {}", e))?;
364 let evaluated_schema = schema_val.clone();
365 let engine_config = RLogicConfig::default();
366
367 let mut instance = Self {
368 schema: Arc::new(schema_val),
369 evaluations: Arc::new(IndexMap::new()),
370 tables: Arc::new(IndexMap::new()),
371 table_metadata: Arc::new(IndexMap::new()),
372 dependencies: Arc::new(IndexMap::new()),
373 sorted_evaluations: Arc::new(Vec::new()),
374 dependents_evaluations: Arc::new(IndexMap::new()),
375 rules_evaluations: Arc::new(Vec::new()),
376 fields_with_rules: Arc::new(Vec::new()),
377 others_evaluations: Arc::new(Vec::new()),
378 value_evaluations: Arc::new(Vec::new()),
379 layout_paths: Arc::new(Vec::new()),
380 options_templates: Arc::new(Vec::new()),
381 subforms: IndexMap::new(),
382 engine: Arc::new(RLogic::with_config(engine_config)),
383 context: context.clone(),
384 data: data.clone(),
385 evaluated_schema: evaluated_schema.clone(),
386 eval_data: EvalData::with_schema_data_context(&evaluated_schema, &data, &context),
387 eval_cache: EvalCache::new(),
388 cache_enabled: true, // Caching enabled by default
389 eval_lock: Mutex::new(()),
390 cached_msgpack_schema: Some(cached_msgpack), // Store for zero-copy retrieval
391 };
392 parse_schema::legacy::parse_schema(&mut instance)?;
393 Ok(instance)
394 }
395
396 /// Create a new JSONEval instance from a pre-parsed ParsedSchema
397 ///
398 /// This enables schema caching: parse once, reuse across multiple evaluations with different data/context.
399 ///
400 /// # Arguments
401 ///
402 /// * `parsed` - Arc-wrapped pre-parsed schema (can be cloned and cached)
403 /// * `context` - Optional JSON context string
404 /// * `data` - Optional JSON data string
405 ///
406 /// # Returns
407 ///
408 /// A Result containing the JSONEval instance or an error
409 ///
410 /// # Example
411 ///
412 /// ```ignore
413 /// use std::sync::Arc;
414 ///
415 /// // Parse schema once and wrap in Arc for caching
416 /// let parsed = Arc::new(ParsedSchema::parse(schema_str)?);
417 /// cache.insert(schema_key, parsed.clone());
418 ///
419 /// // Reuse across multiple evaluations (Arc::clone is cheap)
420 /// let eval1 = JSONEval::with_parsed_schema(parsed.clone(), Some(context1), Some(data1))?;
421 /// let eval2 = JSONEval::with_parsed_schema(parsed.clone(), Some(context2), Some(data2))?;
422 /// ```
423 pub fn with_parsed_schema(
424 parsed: Arc<ParsedSchema>,
425 context: Option<&str>,
426 data: Option<&str>,
427 ) -> Result<Self, String> {
428 let context: Value = json_parser::parse_json_str(context.unwrap_or("{}"))
429 .map_err(|e| format!("Failed to parse context: {}", e))?;
430 let data: Value = json_parser::parse_json_str(data.unwrap_or("{}"))
431 .map_err(|e| format!("Failed to parse data: {}", e))?;
432
433 let evaluated_schema = parsed.schema.clone();
434
435 // Share the engine Arc (cheap pointer clone, not data clone)
436 // Multiple JSONEval instances created from the same ParsedSchema will share the compiled RLogic
437 let engine = parsed.engine.clone();
438
439 // Convert Arc<ParsedSchema> subforms to Box<JSONEval> subforms
440 // This is a one-time conversion when creating JSONEval from ParsedSchema
441 let mut subforms = IndexMap::new();
442 for (path, subform_parsed) in &parsed.subforms {
443 // Create JSONEval from the cached ParsedSchema
444 let subform_eval = JSONEval::with_parsed_schema(
445 subform_parsed.clone(),
446 Some("{}"),
447 None
448 )?;
449 subforms.insert(path.clone(), Box::new(subform_eval));
450 }
451
452 let instance = Self {
453 schema: Arc::clone(&parsed.schema),
454 // Zero-copy Arc clones (just increments reference count, no data copying)
455 evaluations: Arc::clone(&parsed.evaluations),
456 tables: Arc::clone(&parsed.tables),
457 table_metadata: Arc::clone(&parsed.table_metadata),
458 dependencies: Arc::clone(&parsed.dependencies),
459 sorted_evaluations: Arc::clone(&parsed.sorted_evaluations),
460 dependents_evaluations: Arc::clone(&parsed.dependents_evaluations),
461 rules_evaluations: Arc::clone(&parsed.rules_evaluations),
462 fields_with_rules: Arc::clone(&parsed.fields_with_rules),
463 others_evaluations: Arc::clone(&parsed.others_evaluations),
464 value_evaluations: Arc::clone(&parsed.value_evaluations),
465 layout_paths: Arc::clone(&parsed.layout_paths),
466 options_templates: Arc::clone(&parsed.options_templates),
467 subforms,
468 engine,
469 context: context.clone(),
470 data: data.clone(),
471 evaluated_schema: (*evaluated_schema).clone(),
472 eval_data: EvalData::with_schema_data_context(&evaluated_schema, &data, &context),
473 eval_cache: EvalCache::new(),
474 cache_enabled: true, // Caching enabled by default
475 eval_lock: Mutex::new(()),
476 cached_msgpack_schema: None, // No MessagePack cache for parsed schema
477 };
478
479 Ok(instance)
480 }
481
482 pub fn reload_schema(
483 &mut self,
484 schema: &str,
485 context: Option<&str>,
486 data: Option<&str>,
487 ) -> Result<(), String> {
488 // Use serde_json for schema (precision) and SIMD for data (speed)
489 let schema_val: Value = serde_json::from_str(schema).map_err(|e| format!("failed to parse schema: {e}"))?;
490 let context: Value = json_parser::parse_json_str(context.unwrap_or("{}"))?;
491 let data: Value = json_parser::parse_json_str(data.unwrap_or("{}"))?;
492 self.schema = Arc::new(schema_val);
493 self.context = context.clone();
494 self.data = data.clone();
495 self.evaluated_schema = (*self.schema).clone();
496 self.engine = Arc::new(RLogic::new());
497 self.dependents_evaluations = Arc::new(IndexMap::new());
498 self.rules_evaluations = Arc::new(Vec::new());
499 self.fields_with_rules = Arc::new(Vec::new());
500 self.others_evaluations = Arc::new(Vec::new());
501 self.value_evaluations = Arc::new(Vec::new());
502 self.layout_paths = Arc::new(Vec::new());
503 self.options_templates = Arc::new(Vec::new());
504 self.subforms.clear();
505 parse_schema::legacy::parse_schema(self)?;
506
507 // Re-initialize eval_data with new schema, data, and context
508 self.eval_data = EvalData::with_schema_data_context(&self.evaluated_schema, &data, &context);
509
510 // Clear cache when schema changes
511 self.eval_cache.clear();
512
513 // Clear MessagePack cache since schema has been mutated
514 self.cached_msgpack_schema = None;
515
516 Ok(())
517 }
518
519 /// Set the timezone offset for datetime operations (TODAY, NOW)
520 ///
521 /// This method updates the RLogic engine configuration with a new timezone offset.
522 /// The offset will be applied to all subsequent datetime evaluations.
523 ///
524 /// # Arguments
525 ///
526 /// * `offset_minutes` - Timezone offset in minutes from UTC (e.g., 420 for UTC+7, -300 for UTC-5)
527 /// Pass `None` to reset to UTC (no offset)
528 ///
529 /// # Example
530 ///
531 /// ```ignore
532 /// let mut eval = JSONEval::new(schema, None, None)?;
533 ///
534 /// // Set to UTC+7 (Jakarta, Bangkok)
535 /// eval.set_timezone_offset(Some(420));
536 ///
537 /// // Reset to UTC
538 /// eval.set_timezone_offset(None);
539 /// ```
540 pub fn set_timezone_offset(&mut self, offset_minutes: Option<i32>) {
541 // Create new config with the timezone offset
542 let mut config = RLogicConfig::default();
543 if let Some(offset) = offset_minutes {
544 config = config.with_timezone_offset(offset);
545 }
546
547 // Recreate the engine with the new configuration
548 // This is necessary because RLogic is wrapped in Arc and config is part of the evaluator
549 self.engine = Arc::new(RLogic::with_config(config));
550
551 // Note: We need to recompile all evaluations because they're associated with the old engine
552 // Re-parse the schema to recompile all evaluations with the new engine
553 let _ = parse_schema::legacy::parse_schema(self);
554
555 // Clear cache since evaluation results may change with new timezone
556 self.eval_cache.clear();
557 }
558
559 /// Reload schema from MessagePack-encoded bytes
560 ///
561 /// # Arguments
562 ///
563 /// * `schema_msgpack` - MessagePack-encoded schema bytes
564 /// * `context` - Optional context data JSON string
565 /// * `data` - Optional initial data JSON string
566 ///
567 /// # Returns
568 ///
569 /// A `Result` indicating success or an error message
570 pub fn reload_schema_msgpack(
571 &mut self,
572 schema_msgpack: &[u8],
573 context: Option<&str>,
574 data: Option<&str>,
575 ) -> Result<(), String> {
576 // Deserialize MessagePack to Value
577 let schema_val: Value = rmp_serde::from_slice(schema_msgpack)
578 .map_err(|e| format!("failed to deserialize MessagePack schema: {e}"))?;
579
580 let context: Value = json_parser::parse_json_str(context.unwrap_or("{}"))?;
581 let data: Value = json_parser::parse_json_str(data.unwrap_or("{}"))?;
582
583 self.schema = Arc::new(schema_val);
584 self.context = context.clone();
585 self.data = data.clone();
586 self.evaluated_schema = (*self.schema).clone();
587 self.engine = Arc::new(RLogic::new());
588 self.dependents_evaluations = Arc::new(IndexMap::new());
589 self.rules_evaluations = Arc::new(Vec::new());
590 self.fields_with_rules = Arc::new(Vec::new());
591 self.others_evaluations = Arc::new(Vec::new());
592 self.value_evaluations = Arc::new(Vec::new());
593 self.layout_paths = Arc::new(Vec::new());
594 self.options_templates = Arc::new(Vec::new());
595 self.subforms.clear();
596 parse_schema::legacy::parse_schema(self)?;
597
598 // Re-initialize eval_data
599 self.eval_data = EvalData::with_schema_data_context(&self.evaluated_schema, &data, &context);
600
601 // Clear cache when schema changes
602 self.eval_cache.clear();
603
604 // Cache the MessagePack for future retrievals
605 self.cached_msgpack_schema = Some(schema_msgpack.to_vec());
606
607 Ok(())
608 }
609
610 /// Reload schema from a cached ParsedSchema
611 ///
612 /// This is the most efficient way to reload as it reuses pre-parsed schema compilation.
613 ///
614 /// # Arguments
615 ///
616 /// * `parsed` - Arc reference to a cached ParsedSchema
617 /// * `context` - Optional context data JSON string
618 /// * `data` - Optional initial data JSON string
619 ///
620 /// # Returns
621 ///
622 /// A `Result` indicating success or an error message
623 pub fn reload_schema_parsed(
624 &mut self,
625 parsed: Arc<ParsedSchema>,
626 context: Option<&str>,
627 data: Option<&str>,
628 ) -> Result<(), String> {
629 let context: Value = json_parser::parse_json_str(context.unwrap_or("{}"))?;
630 let data: Value = json_parser::parse_json_str(data.unwrap_or("{}"))?;
631
632 // Share all the pre-compiled data from ParsedSchema
633 self.schema = Arc::clone(&parsed.schema);
634 self.evaluations = parsed.evaluations.clone();
635 self.tables = parsed.tables.clone();
636 self.table_metadata = parsed.table_metadata.clone();
637 self.dependencies = parsed.dependencies.clone();
638 self.sorted_evaluations = parsed.sorted_evaluations.clone();
639 self.dependents_evaluations = parsed.dependents_evaluations.clone();
640 self.rules_evaluations = parsed.rules_evaluations.clone();
641 self.fields_with_rules = parsed.fields_with_rules.clone();
642 self.others_evaluations = parsed.others_evaluations.clone();
643 self.value_evaluations = parsed.value_evaluations.clone();
644 self.layout_paths = parsed.layout_paths.clone();
645 self.options_templates = parsed.options_templates.clone();
646
647 // Share the engine Arc (cheap pointer clone, not data clone)
648 self.engine = parsed.engine.clone();
649
650 // Convert Arc<ParsedSchema> subforms to Box<JSONEval> subforms
651 let mut subforms = IndexMap::new();
652 for (path, subform_parsed) in &parsed.subforms {
653 let subform_eval = JSONEval::with_parsed_schema(
654 subform_parsed.clone(),
655 Some("{}"),
656 None
657 )?;
658 subforms.insert(path.clone(), Box::new(subform_eval));
659 }
660 self.subforms = subforms;
661
662 self.context = context.clone();
663 self.data = data.clone();
664 self.evaluated_schema = (*self.schema).clone();
665
666 // Re-initialize eval_data
667 self.eval_data = EvalData::with_schema_data_context(&self.evaluated_schema, &data, &context);
668
669 // Clear cache when schema changes
670 self.eval_cache.clear();
671
672 // Clear MessagePack cache since we're loading from ParsedSchema
673 self.cached_msgpack_schema = None;
674
675 Ok(())
676 }
677
678 /// Reload schema from ParsedSchemaCache using a cache key
679 ///
680 /// This is the recommended way for cross-platform cached schema reloading.
681 ///
682 /// # Arguments
683 ///
684 /// * `cache_key` - Key to lookup in the global ParsedSchemaCache
685 /// * `context` - Optional context data JSON string
686 /// * `data` - Optional initial data JSON string
687 ///
688 /// # Returns
689 ///
690 /// A `Result` indicating success or an error message
691 pub fn reload_schema_from_cache(
692 &mut self,
693 cache_key: &str,
694 context: Option<&str>,
695 data: Option<&str>,
696 ) -> Result<(), String> {
697 // Get the cached ParsedSchema from global cache
698 let parsed = PARSED_SCHEMA_CACHE.get(cache_key)
699 .ok_or_else(|| format!("Schema '{}' not found in cache", cache_key))?;
700
701 // Use reload_schema_parsed with the cached schema
702 self.reload_schema_parsed(parsed, context, data)
703 }
704
705 /// Evaluate the schema with the given data and context.
706 ///
707 /// # Arguments
708 ///
709 /// * `data` - The data to evaluate.
710 /// * `context` - The context to evaluate.
711 ///
712 /// # Returns
713 ///
714 /// A `Result` indicating success or an error message.
715 pub fn evaluate(&mut self, data: &str, context: Option<&str>, paths: Option<&[String]>) -> Result<(), String> {
716 time_block!("evaluate() [total]", {
717 let context_provided = context.is_some();
718
719 // Use SIMD-accelerated JSON parsing
720 let data: Value = time_block!(" parse data", {
721 json_parser::parse_json_str(data)?
722 });
723 let context: Value = time_block!(" parse context", {
724 json_parser::parse_json_str(context.unwrap_or("{}"))?
725 });
726
727 self.data = data.clone();
728
729 // Collect top-level data keys to selectively purge cache
730 let changed_data_paths: Vec<String> = if let Some(obj) = data.as_object() {
731 obj.keys().map(|k| format!("/{}", k)).collect()
732 } else {
733 Vec::new()
734 };
735
736 // Replace data and context in existing eval_data
737 time_block!(" replace_data_and_context", {
738 self.eval_data.replace_data_and_context(data, context);
739 });
740
741 // Selectively purge cache entries that depend on changed top-level data keys
742 // This is more efficient than clearing entire cache
743 time_block!(" purge_cache", {
744 self.purge_cache_for_changed_data(&changed_data_paths);
745
746 // Also purge context-dependent cache if context was provided
747 if context_provided {
748 self.purge_cache_for_context_change();
749 }
750 });
751
752 // Call internal evaluate (uses existing data if not provided)
753 self.evaluate_internal(paths)
754 })
755 }
756
757 /// Internal evaluate that can be called when data is already set
758 /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
759 fn evaluate_internal(&mut self, paths: Option<&[String]>) -> Result<(), String> {
760 time_block!(" evaluate_internal() [total]", {
761 // Acquire lock for synchronous execution
762 let _lock = self.eval_lock.lock().unwrap();
763
764 // Normalize paths to schema pointers for correct filtering
765 let normalized_paths_storage; // Keep alive
766 let normalized_paths = if let Some(p_list) = paths {
767 normalized_paths_storage = p_list.iter()
768 .flat_map(|p| {
769 let normalized = if p.starts_with("#/") {
770 // Case 1: JSON Schema path (e.g. #/properties/foo) - keep as is
771 p.to_string()
772 } else if p.starts_with('/') {
773 // Case 2: Rust Pointer path (e.g. /properties/foo) - ensure # prefix
774 format!("#{}", p)
775 } else {
776 // Case 3: Dot notation (e.g. properties.foo) - replace dots with slashes and add prefix
777 format!("#/{}", p.replace('.', "/"))
778 };
779
780 vec![normalized]
781 })
782 .collect::<Vec<_>>();
783 Some(normalized_paths_storage.as_slice())
784 } else {
785 None
786 };
787
788 // Clone sorted_evaluations (Arc clone is cheap, then clone inner Vec)
789 let eval_batches: Vec<Vec<String>> = (*self.sorted_evaluations).clone();
790
791 // Process each batch - parallelize evaluations within each batch
792 // Batches are processed sequentially to maintain dependency order
793 // Process value evaluations (simple computed fields)
794 // These are independent of rule batches and should always run
795 let eval_data_values = self.eval_data.clone();
796 time_block!(" evaluate values", {
797 #[cfg(feature = "parallel")]
798 if self.value_evaluations.len() > 100 {
799 let value_results: Mutex<Vec<(String, Value)>> = Mutex::new(Vec::with_capacity(self.value_evaluations.len()));
800
801 self.value_evaluations.par_iter().for_each(|eval_key| {
802 // Skip if has dependencies (will be handled in sorted batches)
803 if let Some(deps) = self.dependencies.get(eval_key) {
804 if !deps.is_empty() {
805 return;
806 }
807 }
808
809 // Filter items if paths are provided
810 if let Some(filter_paths) = normalized_paths {
811 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())) {
812 return;
813 }
814 }
815
816 // For value evaluations (e.g. /properties/foo/value), we want the value at that path
817 // The path in eval_key is like "#/properties/foo/value"
818 let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
819
820 // Try cache first (thread-safe)
821 if let Some(_) = self.try_get_cached(eval_key, &eval_data_values) {
822 return;
823 }
824
825 // Cache miss - evaluate
826 if let Some(logic_id) = self.evaluations.get(eval_key) {
827 if let Ok(val) = self.engine.run(logic_id, eval_data_values.data()) {
828 let cleaned_val = clean_float_noise(val);
829 // Cache result (thread-safe)
830 self.cache_result(eval_key, Value::Null, &eval_data_values);
831 value_results.lock().unwrap().push((pointer_path, cleaned_val));
832 }
833 }
834 });
835
836 // Write results to evaluated_schema
837 for (result_path, value) in value_results.into_inner().unwrap() {
838 if let Some(pointer_value) = self.evaluated_schema.pointer_mut(&result_path) {
839 *pointer_value = value;
840 }
841 }
842 }
843
844 // Sequential execution for values (if not parallel or small count)
845 #[cfg(feature = "parallel")]
846 let value_eval_items = if self.value_evaluations.len() > 100 { &self.value_evaluations[0..0] } else { &self.value_evaluations };
847
848 #[cfg(not(feature = "parallel"))]
849 let value_eval_items = &self.value_evaluations;
850
851 for eval_key in value_eval_items.iter() {
852 // Skip if has dependencies (will be handled in sorted batches)
853 if let Some(deps) = self.dependencies.get(eval_key) {
854 if !deps.is_empty() {
855 continue;
856 }
857 }
858
859 // Filter items if paths are provided
860 if let Some(filter_paths) = normalized_paths {
861 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())) {
862 continue;
863 }
864 }
865
866 let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
867
868 // Try cache first
869 if let Some(_) = self.try_get_cached(eval_key, &eval_data_values) {
870 continue;
871 }
872
873 // Cache miss - evaluate
874 if let Some(logic_id) = self.evaluations.get(eval_key) {
875 if let Ok(val) = self.engine.run(logic_id, eval_data_values.data()) {
876 let cleaned_val = clean_float_noise(val);
877 // Cache result
878 self.cache_result(eval_key, Value::Null, &eval_data_values);
879
880 if let Some(pointer_value) = self.evaluated_schema.pointer_mut(&pointer_path) {
881 *pointer_value = cleaned_val;
882 }
883 }
884 }
885 }
886 });
887
888 time_block!(" process batches", {
889 for batch in eval_batches {
890 // Skip empty batches
891 if batch.is_empty() {
892 continue;
893 }
894
895 // Check if we can skip this entire batch optimization
896 // If paths are provided, we can check if ANY item in batch matches ANY path
897 if let Some(filter_paths) = normalized_paths {
898 if !filter_paths.is_empty() {
899 let batch_has_match = batch.iter().any(|eval_key| {
900 filter_paths.iter().any(|p| eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str()))
901 });
902 if !batch_has_match {
903 continue;
904 }
905 }
906 }
907
908 // No pre-checking cache - we'll check inside parallel execution
909 // This allows thread-safe cache access during parallel evaluation
910
911 // Parallel execution within batch (no dependencies between items)
912 // Use Mutex for thread-safe result collection
913 // Store both eval_key and result for cache storage
914 let eval_data_snapshot = self.eval_data.clone();
915
916 // Parallelize only if batch has multiple items (overhead not worth it for single item)
917
918
919 #[cfg(feature = "parallel")]
920 if batch.len() > 1000 {
921 let results: Mutex<Vec<(String, String, Value)>> = Mutex::new(Vec::with_capacity(batch.len()));
922 batch.par_iter().for_each(|eval_key| {
923 // Filter individual items if paths are provided
924 if let Some(filter_paths) = normalized_paths {
925 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())) {
926 return;
927 }
928 }
929
930 let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
931
932 // Try cache first (thread-safe)
933 if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
934 return;
935 }
936
937 // Cache miss - evaluate
938 let is_table = self.table_metadata.contains_key(eval_key);
939
940 if is_table {
941 // Evaluate table using sandboxed metadata (parallel-safe, immutable parent scope)
942 if let Ok(rows) = table_evaluate::evaluate_table(self, eval_key, &eval_data_snapshot) {
943 let value = Value::Array(rows);
944 // Cache result (thread-safe)
945 self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
946 results.lock().unwrap().push((eval_key.clone(), pointer_path, value));
947 }
948 } else {
949 if let Some(logic_id) = self.evaluations.get(eval_key) {
950 // Evaluate directly with snapshot
951 if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
952 let cleaned_val = clean_float_noise(val);
953 // Cache result (thread-safe)
954 self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
955 results.lock().unwrap().push((eval_key.clone(), pointer_path, cleaned_val));
956 }
957 }
958 }
959 });
960
961 // Write all results back sequentially (already cached in parallel execution)
962 for (_eval_key, path, value) in results.into_inner().unwrap() {
963 let cleaned_value = clean_float_noise(value);
964
965 self.eval_data.set(&path, cleaned_value.clone());
966 // Also write to evaluated_schema
967 if let Some(schema_value) = self.evaluated_schema.pointer_mut(&path) {
968 *schema_value = cleaned_value;
969 }
970 }
971 continue;
972 }
973
974 // Sequential execution (single item or parallel feature disabled)
975 #[cfg(not(feature = "parallel"))]
976 let batch_items = &batch;
977
978 #[cfg(feature = "parallel")]
979 let batch_items = if batch.len() > 1000 { &batch[0..0] } else { &batch }; // Empty slice if already processed in parallel
980
981 for eval_key in batch_items {
982 // Filter individual items if paths are provided
983 if let Some(filter_paths) = normalized_paths {
984 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())) {
985 continue;
986 }
987 }
988
989 let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
990
991 // Try cache first
992 if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
993 continue;
994 }
995
996 // Cache miss - evaluate
997 let is_table = self.table_metadata.contains_key(eval_key);
998
999 if is_table {
1000 if let Ok(rows) = table_evaluate::evaluate_table(self, eval_key, &eval_data_snapshot) {
1001 let value = Value::Array(rows);
1002 // Cache result
1003 self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
1004
1005 let cleaned_value = clean_float_noise(value);
1006 self.eval_data.set(&pointer_path, cleaned_value.clone());
1007 if let Some(schema_value) = self.evaluated_schema.pointer_mut(&pointer_path) {
1008 *schema_value = cleaned_value;
1009 }
1010 }
1011 } else {
1012 if let Some(logic_id) = self.evaluations.get(eval_key) {
1013 if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
1014 let cleaned_val = clean_float_noise(val);
1015 // Cache result
1016 self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
1017
1018 self.eval_data.set(&pointer_path, cleaned_val.clone());
1019 if let Some(schema_value) = self.evaluated_schema.pointer_mut(&pointer_path) {
1020 *schema_value = cleaned_val;
1021 }
1022 }
1023 }
1024 }
1025 }
1026 }
1027 });
1028
1029 // Drop lock before calling evaluate_others
1030 drop(_lock);
1031
1032 self.evaluate_others(paths);
1033
1034 Ok(())
1035 })
1036 }
1037
1038 /// Get the evaluated schema with optional layout resolution.
1039 ///
1040 /// # Arguments
1041 ///
1042 /// * `skip_layout` - Whether to skip layout resolution.
1043 ///
1044 /// # Returns
1045 ///
1046 /// The evaluated schema as a JSON value.
1047 pub fn get_evaluated_schema(&mut self, skip_layout: bool) -> Value {
1048 time_block!("get_evaluated_schema()", {
1049 if !skip_layout {
1050 self.resolve_layout_internal();
1051 }
1052
1053 self.evaluated_schema.clone()
1054 })
1055 }
1056
1057 /// Get the evaluated schema as MessagePack binary format
1058 ///
1059 /// # Arguments
1060 ///
1061 /// * `skip_layout` - Whether to skip layout resolution.
1062 ///
1063 /// # Returns
1064 ///
1065 /// The evaluated schema serialized as MessagePack bytes
1066 ///
1067 /// # Zero-Copy Optimization
1068 ///
1069 /// This method serializes the evaluated schema to MessagePack. The resulting Vec<u8>
1070 /// is then passed to FFI/WASM boundaries via raw pointers (zero-copy at boundary).
1071 /// The serialization step itself (Value -> MessagePack) cannot be avoided but is
1072 /// highly optimized by rmp-serde.
1073 pub fn get_evaluated_schema_msgpack(&mut self, skip_layout: bool) -> Result<Vec<u8>, String> {
1074 if !skip_layout {
1075 self.resolve_layout_internal();
1076 }
1077
1078 // Serialize evaluated schema to MessagePack
1079 // Note: This is the only copy required. The FFI layer then returns raw pointers
1080 // to this data for zero-copy transfer to calling code.
1081 rmp_serde::to_vec(&self.evaluated_schema)
1082 .map_err(|e| format!("Failed to serialize schema to MessagePack: {}", e))
1083 }
1084
1085 /// Get all schema values (evaluations ending with .value)
1086 /// Mutates self.data by overriding with values from value evaluations
1087 /// Returns the modified data
1088 pub fn get_schema_value(&mut self) -> Value {
1089 // Ensure self.data is an object
1090 if !self.data.is_object() {
1091 self.data = Value::Object(serde_json::Map::new());
1092 }
1093
1094 // Override self.data with values from value evaluations
1095 for eval_key in self.value_evaluations.iter() {
1096 let clean_key = eval_key.replace("#", "");
1097
1098 // Exclude rules.*.value, options.*.value, and $params
1099 if clean_key.starts_with("/$params") || (clean_key.ends_with("/value") && (clean_key.contains("/rules/") || clean_key.contains("/options/"))) {
1100 continue;
1101 }
1102
1103 let path = clean_key.replace("/properties", "").replace("/value", "");
1104
1105 // Get the value from evaluated_schema
1106 let value = match self.evaluated_schema.pointer(&clean_key) {
1107 Some(v) => v.clone(),
1108 None => continue,
1109 };
1110
1111 // Parse the path and create nested structure as needed
1112 let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1113
1114 if path_parts.is_empty() {
1115 continue;
1116 }
1117
1118 // Navigate/create nested structure
1119 let mut current = &mut self.data;
1120 for (i, part) in path_parts.iter().enumerate() {
1121 let is_last = i == path_parts.len() - 1;
1122
1123 if is_last {
1124 // Set the value at the final key
1125 if let Some(obj) = current.as_object_mut() {
1126 obj.insert(part.to_string(), clean_float_noise(value.clone()));
1127 }
1128 } else {
1129 // Ensure current is an object, then navigate/create intermediate objects
1130 if let Some(obj) = current.as_object_mut() {
1131 current = obj.entry(part.to_string())
1132 .or_insert_with(|| Value::Object(serde_json::Map::new()));
1133 } else {
1134 // Skip this path if current is not an object and can't be made into one
1135 break;
1136 }
1137 }
1138 }
1139 }
1140
1141 clean_float_noise(self.data.clone())
1142 }
1143
1144 /// Get the evaluated schema without $params field.
1145 /// This method filters out $params from the root level only.
1146 ///
1147 /// # Arguments
1148 ///
1149 /// * `skip_layout` - Whether to skip layout resolution.
1150 ///
1151 /// # Returns
1152 ///
1153 /// The evaluated schema with $params removed.
1154 pub fn get_evaluated_schema_without_params(&mut self, skip_layout: bool) -> Value {
1155 if !skip_layout {
1156 self.resolve_layout_internal();
1157 }
1158
1159 // Filter $params at root level only
1160 if let Value::Object(mut map) = self.evaluated_schema.clone() {
1161 map.remove("$params");
1162 Value::Object(map)
1163 } else {
1164 self.evaluated_schema.clone()
1165 }
1166 }
1167
1168 /// Get a value from the evaluated schema using dotted path notation.
1169 /// Converts dotted notation (e.g., "properties.field.value") to JSON pointer format.
1170 ///
1171 /// # Arguments
1172 ///
1173 /// * `path` - The dotted path to the value (e.g., "properties.field.value")
1174 /// * `skip_layout` - Whether to skip layout resolution.
1175 ///
1176 /// # Returns
1177 ///
1178 /// The value at the specified path, or None if not found.
1179 pub fn get_evaluated_schema_by_path(&mut self, path: &str, skip_layout: bool) -> Option<Value> {
1180 if !skip_layout {
1181 self.resolve_layout_internal();
1182 }
1183
1184 // Convert dotted notation to JSON pointer
1185 let pointer = if path.is_empty() {
1186 "".to_string()
1187 } else {
1188 format!("/{}", path.replace(".", "/"))
1189 };
1190
1191 self.evaluated_schema.pointer(&pointer).cloned()
1192 }
1193
1194 /// Get values from the evaluated schema using multiple dotted path notations.
1195 /// Returns data in the specified format. Skips paths that are not found.
1196 ///
1197 /// # Arguments
1198 ///
1199 /// * `paths` - Array of dotted paths to retrieve (e.g., ["properties.field1", "properties.field2"])
1200 /// * `skip_layout` - Whether to skip layout resolution.
1201 /// * `format` - Optional return format (Nested, Flat, or Array). Defaults to Nested.
1202 ///
1203 /// # Returns
1204 ///
1205 /// Data in the specified format, or an empty object/array if no paths are found.
1206 pub fn get_evaluated_schema_by_paths(&mut self, paths: &[String], skip_layout: bool, format: Option<ReturnFormat>) -> Value {
1207 let format = format.unwrap_or_default();
1208 if !skip_layout {
1209 self.resolve_layout_internal();
1210 }
1211
1212 let mut result = serde_json::Map::new();
1213
1214 for path in paths {
1215 // Convert dotted notation to JSON pointer
1216 let pointer = if path.is_empty() {
1217 "".to_string()
1218 } else {
1219 format!("/{}", path.replace(".", "/"))
1220 };
1221
1222 // Get value at path, skip if not found
1223 if let Some(value) = self.evaluated_schema.pointer(&pointer) {
1224 // Store the full path structure to maintain the hierarchy
1225 // Clone only once per path
1226 self.insert_at_path(&mut result, path, value.clone());
1227 }
1228 }
1229
1230 self.convert_to_format(result, paths, format)
1231 }
1232
1233 /// Helper function to insert a value at a dotted path in a JSON object
1234 fn insert_at_path(&self, obj: &mut serde_json::Map<String, Value>, path: &str, value: Value) {
1235 if path.is_empty() {
1236 // If path is empty, merge the value into the root
1237 if let Value::Object(map) = value {
1238 for (k, v) in map {
1239 obj.insert(k, v);
1240 }
1241 }
1242 return;
1243 }
1244
1245 let parts: Vec<&str> = path.split('.').collect();
1246 if parts.is_empty() {
1247 return;
1248 }
1249
1250 let mut current = obj;
1251 let last_index = parts.len() - 1;
1252
1253 for (i, part) in parts.iter().enumerate() {
1254 if i == last_index {
1255 // Last part - insert the value
1256 current.insert(part.to_string(), value);
1257 break;
1258 } else {
1259 // Intermediate part - ensure object exists
1260 current = current
1261 .entry(part.to_string())
1262 .or_insert_with(|| Value::Object(serde_json::Map::new()))
1263 .as_object_mut()
1264 .unwrap();
1265 }
1266 }
1267 }
1268
1269 /// Convert result map to the requested format
1270 fn convert_to_format(&self, result: serde_json::Map<String, Value>, paths: &[String], format: ReturnFormat) -> Value {
1271 match format {
1272 ReturnFormat::Nested => Value::Object(result),
1273 ReturnFormat::Flat => {
1274 // Flatten nested object to dotted keys
1275 let mut flat = serde_json::Map::new();
1276 self.flatten_object(&result, String::new(), &mut flat);
1277 Value::Object(flat)
1278 }
1279 ReturnFormat::Array => {
1280 // Return array of values in order of requested paths
1281 let values: Vec<Value> = paths.iter()
1282 .map(|path| {
1283 let pointer = if path.is_empty() {
1284 "".to_string()
1285 } else {
1286 format!("/{}", path.replace(".", "/"))
1287 };
1288 Value::Object(result.clone()).pointer(&pointer).cloned().unwrap_or(Value::Null)
1289 })
1290 .collect();
1291 Value::Array(values)
1292 }
1293 }
1294 }
1295
1296 /// Recursively flatten a nested object into dotted keys
1297 fn flatten_object(&self, obj: &serde_json::Map<String, Value>, prefix: String, result: &mut serde_json::Map<String, Value>) {
1298 for (key, value) in obj {
1299 let new_key = if prefix.is_empty() {
1300 key.clone()
1301 } else {
1302 format!("{}.{}", prefix, key)
1303 };
1304
1305 if let Value::Object(nested) = value {
1306 self.flatten_object(nested, new_key, result);
1307 } else {
1308 result.insert(new_key, value.clone());
1309 }
1310 }
1311 }
1312
1313 /// Get a value from the schema using dotted path notation.
1314 /// Converts dotted notation (e.g., "properties.field.value") to JSON pointer format.
1315 ///
1316 /// # Arguments
1317 ///
1318 /// * `path` - The dotted path to the value (e.g., "properties.field.value")
1319 ///
1320 /// # Returns
1321 ///
1322 /// The value at the specified path, or None if not found.
1323 pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
1324 // Convert dotted notation to JSON pointer
1325 let pointer = if path.is_empty() {
1326 "".to_string()
1327 } else {
1328 format!("/{}", path.replace(".", "/"))
1329 };
1330
1331 self.schema.pointer(&pointer).cloned()
1332 }
1333
1334 /// Get values from the schema using multiple dotted path notations.
1335 /// Returns data in the specified format. Skips paths that are not found.
1336 ///
1337 /// # Arguments
1338 ///
1339 /// * `paths` - Array of dotted paths to retrieve (e.g., ["properties.field1", "properties.field2"])
1340 /// * `format` - Optional return format (Nested, Flat, or Array). Defaults to Nested.
1341 ///
1342 /// # Returns
1343 ///
1344 /// Data in the specified format, or an empty object/array if no paths are found.
1345 pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
1346 let format = format.unwrap_or_default();
1347 let mut result = serde_json::Map::new();
1348
1349 for path in paths {
1350 // Convert dotted notation to JSON pointer
1351 let pointer = if path.is_empty() {
1352 "".to_string()
1353 } else {
1354 format!("/{}", path.replace(".", "/"))
1355 };
1356
1357 // Get value at path, skip if not found
1358 if let Some(value) = self.schema.pointer(&pointer) {
1359 // Store the full path structure to maintain the hierarchy
1360 // Clone only once per path
1361 self.insert_at_path(&mut result, path, value.clone());
1362 }
1363 }
1364
1365 self.convert_to_format(result, paths, format)
1366 }
1367
1368 /// Check if a dependency should be cached
1369 /// Caches everything except keys starting with $ (except $context)
1370 #[inline]
1371 fn should_cache_dependency(key: &str) -> bool {
1372 if key.starts_with("/$") || key.starts_with('$') {
1373 // Only cache $context, exclude other $ keys like $params
1374 key == "$context" || key.starts_with("$context.") || key.starts_with("/$context")
1375 } else {
1376 true
1377 }
1378 }
1379
1380 /// Helper: Try to get cached result for an evaluation (thread-safe)
1381 /// Helper: Try to get cached result (zero-copy via Arc)
1382 fn try_get_cached(&self, eval_key: &str, eval_data: &EvalData) -> Option<Value> {
1383 // Skip cache lookup if caching is disabled
1384 if !self.cache_enabled {
1385 return None;
1386 }
1387
1388 // Get dependencies for this evaluation
1389 let deps = self.dependencies.get(eval_key)?;
1390
1391 // If no dependencies, use simple cache key
1392 let cache_key = if deps.is_empty() {
1393 CacheKey::simple(eval_key.to_string())
1394 } else {
1395 // Filter dependencies (exclude $ keys except $context)
1396 let filtered_deps: IndexSet<String> = deps
1397 .iter()
1398 .filter(|dep_key| JSONEval::should_cache_dependency(dep_key))
1399 .cloned()
1400 .collect();
1401
1402 // Collect dependency values
1403 let dep_values: Vec<(String, &Value)> = filtered_deps
1404 .iter()
1405 .filter_map(|dep_key| {
1406 eval_data.get(dep_key).map(|v| (dep_key.clone(), v))
1407 })
1408 .collect();
1409
1410 CacheKey::new(eval_key.to_string(), &filtered_deps, &dep_values)
1411 };
1412
1413 // Try cache lookup (zero-copy via Arc, thread-safe)
1414 self.eval_cache.get(&cache_key).map(|arc_val| (*arc_val).clone())
1415 }
1416
1417 /// Helper: Store evaluation result in cache (thread-safe)
1418 fn cache_result(&self, eval_key: &str, value: Value, eval_data: &EvalData) {
1419 // Skip cache insertion if caching is disabled
1420 if !self.cache_enabled {
1421 return;
1422 }
1423
1424 // Get dependencies for this evaluation
1425 let deps = match self.dependencies.get(eval_key) {
1426 Some(d) => d,
1427 None => {
1428 // No dependencies - use simple cache key
1429 let cache_key = CacheKey::simple(eval_key.to_string());
1430 self.eval_cache.insert(cache_key, value);
1431 return;
1432 }
1433 };
1434
1435 // Filter and collect dependency values (exclude $ keys except $context)
1436 let filtered_deps: IndexSet<String> = deps
1437 .iter()
1438 .filter(|dep_key| JSONEval::should_cache_dependency(dep_key))
1439 .cloned()
1440 .collect();
1441
1442 let dep_values: Vec<(String, &Value)> = filtered_deps
1443 .iter()
1444 .filter_map(|dep_key| {
1445 eval_data.get(dep_key).map(|v| (dep_key.clone(), v))
1446 })
1447 .collect();
1448
1449 let cache_key = CacheKey::new(eval_key.to_string(), &filtered_deps, &dep_values);
1450 self.eval_cache.insert(cache_key, value);
1451 }
1452
1453 /// Selectively purge cache entries that depend on changed data paths
1454 /// Only removes cache entries whose dependencies intersect with changed_paths
1455 /// Compares old vs new values and only purges if values actually changed
1456 fn purge_cache_for_changed_data_with_comparison(
1457 &self,
1458 changed_data_paths: &[String],
1459 old_data: &Value,
1460 new_data: &Value
1461 ) {
1462 if changed_data_paths.is_empty() {
1463 return;
1464 }
1465
1466 // Check which paths actually have different values
1467 let mut actually_changed_paths = Vec::new();
1468 for path in changed_data_paths {
1469 let old_val = old_data.pointer(path);
1470 let new_val = new_data.pointer(path);
1471
1472 // Only add to changed list if values differ
1473 if old_val != new_val {
1474 actually_changed_paths.push(path.clone());
1475 }
1476 }
1477
1478 // If no values actually changed, no need to purge
1479 if actually_changed_paths.is_empty() {
1480 return;
1481 }
1482
1483 // Find all eval_keys that depend on the actually changed data paths
1484 let mut affected_eval_keys = IndexSet::new();
1485
1486 for (eval_key, deps) in self.dependencies.iter() {
1487 // Check if this evaluation depends on any of the changed paths
1488 let is_affected = deps.iter().any(|dep| {
1489 // Check if the dependency matches any changed path
1490 actually_changed_paths.iter().any(|changed_path| {
1491 // Exact match or prefix match (for nested fields)
1492 dep == changed_path ||
1493 dep.starts_with(&format!("{}/", changed_path)) ||
1494 changed_path.starts_with(&format!("{}/", dep))
1495 })
1496 });
1497
1498 if is_affected {
1499 affected_eval_keys.insert(eval_key.clone());
1500 }
1501 }
1502
1503 // Remove all cache entries for affected eval_keys using retain
1504 // Keep entries whose eval_key is NOT in the affected set
1505 self.eval_cache.retain(|cache_key, _| {
1506 !affected_eval_keys.contains(&cache_key.eval_key)
1507 });
1508 }
1509
1510 /// Selectively purge cache entries that depend on changed data paths
1511 /// Simpler version without value comparison for cases where we don't have old data
1512 fn purge_cache_for_changed_data(&self, changed_data_paths: &[String]) {
1513 if changed_data_paths.is_empty() {
1514 return;
1515 }
1516
1517 // Find all eval_keys that depend on the changed paths
1518 let mut affected_eval_keys = IndexSet::new();
1519
1520 for (eval_key, deps) in self.dependencies.iter() {
1521 // Check if this evaluation depends on any of the changed paths
1522 let is_affected = deps.iter().any(|dep| {
1523 // Check if dependency path matches any changed data path using flexible matching
1524 changed_data_paths.iter().any(|changed_for_purge| {
1525 // Check both directions:
1526 // 1. Dependency matches changed data (dependency is child of change)
1527 // 2. Changed data matches dependency (change is child of dependency)
1528 Self::paths_match_flexible(dep, changed_for_purge) ||
1529 Self::paths_match_flexible(changed_for_purge, dep)
1530 })
1531 });
1532
1533 if is_affected {
1534 affected_eval_keys.insert(eval_key.clone());
1535 }
1536 }
1537
1538 // Remove all cache entries for affected eval_keys using retain
1539 // Keep entries whose eval_key is NOT in the affected set
1540 self.eval_cache.retain(|cache_key, _| {
1541 !affected_eval_keys.contains(&cache_key.eval_key)
1542 });
1543 }
1544
1545 /// Flexible path matching that handles structural schema keywords (e.g. properties, oneOf)
1546 /// Returns true if schema_path structurally matches data_path
1547 fn paths_match_flexible(schema_path: &str, data_path: &str) -> bool {
1548 let s_segs: Vec<&str> = schema_path.trim_start_matches('#').trim_start_matches('/').split('/').filter(|s| !s.is_empty()).collect();
1549 let d_segs: Vec<&str> = data_path.trim_start_matches('/').split('/').filter(|s| !s.is_empty()).collect();
1550
1551 let mut d_idx = 0;
1552
1553 for s_seg in s_segs {
1554 // If we matched all data segments, we are good (schema is deeper/parent)
1555 if d_idx >= d_segs.len() {
1556 return true;
1557 }
1558
1559 let d_seg = d_segs[d_idx];
1560
1561 if s_seg == d_seg {
1562 // Exact match, advance data pointer
1563 d_idx += 1;
1564 } else if s_seg == "items" || s_seg == "additionalProperties" || s_seg == "patternProperties" {
1565 // Wildcard match for arrays/maps - consume data segment if it looks valid
1566 // Note: items matches array index (numeric). additionalProperties matches any key.
1567 if s_seg == "items" {
1568 // Only match if data segment is numeric (array index)
1569 if d_seg.chars().all(|c| c.is_ascii_digit()) {
1570 d_idx += 1;
1571 }
1572 } else {
1573 // additionalProperties/patternProperties matches any string key
1574 d_idx += 1;
1575 }
1576 } else if Self::is_structural_keyword(s_seg) || s_seg.chars().all(|c| c.is_ascii_digit()) {
1577 // Skip structural keywords (properties, oneOf, etc) and numeric indices in schema (e.g. oneOf/0)
1578 continue;
1579 } else {
1580 // Mismatch: schema has a named segment that data doesn't have
1581 return false;
1582 }
1583 }
1584
1585 // Return true if we consumed all data segments
1586 // (If data is longer than schema, it's NOT a match - e.g. path is too deep for this schema node)
1587 // Wait, if dependency is on /a/b, and change is /a/b/c.
1588 // Schema: /a/b. Data: /a/b/c.
1589 // s runs out. d remains.
1590 // Is /a/b a valid dependency for /a/b/c?
1591 // Yes, parent invalidation.
1592 // But the calling logic checks both directions (dep vs change, change vs dep).
1593 // This function checks if "schema_path covers data_path".
1594 // If s runs out and d remains, it means schema path is a PREFIX of data path structure.
1595 // So return true.
1596 true
1597 }
1598
1599 fn is_structural_keyword(s: &str) -> bool {
1600 matches!(s,
1601 "properties" | "definitions" | "$defs" |
1602 "allOf" | "anyOf" | "oneOf" |
1603 "not" | "if" | "then" | "else" |
1604 "dependentSchemas" | "$params" | "dependencies"
1605 )
1606 }
1607
1608 /// Purge cache entries that depend on context
1609 fn purge_cache_for_context_change(&self) {
1610 // Find all eval_keys that depend on $context
1611 let mut affected_eval_keys = IndexSet::new();
1612
1613 for (eval_key, deps) in self.dependencies.iter() {
1614 let is_affected = deps.iter().any(|dep| {
1615 dep == "$context" || dep.starts_with("$context.") || dep.starts_with("/$context")
1616 });
1617
1618 if is_affected {
1619 affected_eval_keys.insert(eval_key.clone());
1620 }
1621 }
1622
1623 self.eval_cache.retain(|cache_key, _| {
1624 !affected_eval_keys.contains(&cache_key.eval_key)
1625 });
1626 }
1627
1628 /// Get cache statistics
1629 pub fn cache_stats(&self) -> CacheStats {
1630 self.eval_cache.stats()
1631 }
1632
1633 /// Clear evaluation cache
1634 pub fn clear_cache(&mut self) {
1635 self.eval_cache.clear();
1636 for subform in self.subforms.values_mut() {
1637 subform.clear_cache();
1638 }
1639 }
1640
1641 /// Get number of cached entries
1642 pub fn cache_len(&self) -> usize {
1643 self.eval_cache.len()
1644 }
1645
1646 /// Enable evaluation caching
1647 /// Useful for reusing JSONEval instances with different data
1648 pub fn enable_cache(&mut self) {
1649 self.cache_enabled = true;
1650 for subform in self.subforms.values_mut() {
1651 subform.enable_cache();
1652 }
1653 }
1654
1655 /// Disable evaluation caching
1656 /// Useful for web API usage where each request creates a new JSONEval instance
1657 /// Improves performance by skipping cache operations that have no benefit for single-use instances
1658 pub fn disable_cache(&mut self) {
1659 self.cache_enabled = false;
1660 self.eval_cache.clear(); // Clear any existing cache entries
1661 for subform in self.subforms.values_mut() {
1662 subform.disable_cache();
1663 }
1664 }
1665
1666 /// Check if caching is enabled
1667 pub fn is_cache_enabled(&self) -> bool {
1668 self.cache_enabled
1669 }
1670
1671 fn evaluate_others(&mut self, paths: Option<&[String]>) {
1672 time_block!(" evaluate_others()", {
1673 // Step 1: Evaluate options URL templates (handles {variable} patterns)
1674 time_block!(" evaluate_options_templates", {
1675 self.evaluate_options_templates(paths);
1676 });
1677
1678 // Step 2: Evaluate "rules" and "others" categories with caching
1679 // Rules are evaluated here so their values are available in evaluated_schema
1680 let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
1681 if combined_count == 0 {
1682 return;
1683 }
1684
1685 time_block!(" evaluate rules+others", {
1686 let eval_data_snapshot = self.eval_data.clone();
1687
1688 let normalized_paths: Option<Vec<String>> = paths.map(|p_list| {
1689 p_list.iter()
1690 .flat_map(|p| {
1691 let ptr = path_utils::dot_notation_to_schema_pointer(p);
1692 // Also support version with /properties/ prefix for root match
1693 let with_props = if ptr.starts_with("#/") {
1694 format!("#/properties/{}", &ptr[2..])
1695 } else {
1696 ptr.clone()
1697 };
1698 vec![ptr, with_props]
1699 })
1700 .collect()
1701 });
1702
1703 #[cfg(feature = "parallel")]
1704 {
1705 let combined_results: Mutex<Vec<(String, Value)>> = Mutex::new(Vec::with_capacity(combined_count));
1706
1707 self.rules_evaluations
1708 .par_iter()
1709 .chain(self.others_evaluations.par_iter())
1710 .for_each(|eval_key| {
1711 // Filter items if paths are provided
1712 if let Some(filter_paths) = normalized_paths.as_ref() {
1713 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())) {
1714 return;
1715 }
1716 }
1717
1718 let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
1719
1720 // Try cache first (thread-safe)
1721 if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
1722 return;
1723 }
1724
1725 // Cache miss - evaluate
1726 if let Some(logic_id) = self.evaluations.get(eval_key) {
1727 if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
1728 let cleaned_val = clean_float_noise(val);
1729 // Cache result (thread-safe)
1730 self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
1731 combined_results.lock().unwrap().push((pointer_path, cleaned_val));
1732 }
1733 }
1734 });
1735
1736 // Write results to evaluated_schema
1737 for (result_path, value) in combined_results.into_inner().unwrap() {
1738 if let Some(pointer_value) = self.evaluated_schema.pointer_mut(&result_path) {
1739 // Special handling for rules with $evaluation
1740 // This includes both direct rules and array items: /rules/evaluation/0/$evaluation
1741 if !result_path.starts_with("$") && result_path.contains("/rules/") && !result_path.ends_with("/value") {
1742 match pointer_value.as_object_mut() {
1743 Some(pointer_obj) => {
1744 pointer_obj.remove("$evaluation");
1745 pointer_obj.insert("value".to_string(), value);
1746 },
1747 None => continue,
1748 }
1749 } else {
1750 *pointer_value = value;
1751 }
1752 }
1753 }
1754 }
1755
1756 #[cfg(not(feature = "parallel"))]
1757 {
1758 // Sequential evaluation
1759 let combined_evals: Vec<&String> = self.rules_evaluations.iter()
1760 .chain(self.others_evaluations.iter())
1761 .collect();
1762
1763 for eval_key in combined_evals {
1764 // Filter items if paths are provided
1765 if let Some(filter_paths) = normalized_paths.as_ref() {
1766 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())) {
1767 continue;
1768 }
1769 }
1770
1771 let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
1772
1773 // Try cache first
1774 if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
1775 continue;
1776 }
1777
1778 // Cache miss - evaluate
1779 if let Some(logic_id) = self.evaluations.get(eval_key) {
1780 if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
1781 let cleaned_val = clean_float_noise(val);
1782 // Cache result
1783 self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
1784
1785 if let Some(pointer_value) = self.evaluated_schema.pointer_mut(&pointer_path) {
1786 if !pointer_path.starts_with("$") && pointer_path.contains("/rules/") && !pointer_path.ends_with("/value") {
1787 match pointer_value.as_object_mut() {
1788 Some(pointer_obj) => {
1789 pointer_obj.remove("$evaluation");
1790 pointer_obj.insert("value".to_string(), cleaned_val);
1791 },
1792 None => continue,
1793 }
1794 } else {
1795 *pointer_value = cleaned_val;
1796 }
1797 }
1798 }
1799 }
1800 }
1801 }
1802 });
1803 });
1804 }
1805
1806 /// Evaluate options URL templates (handles {variable} patterns)
1807 fn evaluate_options_templates(&mut self, paths: Option<&[String]>) {
1808 // Use pre-collected options templates from parsing (Arc clone is cheap)
1809 let templates_to_eval = self.options_templates.clone();
1810
1811 // Evaluate each template
1812 for (path, template_str, params_path) in templates_to_eval.iter() {
1813 // Filter items if paths are provided
1814 // 'path' here is the schema path to the field (dot notation or similar, need to check)
1815 // It seems to be schema pointer based on usage in other methods
1816 if let Some(filter_paths) = paths {
1817 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| path.starts_with(p.as_str()) || p.starts_with(path.as_str())) {
1818 continue;
1819 }
1820 }
1821
1822 if let Some(params) = self.evaluated_schema.pointer(¶ms_path) {
1823 if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
1824 if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
1825 *target = Value::String(evaluated);
1826 }
1827 }
1828 }
1829 }
1830 }
1831
1832 /// Evaluate a template string like "api/users/{id}" with params
1833 fn evaluate_template(&self, template: &str, params: &Value) -> Result<String, String> {
1834 let mut result = template.to_string();
1835
1836 // Simple template evaluation: replace {key} with params.key
1837 if let Value::Object(params_map) = params {
1838 for (key, value) in params_map {
1839 let placeholder = format!("{{{}}}", key);
1840 if let Some(str_val) = value.as_str() {
1841 result = result.replace(&placeholder, str_val);
1842 } else {
1843 // Convert non-string values to strings
1844 result = result.replace(&placeholder, &value.to_string());
1845 }
1846 }
1847 }
1848
1849 Ok(result)
1850 }
1851
1852 /// Compile a logic expression from a JSON string and store it globally
1853 ///
1854 /// Returns a CompiledLogicId that can be used with run_logic for zero-clone evaluation.
1855 /// The compiled logic is stored in a global thread-safe cache and can be shared across
1856 /// different JSONEval instances. If the same logic was compiled before, returns the existing ID.
1857 ///
1858 /// For repeated evaluations with different data, compile once and run multiple times.
1859 ///
1860 /// # Arguments
1861 ///
1862 /// * `logic_str` - JSON logic expression as a string
1863 ///
1864 /// # Returns
1865 ///
1866 /// A CompiledLogicId that can be reused for multiple evaluations across instances
1867 pub fn compile_logic(&self, logic_str: &str) -> Result<CompiledLogicId, String> {
1868 rlogic::compiled_logic_store::compile_logic(logic_str)
1869 }
1870
1871 /// Compile a logic expression from a Value and store it globally
1872 ///
1873 /// This is more efficient than compile_logic when you already have a parsed Value,
1874 /// as it avoids the JSON string serialization/parsing overhead.
1875 ///
1876 /// Returns a CompiledLogicId that can be used with run_logic for zero-clone evaluation.
1877 /// The compiled logic is stored in a global thread-safe cache and can be shared across
1878 /// different JSONEval instances. If the same logic was compiled before, returns the existing ID.
1879 ///
1880 /// # Arguments
1881 ///
1882 /// * `logic` - JSON logic expression as a Value
1883 ///
1884 /// # Returns
1885 ///
1886 /// A CompiledLogicId that can be reused for multiple evaluations across instances
1887 pub fn compile_logic_value(&self, logic: &Value) -> Result<CompiledLogicId, String> {
1888 rlogic::compiled_logic_store::compile_logic_value(logic)
1889 }
1890
1891 /// Run pre-compiled logic with zero-clone pattern
1892 ///
1893 /// Uses references to avoid data cloning - similar to evaluate method.
1894 /// This is the most efficient way to evaluate logic multiple times with different data.
1895 /// The CompiledLogicId is retrieved from global storage, allowing the same compiled logic
1896 /// to be used across different JSONEval instances.
1897 ///
1898 /// # Arguments
1899 ///
1900 /// * `logic_id` - Pre-compiled logic ID from compile_logic
1901 /// * `data` - Optional data to evaluate against (uses existing data if None)
1902 /// * `context` - Optional context to use (uses existing context if None)
1903 ///
1904 /// # Returns
1905 ///
1906 /// The result of the evaluation as a Value
1907 pub fn run_logic(&mut self, logic_id: CompiledLogicId, data: Option<&Value>, context: Option<&Value>) -> Result<Value, String> {
1908 // Get compiled logic from global store
1909 let compiled_logic = rlogic::compiled_logic_store::get_compiled_logic(logic_id)
1910 .ok_or_else(|| format!("Compiled logic ID {:?} not found in store", logic_id))?;
1911
1912 // Get the data to evaluate against
1913 // If custom data is provided, merge it with context and $params
1914 // Otherwise, use the existing eval_data which already has everything merged
1915 let eval_data_value = if let Some(input_data) = data {
1916 let context_value = context.unwrap_or(&self.context);
1917
1918 self.eval_data.replace_data_and_context(input_data.clone(), context_value.clone());
1919 self.eval_data.data()
1920 } else {
1921 self.eval_data.data()
1922 };
1923
1924 // Create an evaluator and run the pre-compiled logic with zero-clone pattern
1925 let evaluator = Evaluator::new();
1926 let result = evaluator.evaluate(&compiled_logic, &eval_data_value)?;
1927
1928 Ok(clean_float_noise(result))
1929 }
1930
1931 /// Compile and run JSON logic in one step (convenience method)
1932 ///
1933 /// This is a convenience wrapper that combines compile_logic and run_logic.
1934 /// For repeated evaluations with different data, use compile_logic once
1935 /// and run_logic multiple times for better performance.
1936 ///
1937 /// # Arguments
1938 ///
1939 /// * `logic_str` - JSON logic expression as a string
1940 /// * `data` - Optional data JSON string to evaluate against (uses existing data if None)
1941 /// * `context` - Optional context JSON string to use (uses existing context if None)
1942 ///
1943 /// # Returns
1944 ///
1945 /// The result of the evaluation as a Value
1946 pub fn compile_and_run_logic(&mut self, logic_str: &str, data: Option<&str>, context: Option<&str>) -> Result<Value, String> {
1947 // Parse the logic string and compile
1948 let compiled_logic = self.compile_logic(logic_str)?;
1949
1950 // Parse data and context if provided
1951 let data_value = if let Some(data_str) = data {
1952 Some(json_parser::parse_json_str(data_str)?)
1953 } else {
1954 None
1955 };
1956
1957 let context_value = if let Some(ctx_str) = context {
1958 Some(json_parser::parse_json_str(ctx_str)?)
1959 } else {
1960 None
1961 };
1962
1963 // Run the compiled logic
1964 self.run_logic(compiled_logic, data_value.as_ref(), context_value.as_ref())
1965 }
1966
1967 /// Resolve layout references with optional evaluation
1968 ///
1969 /// # Arguments
1970 ///
1971 /// * `evaluate` - If true, runs evaluation before resolving layout. If false, only resolves layout.
1972 ///
1973 /// # Returns
1974 ///
1975 /// A Result indicating success or an error message.
1976 pub fn resolve_layout(&mut self, evaluate: bool) -> Result<(), String> {
1977 if evaluate {
1978 // Use existing data
1979 let data_str = serde_json::to_string(&self.data)
1980 .map_err(|e| format!("Failed to serialize data: {}", e))?;
1981 self.evaluate(&data_str, None, None)?;
1982 }
1983
1984 self.resolve_layout_internal();
1985 Ok(())
1986 }
1987
1988 fn resolve_layout_internal(&mut self) {
1989 time_block!(" resolve_layout_internal()", {
1990 // Use cached layout paths (collected at parse time)
1991 // Clone Arc reference (cheap)
1992 let layout_paths = self.layout_paths.clone();
1993
1994 time_block!(" resolve_layout_elements", {
1995 for layout_path in layout_paths.iter() {
1996 self.resolve_layout_elements(layout_path);
1997 }
1998 });
1999
2000 // After resolving all references, propagate parent hidden/disabled to children
2001 time_block!(" propagate_parent_conditions", {
2002 for layout_path in layout_paths.iter() {
2003 self.propagate_parent_conditions(layout_path);
2004 }
2005 });
2006 });
2007 }
2008
2009 /// Propagate parent hidden/disabled conditions to children recursively
2010 fn propagate_parent_conditions(&mut self, layout_elements_path: &str) {
2011 // Normalize path from schema format (#/) to JSON pointer format (/)
2012 let normalized_path = path_utils::normalize_to_json_pointer(layout_elements_path);
2013
2014 // Extract elements array to avoid borrow checker issues
2015 let elements = if let Some(Value::Array(arr)) = self.evaluated_schema.pointer_mut(&normalized_path) {
2016 mem::take(arr)
2017 } else {
2018 return;
2019 };
2020
2021 // Process elements (now we can borrow self immutably)
2022 let mut updated_elements = Vec::with_capacity(elements.len());
2023 for element in elements {
2024 updated_elements.push(self.apply_parent_conditions(element, false, false));
2025 }
2026
2027 // Write back the updated elements
2028 if let Some(target) = self.evaluated_schema.pointer_mut(&normalized_path) {
2029 *target = Value::Array(updated_elements);
2030 }
2031 }
2032
2033 /// Recursively apply parent hidden/disabled conditions to an element and its children
2034 fn apply_parent_conditions(&self, element: Value, parent_hidden: bool, parent_disabled: bool) -> Value {
2035 if let Value::Object(mut map) = element {
2036 // Get current element's condition
2037 let mut element_hidden = parent_hidden;
2038 let mut element_disabled = parent_disabled;
2039
2040 // Check condition field (used by field elements with $ref)
2041 if let Some(Value::Object(condition)) = map.get("condition") {
2042 if let Some(Value::Bool(hidden)) = condition.get("hidden") {
2043 element_hidden = element_hidden || *hidden;
2044 }
2045 if let Some(Value::Bool(disabled)) = condition.get("disabled") {
2046 element_disabled = element_disabled || *disabled;
2047 }
2048 }
2049
2050 // Check hideLayout field (used by direct layout elements without $ref)
2051 if let Some(Value::Object(hide_layout)) = map.get("hideLayout") {
2052 // Check hideLayout.all
2053 if let Some(Value::Bool(all_hidden)) = hide_layout.get("all") {
2054 if *all_hidden {
2055 element_hidden = true;
2056 }
2057 }
2058 }
2059
2060 // Update condition to include parent state (for field elements)
2061 if parent_hidden || parent_disabled {
2062 // Update condition field if it exists or if this is a field element
2063 if map.contains_key("condition") || map.contains_key("$ref") || map.contains_key("$fullpath") {
2064 let mut condition = if let Some(Value::Object(c)) = map.get("condition") {
2065 c.clone()
2066 } else {
2067 serde_json::Map::new()
2068 };
2069
2070 if parent_hidden {
2071 condition.insert("hidden".to_string(), Value::Bool(true));
2072 }
2073 if parent_disabled {
2074 condition.insert("disabled".to_string(), Value::Bool(true));
2075 }
2076
2077 map.insert("condition".to_string(), Value::Object(condition));
2078 }
2079
2080 // Update hideLayout for direct layout elements
2081 if parent_hidden && (map.contains_key("hideLayout") || map.contains_key("type")) {
2082 let mut hide_layout = if let Some(Value::Object(h)) = map.get("hideLayout") {
2083 h.clone()
2084 } else {
2085 serde_json::Map::new()
2086 };
2087
2088 // Set hideLayout.all to true when parent is hidden
2089 hide_layout.insert("all".to_string(), Value::Bool(true));
2090 map.insert("hideLayout".to_string(), Value::Object(hide_layout));
2091 }
2092 }
2093
2094 // Update $parentHide flag if element has it (came from $ref resolution)
2095 // Only update if the element already has the field (to avoid adding it to non-ref elements)
2096 if map.contains_key("$parentHide") {
2097 map.insert("$parentHide".to_string(), Value::Bool(parent_hidden));
2098 }
2099
2100 // Recursively process children if elements array exists
2101 if let Some(Value::Array(elements)) = map.get("elements") {
2102 let mut updated_children = Vec::with_capacity(elements.len());
2103 for child in elements {
2104 updated_children.push(self.apply_parent_conditions(
2105 child.clone(),
2106 element_hidden,
2107 element_disabled,
2108 ));
2109 }
2110 map.insert("elements".to_string(), Value::Array(updated_children));
2111 }
2112
2113 return Value::Object(map);
2114 }
2115
2116 element
2117 }
2118
2119 /// Resolve $ref references in layout elements (recursively)
2120 fn resolve_layout_elements(&mut self, layout_elements_path: &str) {
2121 // Normalize path from schema format (#/) to JSON pointer format (/)
2122 let normalized_path = path_utils::normalize_to_json_pointer(layout_elements_path);
2123
2124 // Always read elements from original schema (not evaluated_schema)
2125 // This ensures we get fresh $ref entries on re-evaluation
2126 // since evaluated_schema elements get mutated to objects after first resolution
2127 let elements = if let Some(Value::Array(arr)) = self.schema.pointer(&normalized_path) {
2128 arr.clone()
2129 } else {
2130 return;
2131 };
2132
2133 // Extract the parent path from normalized_path (e.g., "/properties/form/$layout/elements" -> "form.$layout")
2134 let parent_path = normalized_path
2135 .trim_start_matches('/')
2136 .replace("/elements", "")
2137 .replace('/', ".");
2138
2139 // Process elements (now we can borrow self immutably)
2140 let mut resolved_elements = Vec::with_capacity(elements.len());
2141 for (index, element) in elements.iter().enumerate() {
2142 let element_path = if parent_path.is_empty() {
2143 format!("elements.{}", index)
2144 } else {
2145 format!("{}.elements.{}", parent_path, index)
2146 };
2147 let resolved = self.resolve_element_ref_recursive(element.clone(), &element_path);
2148 resolved_elements.push(resolved);
2149 }
2150
2151 // Write back the resolved elements
2152 if let Some(target) = self.evaluated_schema.pointer_mut(&normalized_path) {
2153 *target = Value::Array(resolved_elements);
2154 }
2155 }
2156
2157 /// Recursively resolve $ref in an element and its nested elements
2158 /// path_context: The dotted path to the current element (e.g., "form.$layout.elements.0")
2159 fn resolve_element_ref_recursive(&self, element: Value, path_context: &str) -> Value {
2160 // First resolve the current element's $ref
2161 let resolved = self.resolve_element_ref(element);
2162
2163 // Then recursively resolve any nested elements arrays
2164 if let Value::Object(mut map) = resolved {
2165 // Ensure all layout elements have metadata fields
2166 // For elements with $ref, these were already set by resolve_element_ref
2167 // For direct layout elements without $ref, set them based on path_context
2168 if !map.contains_key("$parentHide") {
2169 map.insert("$parentHide".to_string(), Value::Bool(false));
2170 }
2171
2172 // Set path metadata for direct layout elements (without $ref)
2173 if !map.contains_key("$fullpath") {
2174 map.insert("$fullpath".to_string(), Value::String(path_context.to_string()));
2175 }
2176
2177 if !map.contains_key("$path") {
2178 // Extract last segment from path_context
2179 let last_segment = path_context.split('.').last().unwrap_or(path_context);
2180 map.insert("$path".to_string(), Value::String(last_segment.to_string()));
2181 }
2182
2183 // Check if this object has an "elements" array
2184 if let Some(Value::Array(elements)) = map.get("elements") {
2185 let mut resolved_nested = Vec::with_capacity(elements.len());
2186 for (index, nested_element) in elements.iter().enumerate() {
2187 let nested_path = format!("{}.elements.{}", path_context, index);
2188 resolved_nested.push(self.resolve_element_ref_recursive(nested_element.clone(), &nested_path));
2189 }
2190 map.insert("elements".to_string(), Value::Array(resolved_nested));
2191 }
2192
2193 return Value::Object(map);
2194 }
2195
2196 resolved
2197 }
2198
2199 /// Resolve $ref in a single element
2200 fn resolve_element_ref(&self, element: Value) -> Value {
2201 match element {
2202 Value::Object(mut map) => {
2203 // Check if element has $ref
2204 if let Some(Value::String(ref_path)) = map.get("$ref").cloned() {
2205 // Convert ref_path to dotted notation for metadata storage
2206 let dotted_path = path_utils::pointer_to_dot_notation(&ref_path);
2207
2208 // Extract last segment for $path and path fields
2209 let last_segment = dotted_path.split('.').last().unwrap_or(&dotted_path);
2210
2211 // Inject metadata fields with dotted notation
2212 map.insert("$fullpath".to_string(), Value::String(dotted_path.clone()));
2213 map.insert("$path".to_string(), Value::String(last_segment.to_string()));
2214 map.insert("$parentHide".to_string(), Value::Bool(false));
2215
2216 // Normalize to JSON pointer for actual lookup
2217 // Try different path formats to find the referenced value
2218 let normalized_path = if ref_path.starts_with('#') || ref_path.starts_with('/') {
2219 // Already a pointer, normalize it
2220 path_utils::normalize_to_json_pointer(&ref_path)
2221 } else {
2222 // Try as schema path first (for paths like "illustration.insured.name")
2223 let schema_pointer = path_utils::dot_notation_to_schema_pointer(&ref_path);
2224 let schema_path = path_utils::normalize_to_json_pointer(&schema_pointer);
2225
2226 // Check if it exists
2227 if self.evaluated_schema.pointer(&schema_path).is_some() {
2228 schema_path
2229 } else {
2230 // Try with /properties/ prefix (for simple refs like "parent_container")
2231 let with_properties = format!("/properties/{}", ref_path.replace('.', "/properties/"));
2232 with_properties
2233 }
2234 };
2235
2236 // Get the referenced value
2237 if let Some(referenced_value) = self.evaluated_schema.pointer(&normalized_path) {
2238 // Clone the referenced value
2239 let resolved = referenced_value.clone();
2240
2241 // If resolved is an object, check for special handling
2242 if let Value::Object(mut resolved_map) = resolved {
2243 // Remove $ref from original map
2244 map.remove("$ref");
2245
2246 // Special case: if resolved has $layout, flatten it
2247 // Extract $layout contents and merge at root level
2248 if let Some(Value::Object(layout_obj)) = resolved_map.remove("$layout") {
2249 // Start with layout properties (they become root properties)
2250 let mut result = layout_obj.clone();
2251
2252 // Remove properties from resolved (we don't want it)
2253 resolved_map.remove("properties");
2254
2255 // Merge remaining resolved_map properties (except type if layout has it)
2256 for (key, value) in resolved_map {
2257 if key != "type" || !result.contains_key("type") {
2258 result.insert(key, value);
2259 }
2260 }
2261
2262 // Finally, merge element override properties
2263 for (key, value) in map {
2264 result.insert(key, value);
2265 }
2266
2267 return Value::Object(result);
2268 } else {
2269 // Normal merge: element properties override referenced properties
2270 for (key, value) in map {
2271 resolved_map.insert(key, value);
2272 }
2273
2274 return Value::Object(resolved_map);
2275 }
2276 } else {
2277 // If referenced value is not an object, just return it
2278 return resolved;
2279 }
2280 }
2281 }
2282
2283 // No $ref or couldn't resolve - return element as-is
2284 Value::Object(map)
2285 }
2286 _ => element,
2287 }
2288 }
2289
2290 /// Evaluate fields that depend on a changed path
2291 /// This processes all dependent fields transitively when a source field changes
2292 ///
2293 /// # Arguments
2294 /// * `changed_paths` - Array of field paths that changed (supports dot notation or schema pointers)
2295 /// * `data` - Optional JSON data to update before processing
2296 /// * `context` - Optional context data
2297 /// * `re_evaluate` - If true, performs full evaluation after processing dependents
2298 pub fn evaluate_dependents(
2299 &mut self,
2300 changed_paths: &[String],
2301 data: Option<&str>,
2302 context: Option<&str>,
2303 re_evaluate: bool,
2304 ) -> Result<Value, String> {
2305 // Acquire lock for synchronous execution
2306 let _lock = self.eval_lock.lock().unwrap();
2307
2308 // Update data if provided
2309 if let Some(data_str) = data {
2310 // Save old data for comparison
2311 let old_data = self.eval_data.clone_data_without(&["$params"]);
2312
2313 let data_value = json_parser::parse_json_str(data_str)?;
2314 let context_value = if let Some(ctx) = context {
2315 json_parser::parse_json_str(ctx)?
2316 } else {
2317 Value::Object(serde_json::Map::new())
2318 };
2319 self.eval_data.replace_data_and_context(data_value.clone(), context_value);
2320
2321 // Selectively purge cache entries that depend on changed data
2322 // Only purge if values actually changed
2323 // Convert changed_paths to data pointer format for cache purging
2324 let data_paths: Vec<String> = changed_paths
2325 .iter()
2326 .map(|path| {
2327 // Robust normalization: normalize to schema pointer first, then strip schema-specific parts
2328 // This handles both "illustration.insured.name" and "#/illustration/properties/insured/properties/name"
2329 let schema_ptr = path_utils::dot_notation_to_schema_pointer(path);
2330
2331 // Remove # prefix and /properties/ segments to get pure data location
2332 let normalized = schema_ptr.trim_start_matches('#')
2333 .replace("/properties/", "/");
2334
2335 // Ensure it starts with / for data pointer
2336 if normalized.starts_with('/') {
2337 normalized
2338 } else {
2339 format!("/{}", normalized)
2340 }
2341 })
2342 .collect();
2343 self.purge_cache_for_changed_data_with_comparison(&data_paths, &old_data, &data_value);
2344 }
2345
2346 let mut result = Vec::new();
2347 let mut processed = IndexSet::new();
2348
2349 // Normalize all changed paths and add to processing queue
2350 // Converts: "illustration.insured.name" -> "#/illustration/properties/insured/properties/name"
2351 let mut to_process: Vec<(String, bool)> = changed_paths
2352 .iter()
2353 .map(|path| (path_utils::dot_notation_to_schema_pointer(path), false))
2354 .collect(); // (path, is_transitive)
2355
2356 // Process dependents recursively (always nested/transitive)
2357 while let Some((current_path, is_transitive)) = to_process.pop() {
2358 if processed.contains(¤t_path) {
2359 continue;
2360 }
2361 processed.insert(current_path.clone());
2362
2363 // Get the value of the changed field for $value context
2364 let current_data_path = path_utils::normalize_to_json_pointer(¤t_path)
2365 .replace("/properties/", "/")
2366 .trim_start_matches('#')
2367 .to_string();
2368 let mut current_value = self.eval_data.data().pointer(¤t_data_path)
2369 .cloned()
2370 .unwrap_or(Value::Null);
2371
2372 // Find dependents for this path
2373 if let Some(dependent_items) = self.dependents_evaluations.get(¤t_path) {
2374 for dep_item in dependent_items {
2375 let ref_path = &dep_item.ref_path;
2376 let pointer_path = path_utils::normalize_to_json_pointer(ref_path);
2377 // Data paths don't include /properties/, strip it for data access
2378 let data_path = pointer_path.replace("/properties/", "/");
2379
2380 let current_ref_value = self.eval_data.data().pointer(&data_path)
2381 .cloned()
2382 .unwrap_or(Value::Null);
2383
2384 // Get field and parent field from schema
2385 let field = self.evaluated_schema.pointer(&pointer_path).cloned();
2386
2387 // Get parent field - skip /properties/ to get actual parent object
2388 let parent_path = if let Some(last_slash) = pointer_path.rfind("/properties") {
2389 &pointer_path[..last_slash]
2390 } else {
2391 "/"
2392 };
2393 let mut parent_field = if parent_path.is_empty() || parent_path == "/" {
2394 self.evaluated_schema.clone()
2395 } else {
2396 self.evaluated_schema.pointer(parent_path).cloned()
2397 .unwrap_or_else(|| Value::Object(serde_json::Map::new()))
2398 };
2399
2400 // omit properties to minimize size of parent field
2401 if let Value::Object(ref mut map) = parent_field {
2402 map.remove("properties");
2403 map.remove("$layout");
2404 }
2405
2406 let mut change_obj = serde_json::Map::new();
2407 change_obj.insert("$ref".to_string(), Value::String(path_utils::pointer_to_dot_notation(&data_path)));
2408 if let Some(f) = field {
2409 change_obj.insert("$field".to_string(), f);
2410 }
2411 change_obj.insert("$parentField".to_string(), parent_field);
2412 change_obj.insert("transitive".to_string(), Value::Bool(is_transitive));
2413
2414 let mut add_transitive = false;
2415 let mut add_deps = false;
2416 // Process clear
2417 if let Some(clear_val) = &dep_item.clear {
2418 let clear_val_clone = clear_val.clone();
2419 let should_clear = Self::evaluate_dependent_value_static(&self.engine, &self.evaluations, &self.eval_data, &clear_val_clone, ¤t_value, ¤t_ref_value)?;
2420 let clear_bool = match should_clear {
2421 Value::Bool(b) => b,
2422 _ => false,
2423 };
2424
2425 if clear_bool {
2426 // Clear the field
2427 if data_path == current_data_path {
2428 current_value = Value::Null;
2429 }
2430 self.eval_data.set(&data_path, Value::Null);
2431 change_obj.insert("clear".to_string(), Value::Bool(true));
2432 add_transitive = true;
2433 add_deps = true;
2434 }
2435 }
2436
2437 // Process value
2438 if let Some(value_val) = &dep_item.value {
2439 let value_val_clone = value_val.clone();
2440 let computed_value = Self::evaluate_dependent_value_static(&self.engine, &self.evaluations, &self.eval_data, &value_val_clone, ¤t_value, ¤t_ref_value)?;
2441 let cleaned_val = clean_float_noise(computed_value.clone());
2442
2443 if cleaned_val != current_ref_value && cleaned_val != Value::Null {
2444 // Set the value
2445 if data_path == current_data_path {
2446 current_value = cleaned_val.clone();
2447 }
2448 self.eval_data.set(&data_path, cleaned_val.clone());
2449 change_obj.insert("value".to_string(), cleaned_val);
2450 add_transitive = true;
2451 add_deps = true;
2452 }
2453 }
2454
2455 // add only when has clear / value
2456 if add_deps {
2457 result.push(Value::Object(change_obj));
2458 }
2459
2460 // Add this dependent to queue for transitive processing
2461 if add_transitive {
2462 to_process.push((ref_path.clone(), true));
2463 }
2464 }
2465 }
2466 }
2467
2468 // If re_evaluate is true, perform full evaluation with the mutated eval_data
2469 // Use evaluate_internal to avoid serialization overhead
2470 // We need to drop the lock first since evaluate_internal acquires its own lock
2471 if re_evaluate {
2472 drop(_lock); // Release the evaluate_dependents lock
2473 self.evaluate_internal(None)?;
2474 }
2475
2476 Ok(Value::Array(result))
2477 }
2478
2479 /// Helper to evaluate a dependent value - uses pre-compiled eval keys for fast lookup
2480 fn evaluate_dependent_value_static(
2481 engine: &RLogic,
2482 evaluations: &IndexMap<String, LogicId>,
2483 eval_data: &EvalData,
2484 value: &Value,
2485 changed_field_value: &Value,
2486 changed_field_ref_value: &Value
2487 ) -> Result<Value, String> {
2488 match value {
2489 // If it's a String, check if it's an eval key reference
2490 Value::String(eval_key) => {
2491 if let Some(logic_id) = evaluations.get(eval_key) {
2492 // It's a pre-compiled evaluation - run it with scoped context
2493 // Create internal context with $value and $refValue
2494 let mut internal_context = serde_json::Map::new();
2495 internal_context.insert("$value".to_string(), changed_field_value.clone());
2496 internal_context.insert("$refValue".to_string(), changed_field_ref_value.clone());
2497 let context_value = Value::Object(internal_context);
2498
2499 let result = engine.run_with_context(logic_id, eval_data.data(), &context_value)
2500 .map_err(|e| format!("Failed to evaluate dependent logic '{}': {}", eval_key, e))?;
2501 Ok(result)
2502 } else {
2503 // It's a regular string value
2504 Ok(value.clone())
2505 }
2506 }
2507 // For backwards compatibility: compile $evaluation on-the-fly
2508 // This shouldn't happen with properly parsed schemas
2509 Value::Object(map) if map.contains_key("$evaluation") => {
2510 Err("Dependent evaluation contains unparsed $evaluation - schema was not properly parsed".to_string())
2511 }
2512 // Primitive value - return as-is
2513 _ => Ok(value.clone()),
2514 }
2515 }
2516
2517 /// Validate form data against schema rules
2518 /// Returns validation errors for fields that don't meet their rules
2519 pub fn validate(
2520 &mut self,
2521 data: &str,
2522 context: Option<&str>,
2523 paths: Option<&[String]>
2524 ) -> Result<ValidationResult, String> {
2525 // Acquire lock for synchronous execution
2526 let _lock = self.eval_lock.lock().unwrap();
2527
2528 // Save old data for comparison
2529 let old_data = self.eval_data.clone_data_without(&["$params"]);
2530
2531 // Parse data and context
2532 let data_value = json_parser::parse_json_str(data)?;
2533 let context_value = if let Some(ctx) = context {
2534 json_parser::parse_json_str(ctx)?
2535 } else {
2536 Value::Object(serde_json::Map::new())
2537 };
2538
2539 // Update eval_data with new data/context
2540 self.eval_data.replace_data_and_context(data_value.clone(), context_value);
2541
2542 // Selectively purge cache for rule evaluations that depend on changed data
2543 // Collect all top-level data keys as potentially changed paths
2544 let changed_data_paths: Vec<String> = if let Some(obj) = data_value.as_object() {
2545 obj.keys().map(|k| format!("/{}", k)).collect()
2546 } else {
2547 Vec::new()
2548 };
2549 self.purge_cache_for_changed_data_with_comparison(&changed_data_paths, &old_data, &data_value);
2550
2551 // Drop lock before calling evaluate_others which needs mutable access
2552 drop(_lock);
2553
2554 // Re-evaluate rule evaluations to ensure fresh values
2555 // This ensures all rule.$evaluation expressions are re-computed
2556 // Re-evaluate rule evaluations to ensure fresh values
2557 // This ensures all rule.$evaluation expressions are re-computed
2558 self.evaluate_others(paths);
2559
2560 // Update evaluated_schema with fresh evaluations
2561 self.evaluated_schema = self.get_evaluated_schema(false);
2562
2563 let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
2564
2565 // Use pre-parsed fields_with_rules from schema parsing (no runtime collection needed)
2566 // This list was collected during schema parse and contains all fields with rules
2567 for field_path in self.fields_with_rules.iter() {
2568 // Check if we should validate this path (path filtering)
2569 if let Some(filter_paths) = paths {
2570 if !filter_paths.is_empty() && !filter_paths.iter().any(|p| field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())) {
2571 continue;
2572 }
2573 }
2574
2575 self.validate_field(field_path, &data_value, &mut errors);
2576 }
2577
2578 let has_error = !errors.is_empty();
2579
2580 Ok(ValidationResult {
2581 has_error,
2582 errors,
2583 })
2584 }
2585
2586 /// Validate a single field that has rules
2587 fn validate_field(
2588 &self,
2589 field_path: &str,
2590 data: &Value,
2591 errors: &mut IndexMap<String, ValidationError>
2592 ) {
2593 // Skip if already has error
2594 if errors.contains_key(field_path) {
2595 return;
2596 }
2597
2598 // Get schema for this field
2599 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
2600
2601 // Remove leading "#" from path for pointer lookup
2602 let pointer_path = schema_path.trim_start_matches('#');
2603
2604 // Try to get schema, if not found, try with /properties/ prefix for standard JSON Schema
2605 let field_schema = match self.evaluated_schema.pointer(pointer_path) {
2606 Some(s) => s,
2607 None => {
2608 // Try with /properties/ prefix (for standard JSON Schema format)
2609 let alt_path = format!("/properties{}", pointer_path);
2610 match self.evaluated_schema.pointer(&alt_path) {
2611 Some(s) => s,
2612 None => return,
2613 }
2614 }
2615 };
2616
2617 // Check if field is hidden (skip validation)
2618 if let Value::Object(schema_map) = field_schema {
2619 if let Some(Value::Object(condition)) = schema_map.get("condition") {
2620 if let Some(Value::Bool(true)) = condition.get("hidden") {
2621 return;
2622 }
2623 }
2624
2625 // Get rules object
2626 let rules = match schema_map.get("rules") {
2627 Some(Value::Object(r)) => r,
2628 _ => return,
2629 };
2630
2631 // Get field data
2632 let field_data = self.get_field_data(field_path, data);
2633
2634 // Validate each rule
2635 for (rule_name, rule_value) in rules {
2636 self.validate_rule(
2637 field_path,
2638 rule_name,
2639 rule_value,
2640 &field_data,
2641 schema_map,
2642 field_schema,
2643 errors
2644 );
2645 }
2646 }
2647 }
2648
2649 /// Get data value for a field path
2650 fn get_field_data(&self, field_path: &str, data: &Value) -> Value {
2651 let parts: Vec<&str> = field_path.split('.').collect();
2652 let mut current = data;
2653
2654 for part in parts {
2655 match current {
2656 Value::Object(map) => {
2657 current = map.get(part).unwrap_or(&Value::Null);
2658 }
2659 _ => return Value::Null,
2660 }
2661 }
2662
2663 current.clone()
2664 }
2665
2666 /// Validate a single rule
2667 fn validate_rule(
2668 &self,
2669 field_path: &str,
2670 rule_name: &str,
2671 rule_value: &Value,
2672 field_data: &Value,
2673 schema_map: &serde_json::Map<String, Value>,
2674 _schema: &Value,
2675 errors: &mut IndexMap<String, ValidationError>
2676 ) {
2677 // Skip if already has error
2678 if errors.contains_key(field_path) {
2679 return;
2680 }
2681
2682 let mut disabled_field = false;
2683 // Check if disabled
2684 if let Some(Value::Object(condition)) = schema_map.get("condition") {
2685 if let Some(Value::Bool(true)) = condition.get("disabled") {
2686 disabled_field = true;
2687 }
2688 }
2689
2690 // Get the evaluated rule from evaluated_schema (which has $evaluation already processed)
2691 // Convert field_path to schema path
2692 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
2693 let rule_path = format!("{}/rules/{}", schema_path.trim_start_matches('#'), rule_name);
2694
2695 // Look up the evaluated rule from evaluated_schema
2696 let evaluated_rule = if let Some(eval_rule) = self.evaluated_schema.pointer(&rule_path) {
2697 eval_rule.clone()
2698 } else {
2699 rule_value.clone()
2700 };
2701
2702 // Extract rule object (after evaluation)
2703 let (rule_active, rule_message, rule_code, rule_data) = match &evaluated_rule {
2704 Value::Object(rule_obj) => {
2705 let active = rule_obj.get("value").unwrap_or(&Value::Bool(false));
2706
2707 // Handle message - could be string or object with "value"
2708 let message = match rule_obj.get("message") {
2709 Some(Value::String(s)) => s.clone(),
2710 Some(Value::Object(msg_obj)) if msg_obj.contains_key("value") => {
2711 msg_obj.get("value")
2712 .and_then(|v| v.as_str())
2713 .unwrap_or("Validation failed")
2714 .to_string()
2715 }
2716 Some(msg_val) => msg_val.as_str().unwrap_or("Validation failed").to_string(),
2717 None => "Validation failed".to_string()
2718 };
2719
2720 let code = rule_obj.get("code")
2721 .and_then(|c| c.as_str())
2722 .map(|s| s.to_string());
2723
2724 // Handle data - extract "value" from objects with $evaluation
2725 let data = rule_obj.get("data").map(|d| {
2726 if let Value::Object(data_obj) = d {
2727 let mut cleaned_data = serde_json::Map::new();
2728 for (key, value) in data_obj {
2729 // If value is an object with only "value" key, extract it
2730 if let Value::Object(val_obj) = value {
2731 if val_obj.len() == 1 && val_obj.contains_key("value") {
2732 cleaned_data.insert(key.clone(), val_obj["value"].clone());
2733 } else {
2734 cleaned_data.insert(key.clone(), value.clone());
2735 }
2736 } else {
2737 cleaned_data.insert(key.clone(), value.clone());
2738 }
2739 }
2740 Value::Object(cleaned_data)
2741 } else {
2742 d.clone()
2743 }
2744 });
2745
2746 (active.clone(), message, code, data)
2747 }
2748 _ => (evaluated_rule.clone(), "Validation failed".to_string(), None, None)
2749 };
2750
2751 // Generate default code if not provided
2752 let error_code = rule_code.or_else(|| Some(format!("{}.{}", field_path, rule_name)));
2753
2754 let is_empty = matches!(field_data, Value::Null) ||
2755 (field_data.is_string() && field_data.as_str().unwrap_or("").is_empty()) ||
2756 (field_data.is_array() && field_data.as_array().unwrap().is_empty());
2757
2758 match rule_name {
2759 "required" => {
2760 if !disabled_field && rule_active == Value::Bool(true) {
2761 if is_empty {
2762 errors.insert(field_path.to_string(), ValidationError {
2763 rule_type: "required".to_string(),
2764 message: rule_message,
2765 code: error_code.clone(),
2766 pattern: None,
2767 field_value: None,
2768 data: None,
2769 });
2770 }
2771 }
2772 }
2773 "minLength" => {
2774 if !is_empty {
2775 if let Some(min) = rule_active.as_u64() {
2776 let len = match field_data {
2777 Value::String(s) => s.len(),
2778 Value::Array(a) => a.len(),
2779 _ => 0
2780 };
2781 if len < min as usize {
2782 errors.insert(field_path.to_string(), ValidationError {
2783 rule_type: "minLength".to_string(),
2784 message: rule_message,
2785 code: error_code.clone(),
2786 pattern: None,
2787 field_value: None,
2788 data: None,
2789 });
2790 }
2791 }
2792 }
2793 }
2794 "maxLength" => {
2795 if !is_empty {
2796 if let Some(max) = rule_active.as_u64() {
2797 let len = match field_data {
2798 Value::String(s) => s.len(),
2799 Value::Array(a) => a.len(),
2800 _ => 0
2801 };
2802 if len > max as usize {
2803 errors.insert(field_path.to_string(), ValidationError {
2804 rule_type: "maxLength".to_string(),
2805 message: rule_message,
2806 code: error_code.clone(),
2807 pattern: None,
2808 field_value: None,
2809 data: None,
2810 });
2811 }
2812 }
2813 }
2814 }
2815 "minValue" => {
2816 if !is_empty {
2817 if let Some(min) = rule_active.as_f64() {
2818 if let Some(val) = field_data.as_f64() {
2819 if val < min {
2820 errors.insert(field_path.to_string(), ValidationError {
2821 rule_type: "minValue".to_string(),
2822 message: rule_message,
2823 code: error_code.clone(),
2824 pattern: None,
2825 field_value: None,
2826 data: None,
2827 });
2828 }
2829 }
2830 }
2831 }
2832 }
2833 "maxValue" => {
2834 if !is_empty {
2835 if let Some(max) = rule_active.as_f64() {
2836 if let Some(val) = field_data.as_f64() {
2837 if val > max {
2838 errors.insert(field_path.to_string(), ValidationError {
2839 rule_type: "maxValue".to_string(),
2840 message: rule_message,
2841 code: error_code.clone(),
2842 pattern: None,
2843 field_value: None,
2844 data: None,
2845 });
2846 }
2847 }
2848 }
2849 }
2850 }
2851 "pattern" => {
2852 if !is_empty {
2853 if let Some(pattern) = rule_active.as_str() {
2854 if let Some(text) = field_data.as_str() {
2855 if let Ok(regex) = regex::Regex::new(pattern) {
2856 if !regex.is_match(text) {
2857 errors.insert(field_path.to_string(), ValidationError {
2858 rule_type: "pattern".to_string(),
2859 message: rule_message,
2860 code: error_code.clone(),
2861 pattern: Some(pattern.to_string()),
2862 field_value: Some(text.to_string()),
2863 data: None,
2864 });
2865 }
2866 }
2867 }
2868 }
2869 }
2870 }
2871 "evaluation" => {
2872 // Handle array of evaluation rules
2873 // Format: "evaluation": [{ "code": "...", "message": "...", "$evaluation": {...} }]
2874 if let Value::Array(eval_array) = &evaluated_rule {
2875 for (idx, eval_item) in eval_array.iter().enumerate() {
2876 if let Value::Object(eval_obj) = eval_item {
2877 // Get the evaluated value (should be in "value" key after evaluation)
2878 let eval_result = eval_obj.get("value").unwrap_or(&Value::Bool(true));
2879
2880 // Check if result is falsy
2881 let is_falsy = match eval_result {
2882 Value::Bool(false) => true,
2883 Value::Null => true,
2884 Value::Number(n) => n.as_f64() == Some(0.0),
2885 Value::String(s) => s.is_empty(),
2886 Value::Array(a) => a.is_empty(),
2887 _ => false,
2888 };
2889
2890 if is_falsy {
2891 let eval_code = eval_obj.get("code")
2892 .and_then(|c| c.as_str())
2893 .map(|s| s.to_string())
2894 .or_else(|| Some(format!("{}.evaluation.{}", field_path, idx)));
2895
2896 let eval_message = eval_obj.get("message")
2897 .and_then(|m| m.as_str())
2898 .unwrap_or("Validation failed")
2899 .to_string();
2900
2901 let eval_data = eval_obj.get("data").cloned();
2902
2903 errors.insert(field_path.to_string(), ValidationError {
2904 rule_type: "evaluation".to_string(),
2905 message: eval_message,
2906 code: eval_code,
2907 pattern: None,
2908 field_value: None,
2909 data: eval_data,
2910 });
2911
2912 // Stop at first failure
2913 break;
2914 }
2915 }
2916 }
2917 }
2918 }
2919 _ => {
2920 // Custom evaluation rules
2921 // In JS: if (!opt.rule.value) then error
2922 // This handles rules with $evaluation that return false/falsy values
2923 if !is_empty {
2924 // Check if rule_active is falsy (false, 0, null, empty string, empty array)
2925 let is_falsy = match &rule_active {
2926 Value::Bool(false) => true,
2927 Value::Null => true,
2928 Value::Number(n) => n.as_f64() == Some(0.0),
2929 Value::String(s) => s.is_empty(),
2930 Value::Array(a) => a.is_empty(),
2931 _ => false,
2932 };
2933
2934 if is_falsy {
2935 errors.insert(field_path.to_string(), ValidationError {
2936 rule_type: "evaluation".to_string(),
2937 message: rule_message,
2938 code: error_code.clone(),
2939 pattern: None,
2940 field_value: None,
2941 data: rule_data,
2942 });
2943 }
2944 }
2945 }
2946 }
2947 }
2948}
2949
2950/// Validation error for a field
2951#[derive(Debug, Clone, Serialize, Deserialize)]
2952pub struct ValidationError {
2953 #[serde(rename = "type")]
2954 pub rule_type: String,
2955 pub message: String,
2956 #[serde(skip_serializing_if = "Option::is_none")]
2957 pub code: Option<String>,
2958 #[serde(skip_serializing_if = "Option::is_none")]
2959 pub pattern: Option<String>,
2960 #[serde(skip_serializing_if = "Option::is_none")]
2961 pub field_value: Option<String>,
2962 #[serde(skip_serializing_if = "Option::is_none")]
2963 pub data: Option<Value>,
2964}
2965
2966/// Result of validation
2967#[derive(Debug, Clone, Serialize, Deserialize)]
2968pub struct ValidationResult {
2969 pub has_error: bool,
2970 pub errors: IndexMap<String, ValidationError>,
2971}
2972