json_eval_rs/rlogic/
compiled_logic_store.rs

1//! Global storage for compiled logic expressions
2//! 
3//! This module provides a thread-safe global store for compiled logic that can be shared
4//! across different JSONEval instances and across FFI boundaries.
5
6use once_cell::sync::Lazy;
7use dashmap::DashMap;
8use std::sync::atomic::{AtomicU64, Ordering};
9use ahash::AHasher;
10use std::hash::{Hash, Hasher};
11use super::CompiledLogic;
12
13/// Unique identifier for a compiled logic expression
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct CompiledLogicId(u64);
16
17impl CompiledLogicId {
18    /// Get the underlying u64 value
19    pub fn as_u64(&self) -> u64 {
20        self.0
21    }
22    
23    /// Create from u64 value
24    pub fn from_u64(id: u64) -> Self {
25        Self(id)
26    }
27}
28
29/// Global storage for compiled logic expressions
30static COMPILED_LOGIC_STORE: Lazy<CompiledLogicStore> = Lazy::new(|| {
31    CompiledLogicStore {
32        store: DashMap::new(),
33        id_map: DashMap::new(),
34        next_id: AtomicU64::new(1), // Start from 1, 0 reserved for invalid
35    }
36});
37
38/// Thread-safe global store for compiled logic
39struct CompiledLogicStore {
40    /// Map from hash to (ID, CompiledLogic)
41    store: DashMap<u64, (CompiledLogicId, CompiledLogic)>,
42    /// Reverse map from ID to CompiledLogic for fast lookup
43    id_map: DashMap<u64, CompiledLogic>,
44    /// Next available ID
45    next_id: AtomicU64,
46}
47
48impl CompiledLogicStore {
49    /// Compile logic from a Value and return an ID
50    /// If the same logic was compiled before, returns the existing ID
51    fn compile_value(&self, logic: &serde_json::Value) -> Result<CompiledLogicId, String> {
52        // Hash the logic value for deduplication
53        let logic_str = serde_json::to_string(logic)
54            .map_err(|e| format!("Failed to serialize logic: {}", e))?;
55        let mut hasher = AHasher::default();
56        logic_str.hash(&mut hasher);
57        let hash = hasher.finish();
58        
59        // Check if already compiled
60        if let Some(entry) = self.store.get(&hash) {
61            return Ok(entry.0);
62        }
63        
64        // Compile using the shared CompiledLogic::compile method
65        let compiled = CompiledLogic::compile(logic)?;
66        
67        // Generate new ID
68        let id = CompiledLogicId(self.next_id.fetch_add(1, Ordering::SeqCst));
69        
70        // Store in both maps
71        self.store.insert(hash, (id, compiled.clone()));
72        self.id_map.insert(id.0, compiled);
73        
74        Ok(id)
75    }
76    
77    /// Compile logic from a JSON string and return an ID
78    /// If the same logic was compiled before, returns the existing ID
79    fn compile(&self, logic_json: &str) -> Result<CompiledLogicId, String> {
80        // Parse JSON
81        let logic: serde_json::Value = serde_json::from_str(logic_json)
82            .map_err(|e| format!("Failed to parse logic JSON: {}", e))?;
83        
84        // Use shared compile_value method
85        self.compile_value(&logic)
86    }
87    
88    /// Get compiled logic by ID (O(1) lookup)
89    fn get(&self, id: CompiledLogicId) -> Option<CompiledLogic> {
90        self.id_map.get(&id.0).map(|v| v.clone())
91    }
92    
93    /// Get statistics about the store
94    fn stats(&self) -> CompiledLogicStoreStats {
95        CompiledLogicStoreStats {
96            compiled_count: self.store.len(),
97            next_id: self.next_id.load(Ordering::SeqCst),
98        }
99    }
100    
101    /// Clear all compiled logic (useful for testing)
102    #[allow(dead_code)]
103    fn clear(&self) {
104        self.store.clear();
105        self.id_map.clear();
106        self.next_id.store(1, Ordering::SeqCst);
107    }
108}
109
110/// Statistics about the compiled logic store
111#[derive(Debug, Clone)]
112pub struct CompiledLogicStoreStats {
113    /// Number of compiled logic expressions stored
114    pub compiled_count: usize,
115    /// Next ID that will be assigned
116    pub next_id: u64,
117}
118
119/// Compile logic from a JSON string and return a unique ID
120/// 
121/// The compiled logic is stored in a global thread-safe cache.
122/// If the same logic was compiled before, returns the existing ID.
123pub fn compile_logic(logic_json: &str) -> Result<CompiledLogicId, String> {
124    COMPILED_LOGIC_STORE.compile(logic_json)
125}
126
127/// Compile logic from a Value and return a unique ID
128/// 
129/// The compiled logic is stored in a global thread-safe cache.
130/// If the same logic was compiled before, returns the existing ID.
131pub fn compile_logic_value(logic: &serde_json::Value) -> Result<CompiledLogicId, String> {
132    COMPILED_LOGIC_STORE.compile_value(logic)
133}
134
135/// Get compiled logic by ID
136pub fn get_compiled_logic(id: CompiledLogicId) -> Option<CompiledLogic> {
137    COMPILED_LOGIC_STORE.get(id)
138}
139
140/// Get statistics about the global compiled logic store
141pub fn get_store_stats() -> CompiledLogicStoreStats {
142    COMPILED_LOGIC_STORE.stats()
143}
144
145/// Clear all compiled logic from the global store
146/// 
147/// **Warning**: This will invalidate all existing CompiledLogicIds
148#[cfg(test)]
149pub fn clear_store() {
150    COMPILED_LOGIC_STORE.clear()
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use std::sync::Mutex;
157    
158    // Test mutex to serialize access to the global store during tests
159    static TEST_MUTEX: Mutex<()> = Mutex::new(());
160
161    #[test]
162    fn test_compile_and_get() {
163        let _lock = TEST_MUTEX.lock().unwrap();
164        clear_store(); // Ensure clean state
165        
166        let logic = r#"{"==": [{"var": "x"}, 10]}"#;
167        let id = compile_logic(logic).expect("Failed to compile");
168        
169        let compiled = get_compiled_logic(id);
170        assert!(compiled.is_some());
171    }
172    
173    #[test]
174    fn test_deduplication() {
175        let _lock = TEST_MUTEX.lock().unwrap();
176        clear_store(); // Ensure clean state
177        
178        let logic = r#"{"*": [{"var": "a"}, 2]}"#;
179        
180        let id1 = compile_logic(logic).expect("Failed to compile");
181        let id2 = compile_logic(logic).expect("Failed to compile");
182        
183        // Same logic should return same ID
184        assert_eq!(id1, id2);
185    }
186    
187    #[test]
188    fn test_different_logic() {
189        let _lock = TEST_MUTEX.lock().unwrap();
190        clear_store(); // Ensure clean state
191        
192        let logic1 = r#"{"*": [{"var": "a"}, 2]}"#;
193        let logic2 = r#"{"*": [{"var": "b"}, 3]}"#;
194        
195        let id1 = compile_logic(logic1).expect("Failed to compile");
196        let id2 = compile_logic(logic2).expect("Failed to compile");
197        
198        // Different logic should return different IDs
199        assert_ne!(id1, id2);
200    }
201    
202    #[test]
203    fn test_stats() {
204        let _lock = TEST_MUTEX.lock().unwrap();
205        clear_store(); // Ensure clean state
206        
207        // Compile some logic to populate the store
208        let logic = r#"{"+": [1, 2, 3]}"#;
209        let _ = compile_logic(logic).expect("Failed to compile");
210        
211        let stats = get_store_stats();
212        assert_eq!(stats.compiled_count, 1);
213        assert_eq!(stats.next_id, 2);
214    }
215}