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