# ParsedSchemaCache Documentation
## Overview
The `ParsedSchemaCache` is a built-in, thread-safe cache for storing and reusing `Arc<ParsedSchema>` instances. This enables high-performance scenarios where schemas are parsed once and reused across multiple evaluations.
## Features
- ✅ **Thread-safe**: Uses `Arc<RwLock<>>` for concurrent access
- ✅ **Caller-controlled**: You decide when to parse, cache, clear, and release memory
- ✅ **Flexible keys**: Use any string identifier for your cached schemas
- ✅ **Arc-based**: Cheap cloning via `Arc` reference counting
- ✅ **Global or local**: Use the built-in global cache or create your own instances
- ✅ **Zero-copy**: Cached schemas share memory across evaluations
## Use Cases
### 1. **Web Servers**
Parse schemas once at startup, cache them, and reuse for every request.
### 2. **Batch Processing**
Parse schema once, evaluate against thousands of data files.
### 3. **Multi-tenant Applications**
Cache schemas per tenant, reload when tenant schema changes.
### 4. **Schema Versioning**
Cache multiple versions of a schema simultaneously.
## API Reference
### Creating a Cache
```rust
use json_eval_rs::ParsedSchemaCache;
// Create a local cache instance
let cache = ParsedSchemaCache::new();
// Or use the global cache
use json_eval_rs::PARSED_SCHEMA_CACHE;
```
### Basic Operations
#### Insert
```rust
use json_eval_rs::{ParsedSchema, ParsedSchemaCache};
use std::sync::Arc;
let cache = ParsedSchemaCache::new();
let parsed = ParsedSchema::parse(schema_json)?;
cache.insert("my-schema".to_string(), Arc::new(parsed));
```
#### Get
```rust
if let Some(cached) = cache.get("my-schema") {
// Use cached schema
let mut eval = JSONEval::with_parsed_schema(cached, Some(context), None)?;
}
```
#### Remove
```rust
// Remove and return the schema
if let Some(removed) = cache.remove("my-schema") {
println!("Removed schema");
}
```
#### Clear
```rust
// Remove all cached schemas
cache.clear();
```
### Advanced Operations
#### Check Existence
```rust
if cache.contains_key("my-schema") {
println!("Schema exists in cache");
}
```
#### Get Cache Size
```rust
println!("Cache has {} entries", cache.len());
println!("Cache is empty: {}", cache.is_empty());
```
#### List All Keys
```rust
let keys = cache.keys();
for key in keys {
println!("Cached schema: {}", key);
}
```
#### Get Statistics
```rust
let stats = cache.stats();
println!("{}", stats); // "ParsedSchemaCache: 3 entries (keys: schema1, schema2, schema3)"
```
#### Lazy Insertion
```rust
// Parse only if not already cached
let schema = cache.get_or_insert_with("my-schema", || {
Arc::new(ParsedSchema::parse(schema_json).unwrap())
});
```
#### Batch Operations
```rust
// Insert multiple schemas at once
cache.insert_batch(vec![
("schema1".to_string(), Arc::new(parsed1)),
("schema2".to_string(), Arc::new(parsed2)),
]);
// Remove multiple schemas at once
let removed = cache.remove_batch(&["schema1".to_string(), "schema2".to_string()]);
```
## Usage Patterns
### Pattern 1: Application-Level Cache
```rust
use json_eval_rs::ParsedSchemaCache;
use std::sync::Arc;
struct MyApp {
schema_cache: ParsedSchemaCache,
}
impl MyApp {
fn new() -> Self {
Self {
schema_cache: ParsedSchemaCache::new(),
}
}
fn load_schema(&self, key: &str, json: &str) -> Result<(), String> {
let parsed = ParsedSchema::parse(json)?;
self.schema_cache.insert(key.to_string(), Arc::new(parsed));
Ok(())
}
fn evaluate(&self, schema_key: &str, data: &str) -> Result<String, String> {
let cached = self.schema_cache.get(schema_key)
.ok_or("Schema not found in cache")?;
let mut eval = JSONEval::with_parsed_schema(cached, None, None)?;
eval.evaluate(data, None)?;
Ok(serde_json::to_string(&eval.get_evaluated_schema(false)).unwrap())
}
}
```
### Pattern 2: Global Cache for Simple Applications
```rust
use json_eval_rs::PARSED_SCHEMA_CACHE;
use std::sync::Arc;
// At application startup
fn init() {
let schema1 = ParsedSchema::parse(schema_json1).unwrap();
PARSED_SCHEMA_CACHE.insert("v1".to_string(), Arc::new(schema1));
let schema2 = ParsedSchema::parse(schema_json2).unwrap();
PARSED_SCHEMA_CACHE.insert("v2".to_string(), Arc::new(schema2));
}
// Anywhere in your application
fn process(version: &str, data: &str) -> Result<(), String> {
let cached = PARSED_SCHEMA_CACHE.get(version)
.ok_or("Schema version not found")?;
let mut eval = JSONEval::with_parsed_schema(cached, None, None)?;
eval.evaluate(data, None)?;
Ok(())
}
```
### Pattern 3: Multi-Tenant Schema Management
```rust
use json_eval_rs::ParsedSchemaCache;
struct TenantSchemaManager {
cache: ParsedSchemaCache,
}
impl TenantSchemaManager {
fn new() -> Self {
Self {
cache: ParsedSchemaCache::new(),
}
}
fn load_tenant_schema(&self, tenant_id: &str, schema_json: &str) -> Result<(), String> {
let key = format!("tenant:{}", tenant_id);
let parsed = ParsedSchema::parse(schema_json)?;
self.cache.insert(key, Arc::new(parsed));
Ok(())
}
fn reload_tenant_schema(&self, tenant_id: &str, schema_json: &str) -> Result<(), String> {
let key = format!("tenant:{}", tenant_id);
// Remove old schema
self.cache.remove(&key);
// Parse and cache new schema
let parsed = ParsedSchema::parse(schema_json)?;
self.cache.insert(key, Arc::new(parsed));
Ok(())
}
fn process_tenant_data(&self, tenant_id: &str, data: &str) -> Result<String, String> {
let key = format!("tenant:{}", tenant_id);
let cached = self.cache.get(&key)
.ok_or("Tenant schema not found")?;
let mut eval = JSONEval::with_parsed_schema(cached, None, None)?;
eval.evaluate(data, None)?;
Ok(serde_json::to_string(&eval.get_evaluated_schema(false)).unwrap())
}
fn remove_tenant(&self, tenant_id: &str) {
let key = format!("tenant:{}", tenant_id);
self.cache.remove(&key);
}
}
```
### Pattern 4: Schema Versioning
```rust
use json_eval_rs::ParsedSchemaCache;
struct VersionedSchemaCache {
cache: ParsedSchemaCache,
}
impl VersionedSchemaCache {
fn new() -> Self {
Self {
cache: ParsedSchemaCache::new(),
}
}
fn load_version(&self, version: &str, schema_json: &str) -> Result<(), String> {
let key = format!("v{}", version);
let parsed = ParsedSchema::parse(schema_json)?;
self.cache.insert(key, Arc::new(parsed));
Ok(())
}
fn evaluate_with_version(&self, version: &str, data: &str) -> Result<String, String> {
let key = format!("v{}", version);
let cached = self.cache.get(&key)
.ok_or(format!("Schema version {} not found", version))?;
let mut eval = JSONEval::with_parsed_schema(cached, None, None)?;
eval.evaluate(data, None)?;
Ok(serde_json::to_string(&eval.get_evaluated_schema(false)).unwrap())
}
fn list_versions(&self) -> Vec<String> {
self.cache.keys()
.into_iter()
.filter_map(|k| k.strip_prefix("v").map(String::from))
.collect()
}
}
```
## Performance Characteristics
### Memory
- **Cache overhead**: `O(n)` where n is the number of cached schemas
- **Per-schema**: Shared via `Arc`, minimal overhead per clone
- **Thread safety**: `RwLock` allows multiple concurrent readers
### Speed
- **Insert/Remove**: `O(1)` average case (HashMap under IndexMap)
- **Get**: `O(1)` average case, cheap Arc clone
- **Clear**: `O(n)` where n is the number of entries
### Benchmarks (from example)
Testing with 100 iterations:
- **Without cache**: ~118μs per iteration (parse + evaluate each time)
- **With cache**: ~54μs per iteration (parse once, evaluate many)
- **Speedup**: ~2.15x faster
Real-world improvements can be much higher (10x-100x) for complex schemas.
## Thread Safety
The cache is fully thread-safe and can be used across multiple threads:
```rust
use json_eval_rs::ParsedSchemaCache;
use std::sync::Arc;
use std::thread;
let cache = Arc::new(ParsedSchemaCache::new());
// Thread 1: Insert
let cache1 = cache.clone();
cache1.insert("shared".to_string(), Arc::new(parsed));
});
// Thread 2: Read
let cache2 = cache.clone();
// Use cached schema
}
});
```
## Best Practices
### 1. **Choose the Right Cache**
- Use **global cache** (`PARSED_SCHEMA_CACHE`) for simple applications
- Use **local cache** for better control and testing
### 2. **Key Naming Conventions**
```rust
// Good: Descriptive, namespaced keys
cache.insert("tenant:123:v2".to_string(), schema);
cache.insert("calculation-engine:v1".to_string(), schema);
// Avoid: Generic keys that may conflict
cache.insert("schema".to_string(), schema);
cache.insert("1".to_string(), schema);
```
### 3. **Memory Management**
```rust
// Remove unused schemas to free memory
cache.remove("old-schema");
// Clear cache when doing bulk reloads
cache.clear();
for (key, schema) in new_schemas {
cache.insert(key, schema);
}
```
### 4. **Error Handling**
```rust
// Always handle missing schemas gracefully
let schema = cache.get("my-schema")
.ok_or("Schema not found. Please load it first.")?;
```
### 5. **Lazy Loading**
```rust
// Use get_or_insert_with to avoid redundant parsing
let schema = cache.get_or_insert_with("my-schema", || {
// Only executes if not already cached
Arc::new(ParsedSchema::parse(schema_json).unwrap())
});
```
## Examples
See `examples/cache_demo.rs` for a comprehensive demonstration of all cache features:
```bash
cargo run --example cache_demo
```
This example demonstrates:
1. Local cache instance usage
2. Global cache usage
3. Performance comparison (cached vs non-cached)
4. Lazy insertion pattern
## Integration with Existing Code
### Before (No Cache)
```rust
fn evaluate_data(schema_json: &str, data: &str) -> Result<String, String> {
let mut eval = JSONEval::new(schema_json, None, Some(data))?;
eval.evaluate(data, None)?;
Ok(serde_json::to_string(&eval.get_evaluated_schema(false)).unwrap())
}
```
### After (With Cache)
```rust
fn evaluate_data(
cache: &ParsedSchemaCache,
schema_key: &str,
data: &str
) -> Result<String, String> {
let cached = cache.get(schema_key)
.ok_or("Schema not found in cache")?;
let mut eval = JSONEval::with_parsed_schema(cached, None, None)?;
eval.evaluate(data, None)?;
Ok(serde_json::to_string(&eval.get_evaluated_schema(false)).unwrap())
}
// One-time initialization
let cache = ParsedSchemaCache::new();
let parsed = ParsedSchema::parse(schema_json)?;
cache.insert("my-schema".to_string(), Arc::new(parsed));
// Many evaluations (reuses parsed schema)
for data_file in data_files {
let result = evaluate_data(&cache, "my-schema", &data_file)?;
}
```
## Troubleshooting
### Q: Why is my cache growing indefinitely?
**A**: You're not removing old entries. Call `cache.remove()` or `cache.clear()` when schemas are no longer needed.
### Q: Can I share the cache between async tasks?
**A**: Yes! The cache is thread-safe. Wrap it in `Arc` if needed:
```rust
let cache = Arc::new(ParsedSchemaCache::new());
```
### Q: How do I know what's in the cache?
**A**: Use `cache.stats()` or `cache.keys()` to inspect cache contents.
### Q: Should I use the global cache or create my own?
**A**:
- Global cache: Simple applications, prototypes, scripts
- Local cache: Libraries, applications with complex lifecycle management, testing
### Q: What happens if I insert with the same key twice?
**A**: The old value is replaced and returned from `insert()`.
## Changelog
### v0.0.11
- ✅ Initial release of `ParsedSchemaCache`
- ✅ Thread-safe Arc-based caching
- ✅ Global `PARSED_SCHEMA_CACHE` instance
- ✅ Comprehensive API with batch operations
- ✅ Full documentation and examples