json_eval_rs/
lib.rs

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