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    /// Reload schema from MessagePack-encoded bytes
520    /// 
521    /// # Arguments
522    /// 
523    /// * `schema_msgpack` - MessagePack-encoded schema bytes
524    /// * `context` - Optional context data JSON string
525    /// * `data` - Optional initial data JSON string
526    /// 
527    /// # Returns
528    /// 
529    /// A `Result` indicating success or an error message
530    pub fn reload_schema_msgpack(
531        &mut self,
532        schema_msgpack: &[u8],
533        context: Option<&str>,
534        data: Option<&str>,
535    ) -> Result<(), String> {
536        // Deserialize MessagePack to Value
537        let schema_val: Value = rmp_serde::from_slice(schema_msgpack)
538            .map_err(|e| format!("failed to deserialize MessagePack schema: {e}"))?;
539        
540        let context: Value = json_parser::parse_json_str(context.unwrap_or("{}"))?;
541        let data: Value = json_parser::parse_json_str(data.unwrap_or("{}"))?;
542        
543        self.schema = Arc::new(schema_val);
544        self.context = context.clone();
545        self.data = data.clone();
546        self.evaluated_schema = (*self.schema).clone();
547        self.engine = Arc::new(RLogic::new());
548        self.dependents_evaluations = Arc::new(IndexMap::new());
549        self.rules_evaluations = Arc::new(Vec::new());
550        self.fields_with_rules = Arc::new(Vec::new());
551        self.others_evaluations = Arc::new(Vec::new());
552        self.value_evaluations = Arc::new(Vec::new());
553        self.layout_paths = Arc::new(Vec::new());
554        self.options_templates = Arc::new(Vec::new());
555        self.subforms.clear();
556        parse_schema::legacy::parse_schema(self)?;
557        
558        // Re-initialize eval_data
559        self.eval_data = EvalData::with_schema_data_context(&self.evaluated_schema, &data, &context);
560        
561        // Clear cache when schema changes
562        self.eval_cache.clear();
563        
564        // Cache the MessagePack for future retrievals
565        self.cached_msgpack_schema = Some(schema_msgpack.to_vec());
566
567        Ok(())
568    }
569
570    /// Reload schema from a cached ParsedSchema
571    /// 
572    /// This is the most efficient way to reload as it reuses pre-parsed schema compilation.
573    /// 
574    /// # Arguments
575    /// 
576    /// * `parsed` - Arc reference to a cached ParsedSchema
577    /// * `context` - Optional context data JSON string
578    /// * `data` - Optional initial data JSON string
579    /// 
580    /// # Returns
581    /// 
582    /// A `Result` indicating success or an error message
583    pub fn reload_schema_parsed(
584        &mut self,
585        parsed: Arc<ParsedSchema>,
586        context: Option<&str>,
587        data: Option<&str>,
588    ) -> Result<(), String> {
589        let context: Value = json_parser::parse_json_str(context.unwrap_or("{}"))?;
590        let data: Value = json_parser::parse_json_str(data.unwrap_or("{}"))?;
591        
592        // Share all the pre-compiled data from ParsedSchema
593        self.schema = Arc::clone(&parsed.schema);
594        self.evaluations = parsed.evaluations.clone();
595        self.tables = parsed.tables.clone();
596        self.table_metadata = parsed.table_metadata.clone();
597        self.dependencies = parsed.dependencies.clone();
598        self.sorted_evaluations = parsed.sorted_evaluations.clone();
599        self.dependents_evaluations = parsed.dependents_evaluations.clone();
600        self.rules_evaluations = parsed.rules_evaluations.clone();
601        self.fields_with_rules = parsed.fields_with_rules.clone();
602        self.others_evaluations = parsed.others_evaluations.clone();
603        self.value_evaluations = parsed.value_evaluations.clone();
604        self.layout_paths = parsed.layout_paths.clone();
605        self.options_templates = parsed.options_templates.clone();
606        
607        // Share the engine Arc (cheap pointer clone, not data clone)
608        self.engine = parsed.engine.clone();
609        
610        // Convert Arc<ParsedSchema> subforms to Box<JSONEval> subforms
611        let mut subforms = IndexMap::new();
612        for (path, subform_parsed) in &parsed.subforms {
613            let subform_eval = JSONEval::with_parsed_schema(
614                subform_parsed.clone(),
615                Some("{}"),
616                None
617            )?;
618            subforms.insert(path.clone(), Box::new(subform_eval));
619        }
620        self.subforms = subforms;
621        
622        self.context = context.clone();
623        self.data = data.clone();
624        self.evaluated_schema = (*self.schema).clone();
625        
626        // Re-initialize eval_data
627        self.eval_data = EvalData::with_schema_data_context(&self.evaluated_schema, &data, &context);
628        
629        // Clear cache when schema changes
630        self.eval_cache.clear();
631        
632        // Clear MessagePack cache since we're loading from ParsedSchema
633        self.cached_msgpack_schema = None;
634
635        Ok(())
636    }
637
638    /// Reload schema from ParsedSchemaCache using a cache key
639    /// 
640    /// This is the recommended way for cross-platform cached schema reloading.
641    /// 
642    /// # Arguments
643    /// 
644    /// * `cache_key` - Key to lookup in the global ParsedSchemaCache
645    /// * `context` - Optional context data JSON string
646    /// * `data` - Optional initial data JSON string
647    /// 
648    /// # Returns
649    /// 
650    /// A `Result` indicating success or an error message
651    pub fn reload_schema_from_cache(
652        &mut self,
653        cache_key: &str,
654        context: Option<&str>,
655        data: Option<&str>,
656    ) -> Result<(), String> {
657        // Get the cached ParsedSchema from global cache
658        let parsed = PARSED_SCHEMA_CACHE.get(cache_key)
659            .ok_or_else(|| format!("Schema '{}' not found in cache", cache_key))?;
660        
661        // Use reload_schema_parsed with the cached schema
662        self.reload_schema_parsed(parsed, context, data)
663    }
664
665    /// Evaluate the schema with the given data and context.
666    ///
667    /// # Arguments
668    ///
669    /// * `data` - The data to evaluate.
670    /// * `context` - The context to evaluate.
671    ///
672    /// # Returns
673    ///
674    /// A `Result` indicating success or an error message.
675    pub fn evaluate(&mut self, data: &str, context: Option<&str>) -> Result<(), String> {
676        time_block!("evaluate() [total]", {
677            // Use SIMD-accelerated JSON parsing
678            let data: Value = time_block!("  parse data", {
679                json_parser::parse_json_str(data)?
680            });
681            let context: Value = time_block!("  parse context", {
682                json_parser::parse_json_str(context.unwrap_or("{}"))?
683            });
684                
685            self.data = data.clone();
686            
687            // Collect top-level data keys to selectively purge cache
688            let changed_data_paths: Vec<String> = if let Some(obj) = data.as_object() {
689                obj.keys().map(|k| k.clone()).collect()
690            } else {
691                Vec::new()
692            };
693            
694            // Replace data and context in existing eval_data
695            time_block!("  replace_data_and_context", {
696                self.eval_data.replace_data_and_context(data, context);
697            });
698            
699            // Selectively purge cache entries that depend on changed top-level data keys
700            // This is more efficient than clearing entire cache
701            time_block!("  purge_cache", {
702                self.purge_cache_for_changed_data(&changed_data_paths);
703            });
704            
705            // Call internal evaluate (uses existing data if not provided)
706            self.evaluate_internal()
707        })
708    }
709    
710    /// Internal evaluate that can be called when data is already set
711    /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
712    fn evaluate_internal(&mut self) -> Result<(), String> {
713        time_block!("  evaluate_internal() [total]", {
714            // Acquire lock for synchronous execution
715            let _lock = self.eval_lock.lock().unwrap();
716
717            // Clone sorted_evaluations (Arc clone is cheap, then clone inner Vec)
718            let eval_batches: Vec<Vec<String>> = (*self.sorted_evaluations).clone();
719
720            // Process each batch - parallelize evaluations within each batch
721            // Batches are processed sequentially to maintain dependency order
722            time_block!("    process batches", {
723                for batch in eval_batches {
724            // Skip empty batches
725            if batch.is_empty() {
726                continue;
727            }
728            
729            // No pre-checking cache - we'll check inside parallel execution
730            // This allows thread-safe cache access during parallel evaluation
731            
732            // Parallel execution within batch (no dependencies between items)
733            // Use Mutex for thread-safe result collection
734            // Store both eval_key and result for cache storage
735            let eval_data_snapshot = self.eval_data.clone();
736            
737            // Parallelize only if batch has multiple items (overhead not worth it for single item)
738            #[cfg(feature = "parallel")]
739            if batch.len() > 1000 {
740                let results: Mutex<Vec<(String, String, Value)>> = Mutex::new(Vec::with_capacity(batch.len()));
741                batch.par_iter().for_each(|eval_key| {
742                    let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
743                    
744                    // Try cache first (thread-safe)
745                    if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
746                        return;
747                    }
748                    
749                    // Cache miss - evaluate
750                    let is_table = self.table_metadata.contains_key(eval_key);
751                    
752                    if is_table {
753                        // Evaluate table using sandboxed metadata (parallel-safe, immutable parent scope)
754                        if let Ok(rows) = table_evaluate::evaluate_table(self, eval_key, &eval_data_snapshot) {
755                            let value = Value::Array(rows);
756                            // Cache result (thread-safe)
757                            self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
758                            results.lock().unwrap().push((eval_key.clone(), pointer_path, value));
759                        }
760                    } else {
761                        if let Some(logic_id) = self.evaluations.get(eval_key) {
762                            // Evaluate directly with snapshot
763                            if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
764                                let cleaned_val = clean_float_noise(val);
765                                // Cache result (thread-safe)
766                                self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
767                                results.lock().unwrap().push((eval_key.clone(), pointer_path, cleaned_val));
768                            }
769                        }
770                    }
771                });
772                
773                // Write all results back sequentially (already cached in parallel execution)
774                for (_eval_key, path, value) in results.into_inner().unwrap() {
775                    let cleaned_value = clean_float_noise(value);
776                    
777                    self.eval_data.set(&path, cleaned_value.clone());
778                    // Also write to evaluated_schema
779                    if let Some(schema_value) = self.evaluated_schema.pointer_mut(&path) {
780                        *schema_value = cleaned_value;
781                    }
782                }
783                continue;
784            }
785            
786            // Sequential execution (single item or parallel feature disabled)
787            #[cfg(not(feature = "parallel"))]
788            let batch_items = &batch;
789            
790            #[cfg(feature = "parallel")]
791            let batch_items = if batch.len() > 1000 { &batch[0..0] } else { &batch }; // Empty slice if already processed in parallel
792            
793            for eval_key in batch_items {
794                let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
795                
796                // Try cache first
797                if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
798                    continue;
799                }
800                
801                // Cache miss - evaluate
802                let is_table = self.table_metadata.contains_key(eval_key);
803                
804                if is_table {
805                    if let Ok(rows) = table_evaluate::evaluate_table(self, eval_key, &eval_data_snapshot) {
806                        let value = Value::Array(rows);
807                        // Cache result
808                        self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
809                        
810                        let cleaned_value = clean_float_noise(value);
811                        self.eval_data.set(&pointer_path, cleaned_value.clone());
812                        if let Some(schema_value) = self.evaluated_schema.pointer_mut(&pointer_path) {
813                            *schema_value = cleaned_value;
814                        }
815                    }
816                } else {
817                    if let Some(logic_id) = self.evaluations.get(eval_key) {
818                        if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
819                            let cleaned_val = clean_float_noise(val);
820                            // Cache result
821                            self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
822                            
823                            self.eval_data.set(&pointer_path, cleaned_val.clone());
824                            if let Some(schema_value) = self.evaluated_schema.pointer_mut(&pointer_path) {
825                                *schema_value = cleaned_val;
826                            }
827                        }
828                    }
829                }
830            }
831                }
832            });
833
834            // Drop lock before calling evaluate_others
835            drop(_lock);
836            
837            self.evaluate_others();
838
839            Ok(())
840        })
841    }
842
843    /// Get the evaluated schema with optional layout resolution.
844    ///
845    /// # Arguments
846    ///
847    /// * `skip_layout` - Whether to skip layout resolution.
848    ///
849    /// # Returns
850    ///
851    /// The evaluated schema as a JSON value.
852    pub fn get_evaluated_schema(&mut self, skip_layout: bool) -> Value {
853        time_block!("get_evaluated_schema()", {
854            if !skip_layout {
855                self.resolve_layout_internal();
856            }
857            
858            self.evaluated_schema.clone()
859        })
860    }
861
862    /// Get the evaluated schema as MessagePack binary format
863    ///
864    /// # Arguments
865    ///
866    /// * `skip_layout` - Whether to skip layout resolution.
867    ///
868    /// # Returns
869    ///
870    /// The evaluated schema serialized as MessagePack bytes
871    ///
872    /// # Zero-Copy Optimization
873    ///
874    /// This method serializes the evaluated schema to MessagePack. The resulting Vec<u8>
875    /// is then passed to FFI/WASM boundaries via raw pointers (zero-copy at boundary).
876    /// The serialization step itself (Value -> MessagePack) cannot be avoided but is
877    /// highly optimized by rmp-serde.
878    pub fn get_evaluated_schema_msgpack(&mut self, skip_layout: bool) -> Result<Vec<u8>, String> {
879        if !skip_layout {
880            self.resolve_layout_internal();
881        }
882        
883        // Serialize evaluated schema to MessagePack
884        // Note: This is the only copy required. The FFI layer then returns raw pointers
885        // to this data for zero-copy transfer to calling code.
886        rmp_serde::to_vec(&self.evaluated_schema)
887            .map_err(|e| format!("Failed to serialize schema to MessagePack: {}", e))
888    }
889
890    /// Get all schema values (evaluations ending with .value)
891    /// Mutates self.data by overriding with values from value evaluations
892    /// Returns the modified data
893    pub fn get_schema_value(&mut self) -> Value {
894        // Ensure self.data is an object
895        if !self.data.is_object() {
896            self.data = Value::Object(serde_json::Map::new());
897        }
898        
899        // Override self.data with values from value evaluations
900        for eval_key in self.value_evaluations.iter() {
901            let clean_key = eval_key.replace("#", "");
902            let path = clean_key.replace("/properties", "").replace("/value", "");
903            
904            // Get the value from evaluated_schema
905            let value = match self.evaluated_schema.pointer(&clean_key) {
906                Some(v) => v.clone(),
907                None => continue,
908            };
909            
910            // Parse the path and create nested structure as needed
911            let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
912            
913            if path_parts.is_empty() {
914                continue;
915            }
916            
917            // Navigate/create nested structure
918            let mut current = &mut self.data;
919            for (i, part) in path_parts.iter().enumerate() {
920                let is_last = i == path_parts.len() - 1;
921                
922                if is_last {
923                    // Set the value at the final key
924                    if let Some(obj) = current.as_object_mut() {
925                        obj.insert(part.to_string(), clean_float_noise(value.clone()));
926                    }
927                } else {
928                    // Ensure current is an object, then navigate/create intermediate objects
929                    if let Some(obj) = current.as_object_mut() {
930                        current = obj.entry(part.to_string())
931                            .or_insert_with(|| Value::Object(serde_json::Map::new()));
932                    } else {
933                        // Skip this path if current is not an object and can't be made into one
934                        break;
935                    }
936                }
937            }
938        }
939        
940        clean_float_noise(self.data.clone())
941    }
942
943    /// Get the evaluated schema without $params field.
944    /// This method filters out $params from the root level only.
945    ///
946    /// # Arguments
947    ///
948    /// * `skip_layout` - Whether to skip layout resolution.
949    ///
950    /// # Returns
951    ///
952    /// The evaluated schema with $params removed.
953    pub fn get_evaluated_schema_without_params(&mut self, skip_layout: bool) -> Value {
954        if !skip_layout {
955            self.resolve_layout_internal();
956        }
957        
958        // Filter $params at root level only
959        if let Value::Object(mut map) = self.evaluated_schema.clone() {
960            map.remove("$params");
961            Value::Object(map)
962        } else {
963            self.evaluated_schema.clone()
964        }
965    }
966
967    /// Get a value from the evaluated schema using dotted path notation.
968    /// Converts dotted notation (e.g., "properties.field.value") to JSON pointer format.
969    ///
970    /// # Arguments
971    ///
972    /// * `path` - The dotted path to the value (e.g., "properties.field.value")
973    /// * `skip_layout` - Whether to skip layout resolution.
974    ///
975    /// # Returns
976    ///
977    /// The value at the specified path, or None if not found.
978    pub fn get_evaluated_schema_by_path(&mut self, path: &str, skip_layout: bool) -> Option<Value> {
979        if !skip_layout {
980            self.resolve_layout_internal();
981        }
982        
983        // Convert dotted notation to JSON pointer
984        let pointer = if path.is_empty() {
985            "".to_string()
986        } else {
987            format!("/{}", path.replace(".", "/"))
988        };
989        
990        self.evaluated_schema.pointer(&pointer).cloned()
991    }
992
993    /// Get values from the evaluated schema using multiple dotted path notations.
994    /// Returns data in the specified format. Skips paths that are not found.
995    ///
996    /// # Arguments
997    ///
998    /// * `paths` - Array of dotted paths to retrieve (e.g., ["properties.field1", "properties.field2"])
999    /// * `skip_layout` - Whether to skip layout resolution.
1000    /// * `format` - Optional return format (Nested, Flat, or Array). Defaults to Nested.
1001    ///
1002    /// # Returns
1003    ///
1004    /// Data in the specified format, or an empty object/array if no paths are found.
1005    pub fn get_evaluated_schema_by_paths(&mut self, paths: &[String], skip_layout: bool, format: Option<ReturnFormat>) -> Value {
1006        let format = format.unwrap_or_default();
1007        if !skip_layout {
1008            self.resolve_layout_internal();
1009        }
1010        
1011        let mut result = serde_json::Map::new();
1012        
1013        for path in paths {
1014            // Convert dotted notation to JSON pointer
1015            let pointer = if path.is_empty() {
1016                "".to_string()
1017            } else {
1018                format!("/{}", path.replace(".", "/"))
1019            };
1020            
1021            // Get value at path, skip if not found
1022            if let Some(value) = self.evaluated_schema.pointer(&pointer) {
1023                // Store the full path structure to maintain the hierarchy
1024                // Clone only once per path
1025                self.insert_at_path(&mut result, path, value.clone());
1026            }
1027        }
1028        
1029        self.convert_to_format(result, paths, format)
1030    }
1031    
1032    /// Helper function to insert a value at a dotted path in a JSON object
1033    fn insert_at_path(&self, obj: &mut serde_json::Map<String, Value>, path: &str, value: Value) {
1034        if path.is_empty() {
1035            // If path is empty, merge the value into the root
1036            if let Value::Object(map) = value {
1037                for (k, v) in map {
1038                    obj.insert(k, v);
1039                }
1040            }
1041            return;
1042        }
1043        
1044        let parts: Vec<&str> = path.split('.').collect();
1045        if parts.is_empty() {
1046            return;
1047        }
1048        
1049        let mut current = obj;
1050        let last_index = parts.len() - 1;
1051        
1052        for (i, part) in parts.iter().enumerate() {
1053            if i == last_index {
1054                // Last part - insert the value
1055                current.insert(part.to_string(), value);
1056                break;
1057            } else {
1058                // Intermediate part - ensure object exists
1059                current = current
1060                    .entry(part.to_string())
1061                    .or_insert_with(|| Value::Object(serde_json::Map::new()))
1062                    .as_object_mut()
1063                    .unwrap();
1064            }
1065        }
1066    }
1067    
1068    /// Convert result map to the requested format
1069    fn convert_to_format(&self, result: serde_json::Map<String, Value>, paths: &[String], format: ReturnFormat) -> Value {
1070        match format {
1071            ReturnFormat::Nested => Value::Object(result),
1072            ReturnFormat::Flat => {
1073                // Flatten nested object to dotted keys
1074                let mut flat = serde_json::Map::new();
1075                self.flatten_object(&result, String::new(), &mut flat);
1076                Value::Object(flat)
1077            }
1078            ReturnFormat::Array => {
1079                // Return array of values in order of requested paths
1080                let values: Vec<Value> = paths.iter()
1081                    .map(|path| {
1082                        let pointer = if path.is_empty() {
1083                            "".to_string()
1084                        } else {
1085                            format!("/{}", path.replace(".", "/"))
1086                        };
1087                        Value::Object(result.clone()).pointer(&pointer).cloned().unwrap_or(Value::Null)
1088                    })
1089                    .collect();
1090                Value::Array(values)
1091            }
1092        }
1093    }
1094    
1095    /// Recursively flatten a nested object into dotted keys
1096    fn flatten_object(&self, obj: &serde_json::Map<String, Value>, prefix: String, result: &mut serde_json::Map<String, Value>) {
1097        for (key, value) in obj {
1098            let new_key = if prefix.is_empty() {
1099                key.clone()
1100            } else {
1101                format!("{}.{}", prefix, key)
1102            };
1103            
1104            if let Value::Object(nested) = value {
1105                self.flatten_object(nested, new_key, result);
1106            } else {
1107                result.insert(new_key, value.clone());
1108            }
1109        }
1110    }
1111
1112    /// Get a value from the schema using dotted path notation.
1113    /// Converts dotted notation (e.g., "properties.field.value") to JSON pointer format.
1114    ///
1115    /// # Arguments
1116    ///
1117    /// * `path` - The dotted path to the value (e.g., "properties.field.value")
1118    ///
1119    /// # Returns
1120    ///
1121    /// The value at the specified path, or None if not found.
1122    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
1123        // Convert dotted notation to JSON pointer
1124        let pointer = if path.is_empty() {
1125            "".to_string()
1126        } else {
1127            format!("/{}", path.replace(".", "/"))
1128        };
1129        
1130        self.schema.pointer(&pointer).cloned()
1131    }
1132
1133    /// Get values from the schema using multiple dotted path notations.
1134    /// Returns data in the specified format. Skips paths that are not found.
1135    ///
1136    /// # Arguments
1137    ///
1138    /// * `paths` - Array of dotted paths to retrieve (e.g., ["properties.field1", "properties.field2"])
1139    /// * `format` - Optional return format (Nested, Flat, or Array). Defaults to Nested.
1140    ///
1141    /// # Returns
1142    ///
1143    /// Data in the specified format, or an empty object/array if no paths are found.
1144    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
1145        let format = format.unwrap_or_default();
1146        let mut result = serde_json::Map::new();
1147        
1148        for path in paths {
1149            // Convert dotted notation to JSON pointer
1150            let pointer = if path.is_empty() {
1151                "".to_string()
1152            } else {
1153                format!("/{}", path.replace(".", "/"))
1154            };
1155            
1156            // Get value at path, skip if not found
1157            if let Some(value) = self.schema.pointer(&pointer) {
1158                // Store the full path structure to maintain the hierarchy
1159                // Clone only once per path
1160                self.insert_at_path(&mut result, path, value.clone());
1161            }
1162        }
1163        
1164        self.convert_to_format(result, paths, format)
1165    }
1166
1167    /// Check if a dependency should be cached
1168    /// Caches everything except keys starting with $ (except $context)
1169    #[inline]
1170    fn should_cache_dependency(key: &str) -> bool {
1171        if key.starts_with("/$") || key.starts_with('$') {
1172            // Only cache $context, exclude other $ keys like $params
1173            key == "$context" || key.starts_with("$context.") || key.starts_with("/$context")
1174        } else {
1175            true
1176        }
1177    }
1178
1179    /// Helper: Try to get cached result for an evaluation (thread-safe)
1180    /// Helper: Try to get cached result (zero-copy via Arc)
1181    fn try_get_cached(&self, eval_key: &str, eval_data: &EvalData) -> Option<Value> {
1182        // Skip cache lookup if caching is disabled
1183        if !self.cache_enabled {
1184            return None;
1185        }
1186        
1187        // Get dependencies for this evaluation
1188        let deps = self.dependencies.get(eval_key)?;
1189        
1190        // If no dependencies, use simple cache key
1191        let cache_key = if deps.is_empty() {
1192            CacheKey::simple(eval_key.to_string())
1193        } else {
1194            // Filter dependencies (exclude $ keys except $context)
1195            let filtered_deps: IndexSet<String> = deps
1196                .iter()
1197                .filter(|dep_key| JSONEval::should_cache_dependency(dep_key))
1198                .cloned()
1199                .collect();
1200            
1201            // Collect dependency values
1202            let dep_values: Vec<(String, &Value)> = filtered_deps
1203                .iter()
1204                .filter_map(|dep_key| {
1205                    eval_data.get(dep_key).map(|v| (dep_key.clone(), v))
1206                })
1207                .collect();
1208            
1209            CacheKey::new(eval_key.to_string(), &filtered_deps, &dep_values)
1210        };
1211        
1212        // Try cache lookup (zero-copy via Arc, thread-safe)
1213        self.eval_cache.get(&cache_key).map(|arc_val| (*arc_val).clone())
1214    }
1215    
1216    /// Helper: Store evaluation result in cache (thread-safe)
1217    fn cache_result(&self, eval_key: &str, value: Value, eval_data: &EvalData) {
1218        // Skip cache insertion if caching is disabled
1219        if !self.cache_enabled {
1220            return;
1221        }
1222        
1223        // Get dependencies for this evaluation
1224        let deps = match self.dependencies.get(eval_key) {
1225            Some(d) => d,
1226            None => {
1227                // No dependencies - use simple cache key
1228                let cache_key = CacheKey::simple(eval_key.to_string());
1229                self.eval_cache.insert(cache_key, value);
1230                return;
1231            }
1232        };
1233        
1234        // Filter and collect dependency values (exclude $ keys except $context)
1235        let filtered_deps: IndexSet<String> = deps
1236            .iter()
1237            .filter(|dep_key| JSONEval::should_cache_dependency(dep_key))
1238            .cloned()
1239            .collect();
1240        
1241        let dep_values: Vec<(String, &Value)> = filtered_deps
1242            .iter()
1243            .filter_map(|dep_key| {
1244                eval_data.get(dep_key).map(|v| (dep_key.clone(), v))
1245            })
1246            .collect();
1247        
1248        let cache_key = CacheKey::new(eval_key.to_string(), &filtered_deps, &dep_values);
1249        self.eval_cache.insert(cache_key, value);
1250    }
1251
1252    /// Selectively purge cache entries that depend on changed data paths
1253    /// Only removes cache entries whose dependencies intersect with changed_paths
1254    /// Compares old vs new values and only purges if values actually changed
1255    fn purge_cache_for_changed_data_with_comparison(
1256        &self, 
1257        changed_data_paths: &[String],
1258        old_data: &Value,
1259        new_data: &Value
1260    ) {
1261        if changed_data_paths.is_empty() {
1262            return;
1263        }
1264        
1265        // Check which paths actually have different values
1266        let mut actually_changed_paths = Vec::new();
1267        for path in changed_data_paths {
1268            let old_val = old_data.pointer(path);
1269            let new_val = new_data.pointer(path);
1270            
1271            // Only add to changed list if values differ
1272            if old_val != new_val {
1273                actually_changed_paths.push(path.clone());
1274            }
1275        }
1276        
1277        // If no values actually changed, no need to purge
1278        if actually_changed_paths.is_empty() {
1279            return;
1280        }
1281        
1282        // Find all eval_keys that depend on the actually changed data paths
1283        let mut affected_eval_keys = IndexSet::new();
1284        
1285        for (eval_key, deps) in self.dependencies.iter() {
1286            // Check if this evaluation depends on any of the changed paths
1287            let is_affected = deps.iter().any(|dep| {
1288                // Check if the dependency matches any changed path
1289                actually_changed_paths.iter().any(|changed_path| {
1290                    // Exact match or prefix match (for nested fields)
1291                    dep == changed_path || 
1292                    dep.starts_with(&format!("{}/", changed_path)) ||
1293                    changed_path.starts_with(&format!("{}/", dep))
1294                })
1295            });
1296            
1297            if is_affected {
1298                affected_eval_keys.insert(eval_key.clone());
1299            }
1300        }
1301        
1302        // Remove all cache entries for affected eval_keys using retain
1303        // Keep entries whose eval_key is NOT in the affected set
1304        self.eval_cache.retain(|cache_key, _| {
1305            !affected_eval_keys.contains(&cache_key.eval_key)
1306        });
1307    }
1308    
1309    /// Selectively purge cache entries that depend on changed data paths
1310    /// Simpler version without value comparison for cases where we don't have old data
1311    fn purge_cache_for_changed_data(&self, changed_data_paths: &[String]) {
1312        if changed_data_paths.is_empty() {
1313            return;
1314        }
1315        
1316        // Find all eval_keys that depend on the changed data paths
1317        let mut affected_eval_keys = IndexSet::new();
1318        
1319        for (eval_key, deps) in self.dependencies.iter() {
1320            // Check if this evaluation depends on any of the changed paths
1321            let is_affected = deps.iter().any(|dep| {
1322                // Check if the dependency matches any changed path
1323                changed_data_paths.iter().any(|changed_path| {
1324                    // Exact match or prefix match (for nested fields)
1325                    dep == changed_path || 
1326                    dep.starts_with(&format!("{}/", changed_path)) ||
1327                    changed_path.starts_with(&format!("{}/", dep))
1328                })
1329            });
1330            
1331            if is_affected {
1332                affected_eval_keys.insert(eval_key.clone());
1333            }
1334        }
1335        
1336        // Remove all cache entries for affected eval_keys using retain
1337        // Keep entries whose eval_key is NOT in the affected set
1338        self.eval_cache.retain(|cache_key, _| {
1339            !affected_eval_keys.contains(&cache_key.eval_key)
1340        });
1341    }
1342
1343    /// Get cache statistics
1344    pub fn cache_stats(&self) -> CacheStats {
1345        self.eval_cache.stats()
1346    }
1347    
1348    /// Clear evaluation cache
1349    pub fn clear_cache(&mut self) {
1350        self.eval_cache.clear();
1351    }
1352    
1353    /// Get number of cached entries
1354    pub fn cache_len(&self) -> usize {
1355        self.eval_cache.len()
1356    }
1357    
1358    /// Enable evaluation caching
1359    /// Useful for reusing JSONEval instances with different data
1360    pub fn enable_cache(&mut self) {
1361        self.cache_enabled = true;
1362    }
1363    
1364    /// Disable evaluation caching
1365    /// Useful for web API usage where each request creates a new JSONEval instance
1366    /// Improves performance by skipping cache operations that have no benefit for single-use instances
1367    pub fn disable_cache(&mut self) {
1368        self.cache_enabled = false;
1369        self.eval_cache.clear(); // Clear any existing cache entries
1370    }
1371    
1372    /// Check if caching is enabled
1373    pub fn is_cache_enabled(&self) -> bool {
1374        self.cache_enabled
1375    }
1376
1377    fn evaluate_others(&mut self) {
1378        time_block!("    evaluate_others()", {
1379            // Step 1: Evaluate options URL templates (handles {variable} patterns)
1380            time_block!("      evaluate_options_templates", {
1381                self.evaluate_options_templates();
1382            });
1383            
1384            // Step 2: Evaluate "rules" and "others" categories with caching
1385            // Rules are evaluated here so their values are available in evaluated_schema
1386            let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
1387            if combined_count == 0 {
1388                return;
1389            }
1390            
1391            time_block!("      evaluate rules+others", {
1392                let eval_data_snapshot = self.eval_data.clone();
1393        
1394        #[cfg(feature = "parallel")]
1395        {
1396            let combined_results: Mutex<Vec<(String, Value)>> = Mutex::new(Vec::with_capacity(combined_count));
1397            
1398            self.rules_evaluations
1399                .par_iter()
1400                .chain(self.others_evaluations.par_iter())
1401                .for_each(|eval_key| {
1402                    let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
1403
1404                    // Try cache first (thread-safe)
1405                    if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
1406                        return;
1407                    }
1408
1409                    // Cache miss - evaluate
1410                    if let Some(logic_id) = self.evaluations.get(eval_key) {
1411                        if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
1412                            let cleaned_val = clean_float_noise(val);
1413                            // Cache result (thread-safe)
1414                            self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
1415                            combined_results.lock().unwrap().push((pointer_path, cleaned_val));
1416                        }
1417                    }
1418                });
1419
1420            // Write results to evaluated_schema
1421            for (result_path, value) in combined_results.into_inner().unwrap() {
1422                if let Some(pointer_value) = self.evaluated_schema.pointer_mut(&result_path) {
1423                    // Special handling for rules with $evaluation
1424                    // This includes both direct rules and array items: /rules/evaluation/0/$evaluation
1425                    if !result_path.starts_with("$") && result_path.contains("/rules/") && !result_path.ends_with("/value") {
1426                        match pointer_value.as_object_mut() {
1427                            Some(pointer_obj) => {
1428                                pointer_obj.remove("$evaluation");
1429                                pointer_obj.insert("value".to_string(), value);
1430                            },
1431                            None => continue,
1432                        }
1433                    } else {
1434                        *pointer_value = value;
1435                    }
1436                }
1437            }
1438        }
1439        
1440        #[cfg(not(feature = "parallel"))]
1441        {
1442            // Sequential evaluation
1443            for eval_key in self.rules_evaluations.iter().chain(self.others_evaluations.iter()) {
1444                let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
1445                
1446                // Try cache first
1447                if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
1448                    continue;
1449                }
1450                
1451                // Cache miss - evaluate
1452                if let Some(logic_id) = self.evaluations.get(eval_key) {
1453                    if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
1454                        let cleaned_val = clean_float_noise(val);
1455                        // Cache result
1456                        self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
1457                        
1458                        if let Some(pointer_value) = self.evaluated_schema.pointer_mut(&pointer_path) {
1459                            if !pointer_path.starts_with("$") && pointer_path.contains("/rules/") && !pointer_path.ends_with("/value") {
1460                                match pointer_value.as_object_mut() {
1461                                    Some(pointer_obj) => {
1462                                        pointer_obj.remove("$evaluation");
1463                                        pointer_obj.insert("value".to_string(), cleaned_val);
1464                                    },
1465                                    None => continue,
1466                                }
1467                            } else {
1468                                *pointer_value = cleaned_val;
1469                            }
1470                        }
1471                    }
1472                }
1473            }
1474        }
1475            });
1476        });
1477    }
1478    
1479    /// Evaluate options URL templates (handles {variable} patterns)
1480    fn evaluate_options_templates(&mut self) {
1481        // Use pre-collected options templates from parsing (Arc clone is cheap)
1482        let templates_to_eval = self.options_templates.clone();
1483        
1484        // Evaluate each template
1485        for (path, template_str, params_path) in templates_to_eval.iter() {
1486            if let Some(params) = self.evaluated_schema.pointer(&params_path) {
1487                if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
1488                    if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
1489                        *target = Value::String(evaluated);
1490                    }
1491                }
1492            }
1493        }
1494    }
1495    
1496    /// Evaluate a template string like "api/users/{id}" with params
1497    fn evaluate_template(&self, template: &str, params: &Value) -> Result<String, String> {
1498        let mut result = template.to_string();
1499        
1500        // Simple template evaluation: replace {key} with params.key
1501        if let Value::Object(params_map) = params {
1502            for (key, value) in params_map {
1503                let placeholder = format!("{{{}}}", key);
1504                if let Some(str_val) = value.as_str() {
1505                    result = result.replace(&placeholder, str_val);
1506                } else {
1507                    // Convert non-string values to strings
1508                    result = result.replace(&placeholder, &value.to_string());
1509                }
1510            }
1511        }
1512        
1513        Ok(result)
1514    }
1515
1516    /// Compile a logic expression from a JSON string and store it globally
1517    /// 
1518    /// Returns a CompiledLogicId that can be used with run_logic for zero-clone evaluation.
1519    /// The compiled logic is stored in a global thread-safe cache and can be shared across
1520    /// different JSONEval instances. If the same logic was compiled before, returns the existing ID.
1521    /// 
1522    /// For repeated evaluations with different data, compile once and run multiple times.
1523    ///
1524    /// # Arguments
1525    ///
1526    /// * `logic_str` - JSON logic expression as a string
1527    ///
1528    /// # Returns
1529    ///
1530    /// A CompiledLogicId that can be reused for multiple evaluations across instances
1531    pub fn compile_logic(&self, logic_str: &str) -> Result<CompiledLogicId, String> {
1532        rlogic::compiled_logic_store::compile_logic(logic_str)
1533    }
1534    
1535    /// Compile a logic expression from a Value and store it globally
1536    /// 
1537    /// This is more efficient than compile_logic when you already have a parsed Value,
1538    /// as it avoids the JSON string serialization/parsing overhead.
1539    /// 
1540    /// Returns a CompiledLogicId that can be used with run_logic for zero-clone evaluation.
1541    /// The compiled logic is stored in a global thread-safe cache and can be shared across
1542    /// different JSONEval instances. If the same logic was compiled before, returns the existing ID.
1543    ///
1544    /// # Arguments
1545    ///
1546    /// * `logic` - JSON logic expression as a Value
1547    ///
1548    /// # Returns
1549    ///
1550    /// A CompiledLogicId that can be reused for multiple evaluations across instances
1551    pub fn compile_logic_value(&self, logic: &Value) -> Result<CompiledLogicId, String> {
1552        rlogic::compiled_logic_store::compile_logic_value(logic)
1553    }
1554    
1555    /// Run pre-compiled logic with zero-clone pattern
1556    /// 
1557    /// Uses references to avoid data cloning - similar to evaluate method.
1558    /// This is the most efficient way to evaluate logic multiple times with different data.
1559    /// The CompiledLogicId is retrieved from global storage, allowing the same compiled logic
1560    /// to be used across different JSONEval instances.
1561    ///
1562    /// # Arguments
1563    ///
1564    /// * `logic_id` - Pre-compiled logic ID from compile_logic
1565    /// * `data` - Optional data to evaluate against (uses existing data if None)
1566    /// * `context` - Optional context to use (uses existing context if None)
1567    ///
1568    /// # Returns
1569    ///
1570    /// The result of the evaluation as a Value
1571    pub fn run_logic(&mut self, logic_id: CompiledLogicId, data: Option<&Value>, context: Option<&Value>) -> Result<Value, String> {
1572        // Get compiled logic from global store
1573        let compiled_logic = rlogic::compiled_logic_store::get_compiled_logic(logic_id)
1574            .ok_or_else(|| format!("Compiled logic ID {:?} not found in store", logic_id))?;
1575        
1576        // Get the data to evaluate against
1577        // If custom data is provided, merge it with context and $params
1578        // Otherwise, use the existing eval_data which already has everything merged
1579        let eval_data_value = if let Some(input_data) = data {
1580            let context_value = context.unwrap_or(&self.context);
1581            
1582            self.eval_data.replace_data_and_context(input_data.clone(), context_value.clone());
1583            self.eval_data.data()
1584        } else {
1585            self.eval_data.data()
1586        };
1587        
1588        // Create an evaluator and run the pre-compiled logic with zero-clone pattern
1589        let evaluator = Evaluator::new();
1590        let result = evaluator.evaluate(&compiled_logic, &eval_data_value)?;
1591        
1592        Ok(clean_float_noise(result))
1593    }
1594    
1595    /// Compile and run JSON logic in one step (convenience method)
1596    /// 
1597    /// This is a convenience wrapper that combines compile_logic and run_logic.
1598    /// For repeated evaluations with different data, use compile_logic once 
1599    /// and run_logic multiple times for better performance.
1600    ///
1601    /// # Arguments
1602    ///
1603    /// * `logic_str` - JSON logic expression as a string
1604    /// * `data` - Optional data JSON string to evaluate against (uses existing data if None)
1605    /// * `context` - Optional context JSON string to use (uses existing context if None)
1606    ///
1607    /// # Returns
1608    ///
1609    /// The result of the evaluation as a Value
1610    pub fn compile_and_run_logic(&mut self, logic_str: &str, data: Option<&str>, context: Option<&str>) -> Result<Value, String> {
1611        // Parse the logic string and compile
1612        let compiled_logic = self.compile_logic(logic_str)?;
1613        
1614        // Parse data and context if provided
1615        let data_value = if let Some(data_str) = data {
1616            Some(json_parser::parse_json_str(data_str)?)
1617        } else {
1618            None
1619        };
1620        
1621        let context_value = if let Some(ctx_str) = context {
1622            Some(json_parser::parse_json_str(ctx_str)?)
1623        } else {
1624            None
1625        };
1626        
1627        // Run the compiled logic
1628        self.run_logic(compiled_logic, data_value.as_ref(), context_value.as_ref())
1629    }
1630
1631    /// Resolve layout references with optional evaluation
1632    ///
1633    /// # Arguments
1634    ///
1635    /// * `evaluate` - If true, runs evaluation before resolving layout. If false, only resolves layout.
1636    ///
1637    /// # Returns
1638    ///
1639    /// A Result indicating success or an error message.
1640    pub fn resolve_layout(&mut self, evaluate: bool) -> Result<(), String> {
1641        if evaluate {
1642            // Use existing data
1643            let data_str = serde_json::to_string(&self.data)
1644                .map_err(|e| format!("Failed to serialize data: {}", e))?;
1645            self.evaluate(&data_str, None)?;
1646        }
1647        
1648        self.resolve_layout_internal();
1649        Ok(())
1650    }
1651    
1652    fn resolve_layout_internal(&mut self) {
1653        time_block!("  resolve_layout_internal()", {
1654            // Use cached layout paths (collected at parse time)
1655            // Clone Arc reference (cheap)
1656            let layout_paths = self.layout_paths.clone();
1657            
1658            time_block!("    resolve_layout_elements", {
1659                for layout_path in layout_paths.iter() {
1660                    self.resolve_layout_elements(layout_path);
1661                }
1662            });
1663            
1664            // After resolving all references, propagate parent hidden/disabled to children
1665            time_block!("    propagate_parent_conditions", {
1666                for layout_path in layout_paths.iter() {
1667                    self.propagate_parent_conditions(layout_path);
1668                }
1669            });
1670        });
1671    }
1672    
1673    /// Propagate parent hidden/disabled conditions to children recursively
1674    fn propagate_parent_conditions(&mut self, layout_elements_path: &str) {
1675        // Normalize path from schema format (#/) to JSON pointer format (/)
1676        let normalized_path = path_utils::normalize_to_json_pointer(layout_elements_path);
1677        
1678        // Extract elements array to avoid borrow checker issues
1679        let elements = if let Some(Value::Array(arr)) = self.evaluated_schema.pointer_mut(&normalized_path) {
1680            mem::take(arr)
1681        } else {
1682            return;
1683        };
1684        
1685        // Process elements (now we can borrow self immutably)
1686        let mut updated_elements = Vec::with_capacity(elements.len());
1687        for element in elements {
1688            updated_elements.push(self.apply_parent_conditions(element, false, false));
1689        }
1690        
1691        // Write back the updated elements
1692        if let Some(target) = self.evaluated_schema.pointer_mut(&normalized_path) {
1693            *target = Value::Array(updated_elements);
1694        }
1695    }
1696    
1697    /// Recursively apply parent hidden/disabled conditions to an element and its children
1698    fn apply_parent_conditions(&self, element: Value, parent_hidden: bool, parent_disabled: bool) -> Value {
1699        if let Value::Object(mut map) = element {
1700            // Get current element's condition
1701            let mut element_hidden = parent_hidden;
1702            let mut element_disabled = parent_disabled;
1703            
1704            // Check condition field (used by field elements with $ref)
1705            if let Some(Value::Object(condition)) = map.get("condition") {
1706                if let Some(Value::Bool(hidden)) = condition.get("hidden") {
1707                    element_hidden = element_hidden || *hidden;
1708                }
1709                if let Some(Value::Bool(disabled)) = condition.get("disabled") {
1710                    element_disabled = element_disabled || *disabled;
1711                }
1712            }
1713            
1714            // Check hideLayout field (used by direct layout elements without $ref)
1715            if let Some(Value::Object(hide_layout)) = map.get("hideLayout") {
1716                // Check hideLayout.all
1717                if let Some(Value::Bool(all_hidden)) = hide_layout.get("all") {
1718                    if *all_hidden {
1719                        element_hidden = true;
1720                    }
1721                }
1722            }
1723            
1724            // Update condition to include parent state (for field elements)
1725            if parent_hidden || parent_disabled {
1726                // Update condition field if it exists or if this is a field element
1727                if map.contains_key("condition") || map.contains_key("$ref") || map.contains_key("$fullpath") {
1728                    let mut condition = if let Some(Value::Object(c)) = map.get("condition") {
1729                        c.clone()
1730                    } else {
1731                        serde_json::Map::new()
1732                    };
1733                    
1734                    if parent_hidden {
1735                        condition.insert("hidden".to_string(), Value::Bool(true));
1736                    }
1737                    if parent_disabled {
1738                        condition.insert("disabled".to_string(), Value::Bool(true));
1739                    }
1740                    
1741                    map.insert("condition".to_string(), Value::Object(condition));
1742                }
1743                
1744                // Update hideLayout for direct layout elements
1745                if parent_hidden && (map.contains_key("hideLayout") || map.contains_key("type")) {
1746                    let mut hide_layout = if let Some(Value::Object(h)) = map.get("hideLayout") {
1747                        h.clone()
1748                    } else {
1749                        serde_json::Map::new()
1750                    };
1751                    
1752                    // Set hideLayout.all to true when parent is hidden
1753                    hide_layout.insert("all".to_string(), Value::Bool(true));
1754                    map.insert("hideLayout".to_string(), Value::Object(hide_layout));
1755                }
1756            }
1757            
1758            // Update $parentHide flag if element has it (came from $ref resolution)
1759            // Only update if the element already has the field (to avoid adding it to non-ref elements)
1760            if map.contains_key("$parentHide") {
1761                map.insert("$parentHide".to_string(), Value::Bool(parent_hidden));
1762            }
1763            
1764            // Recursively process children if elements array exists
1765            if let Some(Value::Array(elements)) = map.get("elements") {
1766                let mut updated_children = Vec::with_capacity(elements.len());
1767                for child in elements {
1768                    updated_children.push(self.apply_parent_conditions(
1769                        child.clone(),
1770                        element_hidden,
1771                        element_disabled,
1772                    ));
1773                }
1774                map.insert("elements".to_string(), Value::Array(updated_children));
1775            }
1776            
1777            return Value::Object(map);
1778        }
1779        
1780        element
1781    }
1782    
1783    /// Resolve $ref references in layout elements (recursively)
1784    fn resolve_layout_elements(&mut self, layout_elements_path: &str) {
1785        // Normalize path from schema format (#/) to JSON pointer format (/)
1786        let normalized_path = path_utils::normalize_to_json_pointer(layout_elements_path);
1787        
1788        // Always read elements from original schema (not evaluated_schema)
1789        // This ensures we get fresh $ref entries on re-evaluation
1790        // since evaluated_schema elements get mutated to objects after first resolution
1791        let elements = if let Some(Value::Array(arr)) = self.schema.pointer(&normalized_path) {
1792            arr.clone()
1793        } else {
1794            return;
1795        };
1796        
1797        // Extract the parent path from normalized_path (e.g., "/properties/form/$layout/elements" -> "form.$layout")
1798        let parent_path = normalized_path
1799            .trim_start_matches('/')
1800            .replace("/elements", "")
1801            .replace('/', ".");
1802        
1803        // Process elements (now we can borrow self immutably)
1804        let mut resolved_elements = Vec::with_capacity(elements.len());
1805        for (index, element) in elements.iter().enumerate() {
1806            let element_path = if parent_path.is_empty() {
1807                format!("elements.{}", index)
1808            } else {
1809                format!("{}.elements.{}", parent_path, index)
1810            };
1811            let resolved = self.resolve_element_ref_recursive(element.clone(), &element_path);
1812            resolved_elements.push(resolved);
1813        }
1814        
1815        // Write back the resolved elements
1816        if let Some(target) = self.evaluated_schema.pointer_mut(&normalized_path) {
1817            *target = Value::Array(resolved_elements);
1818        }
1819    }
1820    
1821    /// Recursively resolve $ref in an element and its nested elements
1822    /// path_context: The dotted path to the current element (e.g., "form.$layout.elements.0")
1823    fn resolve_element_ref_recursive(&self, element: Value, path_context: &str) -> Value {
1824        // First resolve the current element's $ref
1825        let resolved = self.resolve_element_ref(element);
1826        
1827        // Then recursively resolve any nested elements arrays
1828        if let Value::Object(mut map) = resolved {
1829            // Ensure all layout elements have metadata fields
1830            // For elements with $ref, these were already set by resolve_element_ref
1831            // For direct layout elements without $ref, set them based on path_context
1832            if !map.contains_key("$parentHide") {
1833                map.insert("$parentHide".to_string(), Value::Bool(false));
1834            }
1835            
1836            // Set path metadata for direct layout elements (without $ref)
1837            if !map.contains_key("$fullpath") {
1838                map.insert("$fullpath".to_string(), Value::String(path_context.to_string()));
1839            }
1840            
1841            if !map.contains_key("$path") {
1842                // Extract last segment from path_context
1843                let last_segment = path_context.split('.').last().unwrap_or(path_context);
1844                map.insert("$path".to_string(), Value::String(last_segment.to_string()));
1845            }
1846            
1847            // Check if this object has an "elements" array
1848            if let Some(Value::Array(elements)) = map.get("elements") {
1849                let mut resolved_nested = Vec::with_capacity(elements.len());
1850                for (index, nested_element) in elements.iter().enumerate() {
1851                    let nested_path = format!("{}.elements.{}", path_context, index);
1852                    resolved_nested.push(self.resolve_element_ref_recursive(nested_element.clone(), &nested_path));
1853                }
1854                map.insert("elements".to_string(), Value::Array(resolved_nested));
1855            }
1856            
1857            return Value::Object(map);
1858        }
1859        
1860        resolved
1861    }
1862    
1863    /// Resolve $ref in a single element
1864    fn resolve_element_ref(&self, element: Value) -> Value {
1865        match element {
1866            Value::Object(mut map) => {
1867                // Check if element has $ref
1868                if let Some(Value::String(ref_path)) = map.get("$ref").cloned() {
1869                    // Convert ref_path to dotted notation for metadata storage
1870                    let dotted_path = path_utils::pointer_to_dot_notation(&ref_path);
1871                    
1872                    // Extract last segment for $path and path fields
1873                    let last_segment = dotted_path.split('.').last().unwrap_or(&dotted_path);
1874                    
1875                    // Inject metadata fields with dotted notation
1876                    map.insert("$fullpath".to_string(), Value::String(dotted_path.clone()));
1877                    map.insert("$path".to_string(), Value::String(last_segment.to_string()));
1878                    map.insert("$parentHide".to_string(), Value::Bool(false));
1879                    
1880                    // Normalize to JSON pointer for actual lookup
1881                    // Try different path formats to find the referenced value
1882                    let normalized_path = if ref_path.starts_with('#') || ref_path.starts_with('/') {
1883                        // Already a pointer, normalize it
1884                        path_utils::normalize_to_json_pointer(&ref_path)
1885                    } else {
1886                        // Try as schema path first (for paths like "illustration.insured.name")
1887                        let schema_pointer = path_utils::dot_notation_to_schema_pointer(&ref_path);
1888                        let schema_path = path_utils::normalize_to_json_pointer(&schema_pointer);
1889                        
1890                        // Check if it exists
1891                        if self.evaluated_schema.pointer(&schema_path).is_some() {
1892                            schema_path
1893                        } else {
1894                            // Try with /properties/ prefix (for simple refs like "parent_container")
1895                            let with_properties = format!("/properties/{}", ref_path.replace('.', "/properties/"));
1896                            with_properties
1897                        }
1898                    };
1899                    
1900                    // Get the referenced value
1901                    if let Some(referenced_value) = self.evaluated_schema.pointer(&normalized_path) {
1902                        // Clone the referenced value
1903                        let resolved = referenced_value.clone();
1904                        
1905                        // If resolved is an object, check for special handling
1906                        if let Value::Object(mut resolved_map) = resolved {
1907                            // Remove $ref from original map
1908                            map.remove("$ref");
1909                            
1910                            // Special case: if resolved has $layout, flatten it
1911                            // Extract $layout contents and merge at root level
1912                            if let Some(Value::Object(layout_obj)) = resolved_map.remove("$layout") {
1913                                // Start with layout properties (they become root properties)
1914                                let mut result = layout_obj.clone();
1915                                
1916                                // Remove properties from resolved (we don't want it)
1917                                resolved_map.remove("properties");
1918                                
1919                                // Merge remaining resolved_map properties (except type if layout has it)
1920                                for (key, value) in resolved_map {
1921                                    if key != "type" || !result.contains_key("type") {
1922                                        result.insert(key, value);
1923                                    }
1924                                }
1925                                
1926                                // Finally, merge element override properties
1927                                for (key, value) in map {
1928                                    result.insert(key, value);
1929                                }
1930                                
1931                                return Value::Object(result);
1932                            } else {
1933                                // Normal merge: element properties override referenced properties
1934                                for (key, value) in map {
1935                                    resolved_map.insert(key, value);
1936                                }
1937                                
1938                                return Value::Object(resolved_map);
1939                            }
1940                        } else {
1941                            // If referenced value is not an object, just return it
1942                            return resolved;
1943                        }
1944                    }
1945                }
1946                
1947                // No $ref or couldn't resolve - return element as-is
1948                Value::Object(map)
1949            }
1950            _ => element,
1951        }
1952    }
1953
1954    /// Evaluate fields that depend on a changed path
1955    /// This processes all dependent fields transitively when a source field changes
1956    /// 
1957    /// # Arguments
1958    /// * `changed_paths` - Array of field paths that changed (supports dot notation or schema pointers)
1959    /// * `data` - Optional JSON data to update before processing
1960    /// * `context` - Optional context data
1961    /// * `re_evaluate` - If true, performs full evaluation after processing dependents
1962    pub fn evaluate_dependents(
1963        &mut self,
1964        changed_paths: &[String],
1965        data: Option<&str>,
1966        context: Option<&str>,
1967        re_evaluate: bool,
1968    ) -> Result<Value, String> {
1969        // Acquire lock for synchronous execution
1970        let _lock = self.eval_lock.lock().unwrap();
1971        
1972        // Update data if provided
1973        if let Some(data_str) = data {
1974            // Save old data for comparison
1975            let old_data = self.eval_data.clone_data_without(&["$params"]);
1976            
1977            let data_value = json_parser::parse_json_str(data_str)?;
1978            let context_value = if let Some(ctx) = context {
1979                json_parser::parse_json_str(ctx)?
1980            } else {
1981                Value::Object(serde_json::Map::new())
1982            };
1983            self.eval_data.replace_data_and_context(data_value.clone(), context_value);
1984            
1985            // Selectively purge cache entries that depend on changed data
1986            // Only purge if values actually changed
1987            // Convert changed_paths to data pointer format for cache purging
1988            let data_paths: Vec<String> = changed_paths
1989                .iter()
1990                .map(|path| {
1991                    // Convert "illustration.insured.ins_dob" to "/illustration/insured/ins_dob"
1992                    format!("/{}", path.replace('.', "/"))
1993                })
1994                .collect();
1995            self.purge_cache_for_changed_data_with_comparison(&data_paths, &old_data, &data_value);
1996        }
1997        
1998        let mut result = Vec::new();
1999        let mut processed = IndexSet::new();
2000        
2001        // Normalize all changed paths and add to processing queue
2002        // Converts: "illustration.insured.name" -> "#/illustration/properties/insured/properties/name"
2003        let mut to_process: Vec<(String, bool)> = changed_paths
2004            .iter()
2005            .map(|path| (path_utils::dot_notation_to_schema_pointer(path), false))
2006            .collect(); // (path, is_transitive)
2007        
2008        // Process dependents recursively (always nested/transitive)
2009        while let Some((current_path, is_transitive)) = to_process.pop() {
2010            if processed.contains(&current_path) {
2011                continue;
2012            }
2013            processed.insert(current_path.clone());
2014            
2015            // Get the value of the changed field for $value context
2016            let current_data_path = path_utils::normalize_to_json_pointer(&current_path)
2017                .replace("/properties/", "/")
2018                .trim_start_matches('#')
2019                .to_string();
2020            let mut current_value = self.eval_data.data().pointer(&current_data_path)
2021                .cloned()
2022                .unwrap_or(Value::Null);
2023            
2024            // Find dependents for this path
2025            if let Some(dependent_items) = self.dependents_evaluations.get(&current_path) {
2026                for dep_item in dependent_items {
2027                    let ref_path = &dep_item.ref_path;
2028                    let pointer_path = path_utils::normalize_to_json_pointer(ref_path);
2029                    // Data paths don't include /properties/, strip it for data access
2030                    let data_path = pointer_path.replace("/properties/", "/");
2031
2032                    let current_ref_value = self.eval_data.data().pointer(&data_path)
2033                        .cloned()
2034                        .unwrap_or(Value::Null);
2035                    
2036                    // Get field and parent field from schema
2037                    let field = self.evaluated_schema.pointer(&pointer_path).cloned();
2038                    
2039                    // Get parent field - skip /properties/ to get actual parent object
2040                    let parent_path = if let Some(last_slash) = pointer_path.rfind("/properties") {
2041                        &pointer_path[..last_slash]
2042                    } else {
2043                        "/"
2044                    };
2045                    let mut parent_field = if parent_path.is_empty() || parent_path == "/" {
2046                        self.evaluated_schema.clone()
2047                    } else {
2048                        self.evaluated_schema.pointer(parent_path).cloned()
2049                            .unwrap_or_else(|| Value::Object(serde_json::Map::new()))
2050                    };
2051
2052                    // omit properties to minimize size of parent field
2053                    if let Value::Object(ref mut map) = parent_field {
2054                        map.remove("properties");
2055                        map.remove("$layout");
2056                    }
2057                    
2058                    let mut change_obj = serde_json::Map::new();
2059                    change_obj.insert("$ref".to_string(), Value::String(path_utils::pointer_to_dot_notation(&data_path)));
2060                    if let Some(f) = field {
2061                        change_obj.insert("$field".to_string(), f);
2062                    }
2063                    change_obj.insert("$parentField".to_string(), parent_field);
2064                    change_obj.insert("transitive".to_string(), Value::Bool(is_transitive));
2065                    
2066                    let mut add_transitive = false;
2067                    let mut add_deps = false;
2068                    // Process clear
2069                    if let Some(clear_val) = &dep_item.clear {
2070                        let clear_val_clone = clear_val.clone();
2071                        let should_clear = Self::evaluate_dependent_value_static(&self.engine, &self.evaluations, &self.eval_data, &clear_val_clone, &current_value, &current_ref_value)?;
2072                        let clear_bool = match should_clear {
2073                            Value::Bool(b) => b,
2074                            _ => false,
2075                        };
2076                        
2077                        if clear_bool {
2078                            // Clear the field
2079                            if data_path == current_data_path {
2080                                current_value = Value::Null;
2081                            }
2082                            self.eval_data.set(&data_path, Value::Null);
2083                            change_obj.insert("clear".to_string(), Value::Bool(true));
2084                            add_transitive = true;
2085                            add_deps = true;
2086                        }
2087                    }
2088                    
2089                    // Process value
2090                    if let Some(value_val) = &dep_item.value {
2091                        let value_val_clone = value_val.clone();
2092                        let computed_value = Self::evaluate_dependent_value_static(&self.engine, &self.evaluations, &self.eval_data, &value_val_clone, &current_value, &current_ref_value)?;
2093                        let cleaned_val = clean_float_noise(computed_value.clone());
2094                        
2095                        if cleaned_val != current_ref_value && cleaned_val != Value::Null {   
2096                            // Set the value
2097                            if data_path == current_data_path {
2098                                current_value = cleaned_val.clone();
2099                            }
2100                            self.eval_data.set(&data_path, cleaned_val.clone());
2101                            change_obj.insert("value".to_string(), cleaned_val);
2102                            add_transitive = true;
2103                            add_deps = true;
2104                        }
2105                    }
2106                    
2107                    // add only when has clear / value
2108                    if add_deps {
2109                        result.push(Value::Object(change_obj));
2110                    }
2111                    
2112                    // Add this dependent to queue for transitive processing
2113                    if add_transitive {
2114                        to_process.push((ref_path.clone(), true));
2115                    }
2116                }
2117            }
2118        }
2119        
2120        // If re_evaluate is true, perform full evaluation with the mutated eval_data
2121        // Use evaluate_internal to avoid serialization overhead
2122        // We need to drop the lock first since evaluate_internal acquires its own lock
2123        if re_evaluate {
2124            drop(_lock);  // Release the evaluate_dependents lock
2125            self.evaluate_internal()?;
2126        }
2127        
2128        Ok(Value::Array(result))
2129    }
2130    
2131    /// Helper to evaluate a dependent value - uses pre-compiled eval keys for fast lookup
2132    fn evaluate_dependent_value_static(
2133        engine: &RLogic,
2134        evaluations: &IndexMap<String, LogicId>,
2135        eval_data: &EvalData,
2136        value: &Value,
2137        changed_field_value: &Value,
2138        changed_field_ref_value: &Value
2139    ) -> Result<Value, String> {
2140        match value {
2141            // If it's a String, check if it's an eval key reference
2142            Value::String(eval_key) => {
2143                if let Some(logic_id) = evaluations.get(eval_key) {
2144                    // It's a pre-compiled evaluation - run it with scoped context
2145                    // Create internal context with $value and $refValue
2146                    let mut internal_context = serde_json::Map::new();
2147                    internal_context.insert("$value".to_string(), changed_field_value.clone());
2148                    internal_context.insert("$refValue".to_string(), changed_field_ref_value.clone());
2149                    let context_value = Value::Object(internal_context);
2150                    
2151                    let result = engine.run_with_context(logic_id, eval_data.data(), &context_value)
2152                        .map_err(|e| format!("Failed to evaluate dependent logic '{}': {}", eval_key, e))?;
2153                    Ok(result)
2154                } else {
2155                    // It's a regular string value
2156                    Ok(value.clone())
2157                }
2158            }
2159            // For backwards compatibility: compile $evaluation on-the-fly
2160            // This shouldn't happen with properly parsed schemas
2161            Value::Object(map) if map.contains_key("$evaluation") => {
2162                Err("Dependent evaluation contains unparsed $evaluation - schema was not properly parsed".to_string())
2163            }
2164            // Primitive value - return as-is
2165            _ => Ok(value.clone()),
2166        }
2167    }
2168
2169    /// Validate form data against schema rules
2170    /// Returns validation errors for fields that don't meet their rules
2171    pub fn validate(
2172        &mut self,
2173        data: &str,
2174        context: Option<&str>,
2175        paths: Option<&[String]>
2176    ) -> Result<ValidationResult, String> {
2177        // Acquire lock for synchronous execution
2178        let _lock = self.eval_lock.lock().unwrap();
2179        
2180        // Save old data for comparison
2181        let old_data = self.eval_data.clone_data_without(&["$params"]);
2182        
2183        // Parse data and context
2184        let data_value = json_parser::parse_json_str(data)?;
2185        let context_value = if let Some(ctx) = context {
2186            json_parser::parse_json_str(ctx)?
2187        } else {
2188            Value::Object(serde_json::Map::new())
2189        };
2190        
2191        // Update eval_data with new data/context
2192        self.eval_data.replace_data_and_context(data_value.clone(), context_value);
2193        
2194        // Selectively purge cache for rule evaluations that depend on changed data
2195        // Collect all top-level data keys as potentially changed paths
2196        let changed_data_paths: Vec<String> = if let Some(obj) = data_value.as_object() {
2197            obj.keys().map(|k| format!("/{}", k)).collect()
2198        } else {
2199            Vec::new()
2200        };
2201        self.purge_cache_for_changed_data_with_comparison(&changed_data_paths, &old_data, &data_value);
2202        
2203        // Drop lock before calling evaluate_others which needs mutable access
2204        drop(_lock);
2205        
2206        // Re-evaluate rule evaluations to ensure fresh values
2207        // This ensures all rule.$evaluation expressions are re-computed
2208        self.evaluate_others();
2209        
2210        // Update evaluated_schema with fresh evaluations
2211        self.evaluated_schema = self.get_evaluated_schema(false);
2212        
2213        let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
2214        
2215        // Use pre-parsed fields_with_rules from schema parsing (no runtime collection needed)
2216        // This list was collected during schema parse and contains all fields with rules
2217        for field_path in self.fields_with_rules.iter() {
2218            // Check if we should validate this path (path filtering)
2219            if let Some(filter_paths) = paths {
2220                if !filter_paths.is_empty() && !filter_paths.iter().any(|p| field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())) {
2221                    continue;
2222                }
2223            }
2224            
2225            self.validate_field(field_path, &data_value, &mut errors);
2226        }
2227        
2228        let has_error = !errors.is_empty();
2229        
2230        Ok(ValidationResult {
2231            has_error,
2232            errors,
2233        })
2234    }
2235    
2236    /// Validate a single field that has rules
2237    fn validate_field(
2238        &self,
2239        field_path: &str,
2240        data: &Value,
2241        errors: &mut IndexMap<String, ValidationError>
2242    ) {
2243        // Skip if already has error
2244        if errors.contains_key(field_path) {
2245            return;
2246        }
2247        
2248        // Get schema for this field
2249        let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
2250        
2251        // Remove leading "#" from path for pointer lookup
2252        let pointer_path = schema_path.trim_start_matches('#');
2253        
2254        // Try to get schema, if not found, try with /properties/ prefix for standard JSON Schema
2255        let field_schema = match self.evaluated_schema.pointer(pointer_path) {
2256            Some(s) => s,
2257            None => {
2258                // Try with /properties/ prefix (for standard JSON Schema format)
2259                let alt_path = format!("/properties{}", pointer_path);
2260                match self.evaluated_schema.pointer(&alt_path) {
2261                    Some(s) => s,
2262                    None => return,
2263                }
2264            }
2265        };
2266        
2267        // Check if field is hidden (skip validation)
2268        if let Value::Object(schema_map) = field_schema {
2269            if let Some(Value::Object(condition)) = schema_map.get("condition") {
2270                if let Some(Value::Bool(true)) = condition.get("hidden") {
2271                    return;
2272                }
2273            }
2274            
2275            // Get rules object
2276            let rules = match schema_map.get("rules") {
2277                Some(Value::Object(r)) => r,
2278                _ => return,
2279            };
2280            
2281            // Get field data
2282            let field_data = self.get_field_data(field_path, data);
2283            
2284            // Validate each rule
2285            for (rule_name, rule_value) in rules {
2286                self.validate_rule(
2287                    field_path,
2288                    rule_name,
2289                    rule_value,
2290                    &field_data,
2291                    schema_map,
2292                    field_schema,
2293                    errors
2294                );
2295            }
2296        }
2297    }
2298    
2299    /// Get data value for a field path
2300    fn get_field_data(&self, field_path: &str, data: &Value) -> Value {
2301        let parts: Vec<&str> = field_path.split('.').collect();
2302        let mut current = data;
2303        
2304        for part in parts {
2305            match current {
2306                Value::Object(map) => {
2307                    current = map.get(part).unwrap_or(&Value::Null);
2308                }
2309                _ => return Value::Null,
2310            }
2311        }
2312        
2313        current.clone()
2314    }
2315    
2316    /// Validate a single rule
2317    fn validate_rule(
2318        &self,
2319        field_path: &str,
2320        rule_name: &str,
2321        rule_value: &Value,
2322        field_data: &Value,
2323        schema_map: &serde_json::Map<String, Value>,
2324        _schema: &Value,
2325        errors: &mut IndexMap<String, ValidationError>
2326    ) {
2327        // Skip if already has error
2328        if errors.contains_key(field_path) {
2329            return;
2330        }
2331        
2332        // Check if disabled
2333        if let Some(Value::Object(condition)) = schema_map.get("condition") {
2334            if let Some(Value::Bool(true)) = condition.get("disabled") {
2335                return;
2336            }
2337        }
2338        
2339        // Get the evaluated rule from evaluated_schema (which has $evaluation already processed)
2340        // Convert field_path to schema path
2341        let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
2342        let rule_path = format!("{}/rules/{}", schema_path.trim_start_matches('#'), rule_name);
2343        
2344        // Look up the evaluated rule from evaluated_schema
2345        let evaluated_rule = if let Some(eval_rule) = self.evaluated_schema.pointer(&rule_path) {
2346            eval_rule.clone()
2347        } else {
2348            rule_value.clone()
2349        };
2350        
2351        // Extract rule object (after evaluation)
2352        let (rule_active, rule_message, rule_code, rule_data) = match &evaluated_rule {
2353            Value::Object(rule_obj) => {
2354                let active = rule_obj.get("value").unwrap_or(&Value::Bool(false));
2355                
2356                // Handle message - could be string or object with "value"
2357                let message = match rule_obj.get("message") {
2358                    Some(Value::String(s)) => s.clone(),
2359                    Some(Value::Object(msg_obj)) if msg_obj.contains_key("value") => {
2360                        msg_obj.get("value")
2361                            .and_then(|v| v.as_str())
2362                            .unwrap_or("Validation failed")
2363                            .to_string()
2364                    }
2365                    Some(msg_val) => msg_val.as_str().unwrap_or("Validation failed").to_string(),
2366                    None => "Validation failed".to_string()
2367                };
2368                
2369                let code = rule_obj.get("code")
2370                    .and_then(|c| c.as_str())
2371                    .map(|s| s.to_string());
2372                
2373                // Handle data - extract "value" from objects with $evaluation
2374                let data = rule_obj.get("data").map(|d| {
2375                    if let Value::Object(data_obj) = d {
2376                        let mut cleaned_data = serde_json::Map::new();
2377                        for (key, value) in data_obj {
2378                            // If value is an object with only "value" key, extract it
2379                            if let Value::Object(val_obj) = value {
2380                                if val_obj.len() == 1 && val_obj.contains_key("value") {
2381                                    cleaned_data.insert(key.clone(), val_obj["value"].clone());
2382                                } else {
2383                                    cleaned_data.insert(key.clone(), value.clone());
2384                                }
2385                            } else {
2386                                cleaned_data.insert(key.clone(), value.clone());
2387                            }
2388                        }
2389                        Value::Object(cleaned_data)
2390                    } else {
2391                        d.clone()
2392                    }
2393                });
2394                
2395                (active.clone(), message, code, data)
2396            }
2397            _ => (evaluated_rule.clone(), "Validation failed".to_string(), None, None)
2398        };
2399        
2400        // Generate default code if not provided
2401        let error_code = rule_code.or_else(|| Some(format!("{}.{}", field_path, rule_name)));
2402        
2403        let is_empty = matches!(field_data, Value::Null) || 
2404                       (field_data.is_string() && field_data.as_str().unwrap_or("").is_empty()) ||
2405                       (field_data.is_array() && field_data.as_array().unwrap().is_empty());
2406        
2407        match rule_name {
2408            "required" => {
2409                if let Value::Bool(true) = rule_active {
2410                    if is_empty {
2411                        errors.insert(field_path.to_string(), ValidationError {
2412                            rule_type: "required".to_string(),
2413                            message: rule_message,
2414                            code: error_code.clone(),
2415                            pattern: None,
2416                            field_value: None,
2417                            data: None,
2418                        });
2419                    }
2420                }
2421            }
2422            "minLength" => {
2423                if !is_empty {
2424                    if let Some(min) = rule_active.as_u64() {
2425                        let len = match field_data {
2426                            Value::String(s) => s.len(),
2427                            Value::Array(a) => a.len(),
2428                            _ => 0
2429                        };
2430                        if len < min as usize {
2431                            errors.insert(field_path.to_string(), ValidationError {
2432                                rule_type: "minLength".to_string(),
2433                                message: rule_message,
2434                                code: error_code.clone(),
2435                                pattern: None,
2436                                field_value: None,
2437                                data: None,
2438                            });
2439                        }
2440                    }
2441                }
2442            }
2443            "maxLength" => {
2444                if !is_empty {
2445                    if let Some(max) = rule_active.as_u64() {
2446                        let len = match field_data {
2447                            Value::String(s) => s.len(),
2448                            Value::Array(a) => a.len(),
2449                            _ => 0
2450                        };
2451                        if len > max as usize {
2452                            errors.insert(field_path.to_string(), ValidationError {
2453                                rule_type: "maxLength".to_string(),
2454                                message: rule_message,
2455                                code: error_code.clone(),
2456                                pattern: None,
2457                                field_value: None,
2458                                data: None,
2459                            });
2460                        }
2461                    }
2462                }
2463            }
2464            "minValue" => {
2465                if !is_empty {
2466                    if let Some(min) = rule_active.as_f64() {
2467                        if let Some(val) = field_data.as_f64() {
2468                            if val < min {
2469                                errors.insert(field_path.to_string(), ValidationError {
2470                                    rule_type: "minValue".to_string(),
2471                                    message: rule_message,
2472                                    code: error_code.clone(),
2473                                    pattern: None,
2474                                    field_value: None,
2475                                    data: None,
2476                                });
2477                            }
2478                        }
2479                    }
2480                }
2481            }
2482            "maxValue" => {
2483                if !is_empty {
2484                    if let Some(max) = rule_active.as_f64() {
2485                        if let Some(val) = field_data.as_f64() {
2486                            if val > max {
2487                                errors.insert(field_path.to_string(), ValidationError {
2488                                    rule_type: "maxValue".to_string(),
2489                                    message: rule_message,
2490                                    code: error_code.clone(),
2491                                    pattern: None,
2492                                    field_value: None,
2493                                    data: None,
2494                                });
2495                            }
2496                        }
2497                    }
2498                }
2499            }
2500            "pattern" => {
2501                if !is_empty {
2502                    if let Some(pattern) = rule_active.as_str() {
2503                        if let Some(text) = field_data.as_str() {
2504                            if let Ok(regex) = regex::Regex::new(pattern) {
2505                                if !regex.is_match(text) {
2506                                    errors.insert(field_path.to_string(), ValidationError {
2507                                        rule_type: "pattern".to_string(),
2508                                        message: rule_message,
2509                                        code: error_code.clone(),
2510                                        pattern: Some(pattern.to_string()),
2511                                        field_value: Some(text.to_string()),
2512                                        data: None,
2513                                    });
2514                                }
2515                            }
2516                        }
2517                    }
2518                }
2519            }
2520            "evaluation" => {
2521                // Handle array of evaluation rules
2522                // Format: "evaluation": [{ "code": "...", "message": "...", "$evaluation": {...} }]
2523                if let Value::Array(eval_array) = &evaluated_rule {
2524                    for (idx, eval_item) in eval_array.iter().enumerate() {
2525                        if let Value::Object(eval_obj) = eval_item {
2526                            // Get the evaluated value (should be in "value" key after evaluation)
2527                            let eval_result = eval_obj.get("value").unwrap_or(&Value::Bool(true));
2528                            
2529                            // Check if result is falsy
2530                            let is_falsy = match eval_result {
2531                                Value::Bool(false) => true,
2532                                Value::Null => true,
2533                                Value::Number(n) => n.as_f64() == Some(0.0),
2534                                Value::String(s) => s.is_empty(),
2535                                Value::Array(a) => a.is_empty(),
2536                                _ => false,
2537                            };
2538                            
2539                            if is_falsy {
2540                                let eval_code = eval_obj.get("code")
2541                                    .and_then(|c| c.as_str())
2542                                    .map(|s| s.to_string())
2543                                    .or_else(|| Some(format!("{}.evaluation.{}", field_path, idx)));
2544                                
2545                                let eval_message = eval_obj.get("message")
2546                                    .and_then(|m| m.as_str())
2547                                    .unwrap_or("Validation failed")
2548                                    .to_string();
2549                                
2550                                let eval_data = eval_obj.get("data").cloned();
2551                                
2552                                errors.insert(field_path.to_string(), ValidationError {
2553                                    rule_type: "evaluation".to_string(),
2554                                    message: eval_message,
2555                                    code: eval_code,
2556                                    pattern: None,
2557                                    field_value: None,
2558                                    data: eval_data,
2559                                });
2560                                
2561                                // Stop at first failure
2562                                break;
2563                            }
2564                        }
2565                    }
2566                }
2567            }
2568            _ => {
2569                // Custom evaluation rules
2570                // In JS: if (!opt.rule.value) then error
2571                // This handles rules with $evaluation that return false/falsy values
2572                if !is_empty {
2573                    // Check if rule_active is falsy (false, 0, null, empty string, empty array)
2574                    let is_falsy = match &rule_active {
2575                        Value::Bool(false) => true,
2576                        Value::Null => true,
2577                        Value::Number(n) => n.as_f64() == Some(0.0),
2578                        Value::String(s) => s.is_empty(),
2579                        Value::Array(a) => a.is_empty(),
2580                        _ => false,
2581                    };
2582                    
2583                    if is_falsy {
2584                        errors.insert(field_path.to_string(), ValidationError {
2585                            rule_type: "evaluation".to_string(),
2586                            message: rule_message,
2587                            code: error_code.clone(),
2588                            pattern: None,
2589                            field_value: None,
2590                            data: rule_data,
2591                        });
2592                    }
2593                }
2594            }
2595        }
2596    }
2597}
2598
2599/// Validation error for a field
2600#[derive(Debug, Clone, Serialize, Deserialize)]
2601pub struct ValidationError {
2602    #[serde(rename = "type")]
2603    pub rule_type: String,
2604    pub message: String,
2605    #[serde(skip_serializing_if = "Option::is_none")]
2606    pub code: Option<String>,
2607    #[serde(skip_serializing_if = "Option::is_none")]
2608    pub pattern: Option<String>,
2609    #[serde(skip_serializing_if = "Option::is_none")]
2610    pub field_value: Option<String>,
2611    #[serde(skip_serializing_if = "Option::is_none")]
2612    pub data: Option<Value>,
2613}
2614
2615/// Result of validation
2616#[derive(Debug, Clone, Serialize, Deserialize)]
2617pub struct ValidationResult {
2618    pub has_error: bool,
2619    pub errors: IndexMap<String, ValidationError>,
2620}
2621