1use std::collections::HashMap;
29use std::hash::{DefaultHasher, Hash, Hasher};
30use std::sync::{Arc, Mutex};
31
32use serde_json::Value;
33
34use crate::{GenericSchemaAdapter, SchemaAdapter};
35
36#[derive(Debug)]
53pub struct SchemaCache {
54 adapter: Arc<dyn SchemaAdapter>,
55 entries: Mutex<HashMap<CacheKey, Value>>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59enum CacheKey {
60 Bound(u64),
61 Legacy { schema: u64, normalized: u64 },
62}
63
64fn hash_canonical(value: &Value, hasher: &mut DefaultHasher) {
73 match value {
74 Value::Null => 0u8.hash(hasher),
75 Value::Bool(flag) => {
76 1u8.hash(hasher);
77 flag.hash(hasher);
78 }
79 Value::Number(number) => {
80 2u8.hash(hasher);
81 number.to_string().hash(hasher);
87 }
88 Value::String(text) => {
89 3u8.hash(hasher);
90 text.hash(hasher);
91 }
92 Value::Array(items) => {
93 4u8.hash(hasher);
94 items.len().hash(hasher);
95 for item in items {
96 hash_canonical(item, hasher);
97 }
98 }
99 Value::Object(members) => {
100 5u8.hash(hasher);
101 members.len().hash(hasher);
102 let mut entries: Vec<(&String, &Value)> = members.iter().collect();
103 entries.sort_unstable_by_key(|(key, _)| *key);
104 for (key, member) in entries {
105 key.hash(hasher);
106 hash_canonical(member, hasher);
107 }
108 }
109 }
110}
111
112impl SchemaCache {
113 pub fn new() -> Self {
128 Self::for_adapter(Arc::new(GenericSchemaAdapter))
129 }
130
131 pub fn for_adapter(adapter: Arc<dyn SchemaAdapter>) -> Self {
146 Self { adapter, entries: Mutex::new(HashMap::new()) }
147 }
148
149 pub fn normalize(&self, schema: &Value) -> Value {
169 let hash = Self::hash_schema(schema);
170 let mut cache = self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
171 cache
172 .entry(CacheKey::Bound(hash))
173 .or_insert_with(|| self.adapter.normalize_schema(schema.clone()))
174 .clone()
175 }
176
177 #[deprecated(
184 note = "bind the adapter with SchemaCache::for_adapter and call SchemaCache::normalize"
185 )]
186 pub fn get_or_normalize(&self, schema: &Value, adapter: &dyn SchemaAdapter) -> Value {
187 let schema_hash = Self::hash_schema(schema);
188 let normalized = adapter.normalize_schema(schema.clone());
189 let key =
190 CacheKey::Legacy { schema: schema_hash, normalized: Self::hash_schema(&normalized) };
191 let mut cache = self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
192 cache.entry(key).or_insert(normalized).clone()
193 }
194
195 pub fn clear(&self) {
217 let mut cache = self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
218 cache.clear();
219 }
220
221 pub fn len(&self) -> usize {
223 let cache = self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
224 cache.len()
225 }
226
227 pub fn is_empty(&self) -> bool {
229 self.len() == 0
230 }
231
232 fn hash_schema(schema: &Value) -> u64 {
242 let mut hasher = DefaultHasher::new();
243 hash_canonical(schema, &mut hasher);
244 hasher.finish()
245 }
246}
247
248impl Default for SchemaCache {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use serde_json::json;
258 use std::sync::atomic::{AtomicUsize, Ordering};
259
260 use crate::GenericSchemaAdapter;
261
262 fn generic_cache() -> SchemaCache {
263 SchemaCache::for_adapter(Arc::new(GenericSchemaAdapter))
264 }
265
266 #[derive(Debug)]
267 struct TaggedAdapter(&'static str);
268
269 impl SchemaAdapter for TaggedAdapter {
270 fn normalize_schema(&self, mut schema: Value) -> Value {
271 schema
272 .as_object_mut()
273 .expect("test schema should be an object")
274 .insert("normalized_by".to_string(), Value::String(self.0.to_string()));
275 schema
276 }
277 }
278
279 #[derive(Debug)]
280 struct CountingAdapter(Arc<AtomicUsize>);
281
282 impl SchemaAdapter for CountingAdapter {
283 fn normalize_schema(&self, schema: Value) -> Value {
284 self.0.fetch_add(1, Ordering::Relaxed);
285 schema
286 }
287 }
288
289 #[test]
290 fn test_cache_returns_normalized_schema() {
291 let cache = generic_cache();
292 let schema = json!({
293 "$schema": "http://json-schema.org/draft-07/schema#",
294 "type": "object",
295 "properties": { "name": { "type": "string" } }
296 });
297
298 let result = cache.normalize(&schema);
299 assert!(result.get("$schema").is_none());
300 assert_eq!(result["type"], "object");
301 }
302
303 #[test]
304 fn test_cache_returns_same_result_on_repeated_calls() {
305 let cache = generic_cache();
306 let schema = json!({
307 "type": "object",
308 "properties": { "x": { "type": "integer", "const": 42 } }
309 });
310
311 let first = cache.normalize(&schema);
312 let second = cache.normalize(&schema);
313 assert_eq!(first, second);
314 }
315
316 #[test]
317 fn repeated_calls_only_invoke_the_bound_adapter_once() {
318 let calls = Arc::new(AtomicUsize::new(0));
319 let cache = SchemaCache::for_adapter(Arc::new(CountingAdapter(Arc::clone(&calls))));
320 let schema = json!({"type": "string"});
321
322 cache.normalize(&schema);
323 cache.normalize(&schema);
324
325 assert_eq!(calls.load(Ordering::Relaxed), 1);
326 }
327
328 #[test]
329 fn adapter_instances_cannot_share_entries() {
330 let schema = json!({"type": "object"});
331 let alpha = SchemaCache::for_adapter(Arc::new(TaggedAdapter("alpha")));
332 let beta = SchemaCache::for_adapter(Arc::new(TaggedAdapter("beta")));
333
334 assert_eq!(alpha.normalize(&schema)["normalized_by"], "alpha");
335 assert_eq!(beta.normalize(&schema)["normalized_by"], "beta");
336 assert_eq!(alpha.len(), 1);
337 assert_eq!(beta.len(), 1);
338 }
339
340 #[test]
341 #[allow(deprecated)]
342 fn deprecated_api_keeps_adapter_results_separate() {
343 let cache = SchemaCache::new();
344 let schema = json!({"type": "object"});
345
346 let alpha = cache.get_or_normalize(&schema, &TaggedAdapter("alpha"));
347 let beta = cache.get_or_normalize(&schema, &TaggedAdapter("beta"));
348
349 assert_eq!(alpha["normalized_by"], "alpha");
350 assert_eq!(beta["normalized_by"], "beta");
351 assert_eq!(cache.len(), 2);
352 }
353
354 #[test]
355 fn test_cache_stores_entries() {
356 let cache = generic_cache();
357
358 assert!(cache.is_empty());
359 assert_eq!(cache.len(), 0);
360
361 let schema1 = json!({"type": "string"});
362 let schema2 = json!({"type": "number"});
363
364 cache.normalize(&schema1);
365 assert_eq!(cache.len(), 1);
366
367 cache.normalize(&schema2);
368 assert_eq!(cache.len(), 2);
369
370 cache.normalize(&schema1);
372 assert_eq!(cache.len(), 2);
373 }
374
375 #[test]
376 fn test_cache_clear_removes_all_entries() {
377 let cache = generic_cache();
378
379 cache.normalize(&json!({"type": "string"}));
380 cache.normalize(&json!({"type": "number"}));
381 assert_eq!(cache.len(), 2);
382
383 cache.clear();
384 assert!(cache.is_empty());
385 }
386
387 #[test]
388 fn test_cache_different_schemas_produce_different_entries() {
389 let cache = generic_cache();
390
391 let schema_a = json!({"type": "string", "format": "hostname"});
392 let schema_b = json!({"type": "string", "format": "email"});
393
394 let result_a = cache.normalize(&schema_a);
395 let result_b = cache.normalize(&schema_b);
396
397 assert!(result_a.get("format").is_none());
399 assert_eq!(result_b["format"], "email");
400 assert_eq!(cache.len(), 2);
401 }
402
403 #[test]
404 fn test_cache_new_is_empty() {
405 let cache = SchemaCache::new();
406 assert!(cache.is_empty());
407 assert_eq!(cache.len(), 0);
408 }
409
410 #[test]
411 fn test_cache_default_is_empty() {
412 let cache = SchemaCache::default();
413 assert!(cache.is_empty());
414 }
415
416 #[test]
417 fn test_cache_handles_empty_schema() {
418 let cache = generic_cache();
419 let schema = json!({});
420
421 let result = cache.normalize(&schema);
422 assert_eq!(result, json!({}));
423 assert_eq!(cache.len(), 1);
424 }
425
426 #[test]
427 fn test_cache_handles_null_schema() {
428 let cache = generic_cache();
429 let schema = Value::Null;
430
431 let result = cache.normalize(&schema);
432 assert_eq!(result, Value::Null);
434 assert_eq!(cache.len(), 1);
435 }
436
437 #[test]
440 fn key_order_does_not_create_a_second_entry() {
441 let cache = generic_cache();
442
443 cache.normalize(&json!({ "type": "object", "properties": { "a": {}, "b": {} } }));
444 cache.normalize(&json!({ "properties": { "b": {}, "a": {} }, "type": "object" }));
445
446 assert_eq!(cache.len(), 1, "key order must not change a schema's identity");
447 }
448
449 #[test]
450 fn genuinely_different_schemas_keep_separate_entries() {
451 let cache = generic_cache();
452
453 cache.normalize(&json!({ "type": "string" }));
454 cache.normalize(&json!({ "type": "integer" }));
455
456 assert_eq!(cache.len(), 2);
457 }
458
459 #[test]
462 fn unusual_property_names_stay_distinct() {
463 let cache = generic_cache();
464
465 cache.normalize(&json!({ "properties": { "a/b": {} } }));
466 cache.normalize(&json!({ "properties": { "a~b": {} } }));
467
468 assert_eq!(cache.len(), 2);
469 }
470
471 #[test]
473 fn a_string_and_a_number_hash_differently() {
474 let cache = generic_cache();
475
476 cache.normalize(&json!({ "const": "1" }));
477 cache.normalize(&json!({ "const": 1 }));
478
479 assert_eq!(cache.len(), 2);
480 }
481
482 #[test]
492 fn numbers_are_identified_by_their_written_form() {
493 let cache = generic_cache();
494
495 cache.normalize(&json!({ "const": 5 }));
496 cache.normalize(&json!({ "const": 5.0 }));
497
498 assert_eq!(cache.len(), 2);
499 }
500}