xerv_core/traits/
schema.rs1use crate::error::{Result, XervError};
4use std::collections::HashMap;
5use std::sync::RwLock;
6
7#[derive(Debug, Clone)]
9pub struct FieldInfo {
10 pub name: String,
12 pub type_name: String,
14 pub offset: usize,
16 pub size: usize,
18 pub optional: bool,
20}
21
22impl FieldInfo {
23 pub fn new(name: impl Into<String>, type_name: impl Into<String>) -> Self {
25 Self {
26 name: name.into(),
27 type_name: type_name.into(),
28 offset: 0,
29 size: 0,
30 optional: false,
31 }
32 }
33
34 pub fn with_offset(mut self, offset: usize) -> Self {
36 self.offset = offset;
37 self
38 }
39
40 pub fn with_size(mut self, size: usize) -> Self {
42 self.size = size;
43 self
44 }
45
46 pub fn optional(mut self) -> Self {
48 self.optional = true;
49 self
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct TypeInfo {
56 pub name: String,
58 pub short_name: String,
60 pub version: u32,
62 pub hash: u64,
64 pub size: usize,
66 pub alignment: usize,
68 pub fields: Vec<FieldInfo>,
70 pub stable_layout: bool,
72}
73
74impl TypeInfo {
75 pub fn new(name: impl Into<String>, version: u32) -> Self {
77 let short_name = name.into();
78 let full_name = format!("{}@v{}", short_name, version);
79
80 Self {
81 name: full_name,
82 short_name,
83 version,
84 hash: 0,
85 size: 0,
86 alignment: 8,
87 fields: Vec::new(),
88 stable_layout: false,
89 }
90 }
91
92 pub fn with_hash(mut self, hash: u64) -> Self {
94 self.hash = hash;
95 self
96 }
97
98 pub fn with_size(mut self, size: usize) -> Self {
100 self.size = size;
101 self
102 }
103
104 pub fn with_alignment(mut self, alignment: usize) -> Self {
106 self.alignment = alignment;
107 self
108 }
109
110 pub fn with_field(mut self, field: FieldInfo) -> Self {
112 self.fields.push(field);
113 self
114 }
115
116 pub fn with_fields(mut self, fields: Vec<FieldInfo>) -> Self {
118 self.fields = fields;
119 self
120 }
121
122 pub fn stable(mut self) -> Self {
124 self.stable_layout = true;
125 self
126 }
127
128 pub fn get_field(&self, name: &str) -> Option<&FieldInfo> {
130 self.fields.iter().find(|f| f.name == name)
131 }
132
133 pub fn is_compatible_with(&self, other: &TypeInfo) -> bool {
139 if self.hash != 0 && self.hash == other.hash {
141 return true;
142 }
143
144 for field in &other.fields {
146 if field.optional {
147 continue;
148 }
149
150 match self.get_field(&field.name) {
151 Some(our_field) => {
152 if our_field.type_name != field.type_name {
153 return false;
154 }
155 }
156 None => return false,
157 }
158 }
159
160 true
161 }
162}
163
164pub trait Schema {
169 fn type_info() -> TypeInfo;
171
172 fn schema_hash() -> u64 {
174 Self::type_info().hash
175 }
176
177 fn validate_layout() -> Result<()> {
179 let info = Self::type_info();
180 if !info.stable_layout {
181 return Err(XervError::NonDeterministicLayout {
182 type_name: info.name,
183 cause: "Type must use #[repr(C)] for stable memory layout".to_string(),
184 });
185 }
186 Ok(())
187 }
188}
189
190pub struct SchemaRegistry {
195 schemas: RwLock<HashMap<String, TypeInfo>>,
197}
198
199impl SchemaRegistry {
200 pub fn new() -> Self {
202 Self {
203 schemas: RwLock::new(HashMap::new()),
204 }
205 }
206
207 pub fn register(&self, info: TypeInfo) {
209 let mut schemas = self.schemas.write().unwrap();
210 schemas.insert(info.name.clone(), info);
211 }
212
213 pub fn get(&self, name: &str) -> Option<TypeInfo> {
215 let schemas = self.schemas.read().unwrap();
216 schemas.get(name).cloned()
217 }
218
219 pub fn get_by_hash(&self, hash: u64) -> Option<TypeInfo> {
221 let schemas = self.schemas.read().unwrap();
222 schemas.values().find(|t| t.hash == hash).cloned()
223 }
224
225 pub fn contains(&self, name: &str) -> bool {
227 let schemas = self.schemas.read().unwrap();
228 schemas.contains_key(name)
229 }
230
231 pub fn names(&self) -> Vec<String> {
233 let schemas = self.schemas.read().unwrap();
234 schemas.keys().cloned().collect()
235 }
236
237 pub fn check_compatibility(&self, from: &str, to: &str) -> bool {
239 let schemas = self.schemas.read().unwrap();
240 match (schemas.get(from), schemas.get(to)) {
241 (Some(from_info), Some(to_info)) => from_info.is_compatible_with(to_info),
242 _ => false,
243 }
244 }
245}
246
247impl Default for SchemaRegistry {
248 fn default() -> Self {
249 Self::new()
250 }
251}
252
253#[allow(dead_code)]
258static GLOBAL_REGISTRY: std::sync::OnceLock<SchemaRegistry> = std::sync::OnceLock::new();
259
260#[allow(dead_code)]
274pub fn global_registry() -> &'static SchemaRegistry {
275 GLOBAL_REGISTRY.get_or_init(SchemaRegistry::new)
276}
277
278#[allow(dead_code)]
291pub fn register_schema(info: TypeInfo) {
292 global_registry().register(info);
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn type_info_creation() {
301 let info = TypeInfo::new("OrderInput", 1)
302 .with_hash(0x12345678)
303 .with_size(64)
304 .with_fields(vec![
305 FieldInfo::new("order_id", "String")
306 .with_offset(0)
307 .with_size(24),
308 FieldInfo::new("amount", "f64").with_offset(24).with_size(8),
309 ])
310 .stable();
311
312 assert_eq!(info.name, "OrderInput@v1");
313 assert_eq!(info.short_name, "OrderInput");
314 assert_eq!(info.version, 1);
315 assert_eq!(info.fields.len(), 2);
316 assert!(info.stable_layout);
317 }
318
319 #[test]
320 fn schema_registry() {
321 let registry = SchemaRegistry::new();
322
323 let info1 = TypeInfo::new("TestType", 1).with_hash(111);
324 let info2 = TypeInfo::new("TestType", 2).with_hash(222);
325
326 registry.register(info1);
327 registry.register(info2);
328
329 assert!(registry.contains("TestType@v1"));
330 assert!(registry.contains("TestType@v2"));
331 assert!(!registry.contains("TestType@v3"));
332
333 let retrieved = registry.get("TestType@v1").unwrap();
334 assert_eq!(retrieved.hash, 111);
335 }
336
337 #[test]
338 fn schema_compatibility() {
339 let v1 = TypeInfo::new("Order", 1).with_fields(vec![
340 FieldInfo::new("id", "String"),
341 FieldInfo::new("amount", "f64"),
342 ]);
343
344 let v2_compatible = TypeInfo::new("Order", 2).with_fields(vec![
346 FieldInfo::new("id", "String"),
347 FieldInfo::new("amount", "f64"),
348 FieldInfo::new("notes", "String").optional(),
349 ]);
350
351 assert!(v2_compatible.is_compatible_with(&v1));
352
353 let v3_incompatible =
355 TypeInfo::new("Order", 3).with_fields(vec![FieldInfo::new("id", "String")]);
356
357 assert!(!v3_incompatible.is_compatible_with(&v1));
358 }
359}