json_eval_rs/
lib.rs

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