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