use std::{
collections::HashMap,
io::{BufRead, BufReader, Write},
path::Path,
};
use blake3;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{Codec, Error, Layout, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaEntry {
pub schema_id: String,
pub schema: Value,
pub count: u64,
}
#[derive(Debug, Clone)]
pub struct SchemaRegistry {
schemas: HashMap<String, SchemaEntry>,
strict: bool,
strict_schema_id: Option<String>,
}
impl SchemaRegistry {
pub fn new(strict: bool) -> Self {
SchemaRegistry {
schemas: HashMap::new(),
strict,
strict_schema_id: None,
}
}
pub fn load(root: &Path, collection: &str) -> Result<Self> {
let path = Layout::schema_registry(root, collection);
let manifest_path = Layout::manifest_file(root, collection);
let strict = if manifest_path.exists() {
let content = std::fs::read_to_string(&manifest_path)?;
let manifest: Value = serde_json::from_str(&content)?;
manifest
.get("strict")
.and_then(|v| v.as_bool())
.unwrap_or(false)
} else {
false
};
let mut registry = SchemaRegistry::new(strict);
if path.exists() {
let file = std::fs::File::open(&path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let entry: SchemaEntry = serde_json::from_str(&line)?;
if registry.strict && registry.strict_schema_id.is_none() {
registry.strict_schema_id = Some(entry.schema_id.clone());
}
registry.schemas.insert(entry.schema_id.clone(), entry);
}
}
Ok(registry)
}
pub fn save(&self, root: &Path, collection: &str) -> Result<()> {
let path = Layout::schema_registry(root, collection);
let mut file = std::fs::File::create(&path)?;
for entry in self.schemas.values() {
let line = serde_json::to_string(entry)?;
writeln!(file, "{}", line)?;
}
Ok(())
}
pub fn compute_schema_id(doc: &Value) -> String {
let schema = Self::extract_schema(doc);
let canonical = Codec::canonicalize(&schema);
let hash = blake3::hash(canonical.as_bytes());
hash.to_hex().to_string()
}
pub fn extract_schema(doc: &Value) -> Value {
match doc {
Value::Object(map) => {
let mut schema_map = serde_json::Map::new();
for (k, v) in map {
schema_map.insert(k.clone(), Self::extract_schema(v));
}
Value::Object(schema_map)
}
Value::Array(arr) => {
if let Some(first) = arr.first() {
Value::Array(vec![Self::extract_schema(first)])
} else {
Value::Array(vec![])
}
}
Value::String(_) => Value::String("string".to_string()),
Value::Number(n) => {
if n.is_i64() {
Value::String("integer".to_string())
} else {
Value::String("number".to_string())
}
}
Value::Bool(_) => Value::String("boolean".to_string()),
Value::Null => Value::String("null".to_string()),
}
}
pub fn register(&mut self, doc: &Value) -> Result<String> {
let schema_id = Self::compute_schema_id(doc);
if self.strict {
if let Some(ref expected) = self.strict_schema_id {
if &schema_id != expected {
return Err(Error::SchemaMismatch {
expected: expected.clone(),
actual: schema_id,
});
}
} else {
self.strict_schema_id = Some(schema_id.clone());
}
}
if let Some(entry) = self.schemas.get_mut(&schema_id) {
entry.count += 1;
} else {
let schema = Self::extract_schema(doc);
self.schemas.insert(
schema_id.clone(),
SchemaEntry {
schema_id: schema_id.clone(),
schema,
count: 1,
},
);
}
Ok(schema_id)
}
pub fn unregister(&mut self, schema_id: &str) {
if let Some(entry) = self.schemas.get_mut(schema_id) {
entry.count = entry.count.saturating_sub(1);
}
}
pub fn schemas(&self) -> impl Iterator<Item = &SchemaEntry> {
self.schemas.values()
}
pub fn get(&self, schema_id: &str) -> Option<&SchemaEntry> {
self.schemas.get(schema_id)
}
pub fn is_strict(&self) -> bool {
self.strict
}
pub fn strict_schema_id(&self) -> Option<&str> {
self.strict_schema_id.as_deref()
}
pub fn schema_count(&self) -> usize {
self.schemas.len()
}
pub fn total_doc_count(&self) -> u64 {
self.schemas.values().map(|e| e.count).sum()
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_compute_schema_id() {
let doc1 = json!({"name": "alice", "age": 30});
let doc2 = json!({"name": "bob", "age": 25});
let doc3 = json!({"name": "charlie"});
let id1 = SchemaRegistry::compute_schema_id(&doc1);
let id2 = SchemaRegistry::compute_schema_id(&doc2);
let id3 = SchemaRegistry::compute_schema_id(&doc3);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_extract_schema() {
let doc = json!({
"name": "alice",
"age": 30,
"active": true,
"tags": ["a", "b"],
"meta": {"x": 1}
});
let schema = SchemaRegistry::extract_schema(&doc);
assert_eq!(schema["name"], "string");
assert_eq!(schema["age"], "integer");
assert_eq!(schema["active"], "boolean");
}
#[test]
fn test_flexible_mode() {
let mut registry = SchemaRegistry::new(false);
let doc1 = json!({"a": 1});
let doc2 = json!({"b": 2});
registry.register(&doc1).unwrap();
registry.register(&doc2).unwrap();
assert_eq!(registry.schema_count(), 2);
}
#[test]
fn test_strict_mode() {
let mut registry = SchemaRegistry::new(true);
let doc1 = json!({"name": "alice"});
let doc2 = json!({"name": "bob"});
let doc3 = json!({"different": "schema"});
registry.register(&doc1).unwrap();
registry.register(&doc2).unwrap();
assert!(registry.register(&doc3).is_err());
}
}