use crate::error::{Result, XervError};
use std::collections::HashMap;
use std::sync::RwLock;
#[derive(Debug, Clone)]
pub struct FieldInfo {
pub name: String,
pub type_name: String,
pub offset: usize,
pub size: usize,
pub optional: bool,
}
impl FieldInfo {
pub fn new(name: impl Into<String>, type_name: impl Into<String>) -> Self {
Self {
name: name.into(),
type_name: type_name.into(),
offset: 0,
size: 0,
optional: false,
}
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
pub fn with_size(mut self, size: usize) -> Self {
self.size = size;
self
}
pub fn optional(mut self) -> Self {
self.optional = true;
self
}
}
#[derive(Debug, Clone)]
pub struct TypeInfo {
pub name: String,
pub short_name: String,
pub version: u32,
pub hash: u64,
pub size: usize,
pub alignment: usize,
pub fields: Vec<FieldInfo>,
pub stable_layout: bool,
}
impl TypeInfo {
pub fn new(name: impl Into<String>, version: u32) -> Self {
let short_name = name.into();
let full_name = format!("{}@v{}", short_name, version);
Self {
name: full_name,
short_name,
version,
hash: 0,
size: 0,
alignment: 8,
fields: Vec::new(),
stable_layout: false,
}
}
pub fn with_hash(mut self, hash: u64) -> Self {
self.hash = hash;
self
}
pub fn with_size(mut self, size: usize) -> Self {
self.size = size;
self
}
pub fn with_alignment(mut self, alignment: usize) -> Self {
self.alignment = alignment;
self
}
pub fn with_field(mut self, field: FieldInfo) -> Self {
self.fields.push(field);
self
}
pub fn with_fields(mut self, fields: Vec<FieldInfo>) -> Self {
self.fields = fields;
self
}
pub fn stable(mut self) -> Self {
self.stable_layout = true;
self
}
pub fn get_field(&self, name: &str) -> Option<&FieldInfo> {
self.fields.iter().find(|f| f.name == name)
}
pub fn is_compatible_with(&self, other: &TypeInfo) -> bool {
if self.hash != 0 && self.hash == other.hash {
return true;
}
for field in &other.fields {
if field.optional {
continue;
}
match self.get_field(&field.name) {
Some(our_field) => {
if our_field.type_name != field.type_name {
return false;
}
}
None => return false,
}
}
true
}
}
pub trait Schema {
fn type_info() -> TypeInfo;
fn schema_hash() -> u64 {
Self::type_info().hash
}
fn validate_layout() -> Result<()> {
let info = Self::type_info();
if !info.stable_layout {
return Err(XervError::NonDeterministicLayout {
type_name: info.name,
cause: "Type must use #[repr(C)] for stable memory layout".to_string(),
});
}
Ok(())
}
}
pub struct SchemaRegistry {
schemas: RwLock<HashMap<String, TypeInfo>>,
}
impl SchemaRegistry {
pub fn new() -> Self {
Self {
schemas: RwLock::new(HashMap::new()),
}
}
pub fn register(&self, info: TypeInfo) {
let mut schemas = self.schemas.write().unwrap();
schemas.insert(info.name.clone(), info);
}
pub fn get(&self, name: &str) -> Option<TypeInfo> {
let schemas = self.schemas.read().unwrap();
schemas.get(name).cloned()
}
pub fn get_by_hash(&self, hash: u64) -> Option<TypeInfo> {
let schemas = self.schemas.read().unwrap();
schemas.values().find(|t| t.hash == hash).cloned()
}
pub fn contains(&self, name: &str) -> bool {
let schemas = self.schemas.read().unwrap();
schemas.contains_key(name)
}
pub fn names(&self) -> Vec<String> {
let schemas = self.schemas.read().unwrap();
schemas.keys().cloned().collect()
}
pub fn check_compatibility(&self, from: &str, to: &str) -> bool {
let schemas = self.schemas.read().unwrap();
match (schemas.get(from), schemas.get(to)) {
(Some(from_info), Some(to_info)) => from_info.is_compatible_with(to_info),
_ => false,
}
}
}
impl Default for SchemaRegistry {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
static GLOBAL_REGISTRY: std::sync::OnceLock<SchemaRegistry> = std::sync::OnceLock::new();
#[allow(dead_code)]
pub fn global_registry() -> &'static SchemaRegistry {
GLOBAL_REGISTRY.get_or_init(SchemaRegistry::new)
}
#[allow(dead_code)]
pub fn register_schema(info: TypeInfo) {
global_registry().register(info);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_info_creation() {
let info = TypeInfo::new("OrderInput", 1)
.with_hash(0x12345678)
.with_size(64)
.with_fields(vec![
FieldInfo::new("order_id", "String")
.with_offset(0)
.with_size(24),
FieldInfo::new("amount", "f64").with_offset(24).with_size(8),
])
.stable();
assert_eq!(info.name, "OrderInput@v1");
assert_eq!(info.short_name, "OrderInput");
assert_eq!(info.version, 1);
assert_eq!(info.fields.len(), 2);
assert!(info.stable_layout);
}
#[test]
fn schema_registry() {
let registry = SchemaRegistry::new();
let info1 = TypeInfo::new("TestType", 1).with_hash(111);
let info2 = TypeInfo::new("TestType", 2).with_hash(222);
registry.register(info1);
registry.register(info2);
assert!(registry.contains("TestType@v1"));
assert!(registry.contains("TestType@v2"));
assert!(!registry.contains("TestType@v3"));
let retrieved = registry.get("TestType@v1").unwrap();
assert_eq!(retrieved.hash, 111);
}
#[test]
fn schema_compatibility() {
let v1 = TypeInfo::new("Order", 1).with_fields(vec![
FieldInfo::new("id", "String"),
FieldInfo::new("amount", "f64"),
]);
let v2_compatible = TypeInfo::new("Order", 2).with_fields(vec![
FieldInfo::new("id", "String"),
FieldInfo::new("amount", "f64"),
FieldInfo::new("notes", "String").optional(),
]);
assert!(v2_compatible.is_compatible_with(&v1));
let v3_incompatible =
TypeInfo::new("Order", 3).with_fields(vec![FieldInfo::new("id", "String")]);
assert!(!v3_incompatible.is_compatible_with(&v1));
}
}