spikard_core/
schema_registry.rs

1//! Schema registry for deduplication and OpenAPI generation
2//!
3//! This module provides a global registry that compiles JSON schemas once at application
4//! startup and reuses them across all routes. This enables:
5//! - Schema deduplication (same schema used by multiple routes)
6//! - OpenAPI spec generation (access to all schemas)
7//! - Memory efficiency (one compiled validator per unique schema)
8
9use crate::validation::SchemaValidator;
10use serde_json::Value;
11use std::collections::HashMap;
12use std::sync::{Arc, RwLock};
13
14/// Global schema registry for compiled validators
15///
16/// Thread-safe registry that ensures each unique schema is compiled exactly once.
17/// Uses RwLock for concurrent read access with occasional writes during startup.
18pub struct SchemaRegistry {
19    /// Map from schema JSON string to compiled validator
20    schemas: RwLock<HashMap<String, Arc<SchemaValidator>>>,
21}
22
23impl SchemaRegistry {
24    /// Create a new empty schema registry
25    pub fn new() -> Self {
26        Self {
27            schemas: RwLock::new(HashMap::new()),
28        }
29    }
30
31    /// Get or compile a schema, returning Arc to the compiled validator
32    ///
33    /// This method is thread-safe and uses a double-check pattern:
34    /// 1. Fast path: Read lock to check if schema exists
35    /// 2. Slow path: Write lock to compile and store new schema
36    ///
37    /// # Arguments
38    /// * `schema` - The JSON schema to compile
39    ///
40    /// # Returns
41    /// Arc-wrapped compiled validator that can be cheaply cloned
42    pub fn get_or_compile(&self, schema: &Value) -> Result<Arc<SchemaValidator>, String> {
43        let key = serde_json::to_string(schema).map_err(|e| format!("Failed to serialize schema: {}", e))?;
44
45        {
46            let schemas = self.schemas.read().unwrap();
47            if let Some(validator) = schemas.get(&key) {
48                return Ok(Arc::clone(validator));
49            }
50        }
51
52        let validator = Arc::new(SchemaValidator::new(schema.clone())?);
53
54        {
55            let mut schemas = self.schemas.write().unwrap();
56            if let Some(existing) = schemas.get(&key) {
57                return Ok(Arc::clone(existing));
58            }
59            schemas.insert(key, Arc::clone(&validator));
60        }
61
62        Ok(validator)
63    }
64
65    /// Get all registered schemas (for OpenAPI generation)
66    ///
67    /// Returns a snapshot of all compiled validators.
68    /// Useful for generating OpenAPI specifications from runtime schema information.
69    pub fn all_schemas(&self) -> Vec<Arc<SchemaValidator>> {
70        let schemas = self.schemas.read().unwrap();
71        schemas.values().cloned().collect()
72    }
73
74    /// Get the number of unique schemas registered
75    ///
76    /// Useful for diagnostics and understanding schema deduplication effectiveness.
77    pub fn schema_count(&self) -> usize {
78        let schemas = self.schemas.read().unwrap();
79        schemas.len()
80    }
81}
82
83impl Default for SchemaRegistry {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use serde_json::json;
93
94    #[test]
95    fn test_schema_deduplication() {
96        let registry = SchemaRegistry::new();
97
98        let schema1 = json!({
99            "type": "object",
100            "properties": {
101                "name": {"type": "string"}
102            }
103        });
104
105        let schema2 = json!({
106            "type": "object",
107            "properties": {
108                "name": {"type": "string"}
109            }
110        });
111
112        let validator1 = registry.get_or_compile(&schema1).unwrap();
113        let validator2 = registry.get_or_compile(&schema2).unwrap();
114
115        assert!(Arc::ptr_eq(&validator1, &validator2));
116
117        assert_eq!(registry.schema_count(), 1);
118    }
119
120    #[test]
121    fn test_different_schemas() {
122        let registry = SchemaRegistry::new();
123
124        let schema1 = json!({
125            "type": "string"
126        });
127
128        let schema2 = json!({
129            "type": "integer"
130        });
131
132        let validator1 = registry.get_or_compile(&schema1).unwrap();
133        let validator2 = registry.get_or_compile(&schema2).unwrap();
134
135        assert!(!Arc::ptr_eq(&validator1, &validator2));
136
137        assert_eq!(registry.schema_count(), 2);
138    }
139
140    #[test]
141    fn test_all_schemas() {
142        let registry = SchemaRegistry::new();
143
144        let schema1 = json!({"type": "string"});
145        let schema2 = json!({"type": "integer"});
146
147        registry.get_or_compile(&schema1).unwrap();
148        registry.get_or_compile(&schema2).unwrap();
149
150        let all = registry.all_schemas();
151        assert_eq!(all.len(), 2);
152    }
153
154    #[test]
155    fn test_concurrent_access() {
156        use std::sync::Arc as StdArc;
157        use std::thread;
158
159        let registry = StdArc::new(SchemaRegistry::new());
160        let schema = json!({
161            "type": "object",
162            "properties": {
163                "id": {"type": "integer"}
164            }
165        });
166
167        let handles: Vec<_> = (0..10)
168            .map(|_| {
169                let registry = StdArc::clone(&registry);
170                let schema = schema.clone();
171                thread::spawn(move || registry.get_or_compile(&schema).unwrap())
172            })
173            .collect();
174
175        let validators: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
176
177        for i in 1..validators.len() {
178            assert!(Arc::ptr_eq(&validators[0], &validators[i]));
179        }
180
181        assert_eq!(registry.schema_count(), 1);
182    }
183}