xerv_core/traits/
schema.rs

1//! Schema trait and registry for type-safe data contracts.
2
3use crate::error::{Result, XervError};
4use std::collections::HashMap;
5use std::sync::RwLock;
6
7/// Information about a field in a schema.
8#[derive(Debug, Clone)]
9pub struct FieldInfo {
10    /// Field name.
11    pub name: String,
12    /// Field type name.
13    pub type_name: String,
14    /// Byte offset within the struct (for stable layouts).
15    pub offset: usize,
16    /// Size in bytes.
17    pub size: usize,
18    /// Whether the field is optional.
19    pub optional: bool,
20}
21
22impl FieldInfo {
23    /// Create a new field info.
24    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    /// Set the offset.
35    pub fn with_offset(mut self, offset: usize) -> Self {
36        self.offset = offset;
37        self
38    }
39
40    /// Set the size.
41    pub fn with_size(mut self, size: usize) -> Self {
42        self.size = size;
43        self
44    }
45
46    /// Mark as optional.
47    pub fn optional(mut self) -> Self {
48        self.optional = true;
49        self
50    }
51}
52
53/// Information about a type schema.
54#[derive(Debug, Clone)]
55pub struct TypeInfo {
56    /// Schema name with version (e.g., "OrderInput@v1").
57    pub name: String,
58    /// Short name without version.
59    pub short_name: String,
60    /// Version number.
61    pub version: u32,
62    /// Hash for quick comparison.
63    pub hash: u64,
64    /// Total size in bytes.
65    pub size: usize,
66    /// Alignment requirement.
67    pub alignment: usize,
68    /// Fields in the schema.
69    pub fields: Vec<FieldInfo>,
70    /// Whether this schema has a stable layout (#[repr(C)]).
71    pub stable_layout: bool,
72}
73
74impl TypeInfo {
75    /// Create a new type info.
76    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    /// Set the hash.
93    pub fn with_hash(mut self, hash: u64) -> Self {
94        self.hash = hash;
95        self
96    }
97
98    /// Set the size.
99    pub fn with_size(mut self, size: usize) -> Self {
100        self.size = size;
101        self
102    }
103
104    /// Set the alignment.
105    pub fn with_alignment(mut self, alignment: usize) -> Self {
106        self.alignment = alignment;
107        self
108    }
109
110    /// Add a field.
111    pub fn with_field(mut self, field: FieldInfo) -> Self {
112        self.fields.push(field);
113        self
114    }
115
116    /// Add multiple fields.
117    pub fn with_fields(mut self, fields: Vec<FieldInfo>) -> Self {
118        self.fields = fields;
119        self
120    }
121
122    /// Mark as having a stable layout.
123    pub fn stable(mut self) -> Self {
124        self.stable_layout = true;
125        self
126    }
127
128    /// Get a field by name.
129    pub fn get_field(&self, name: &str) -> Option<&FieldInfo> {
130        self.fields.iter().find(|f| f.name == name)
131    }
132
133    /// Check if this schema is compatible with another.
134    ///
135    /// A schema is forward-compatible if:
136    /// - All required fields in `other` exist in `self`
137    /// - Field types match
138    pub fn is_compatible_with(&self, other: &TypeInfo) -> bool {
139        // Same hash means identical
140        if self.hash != 0 && self.hash == other.hash {
141            return true;
142        }
143
144        // Check all required fields
145        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
164/// The trait for types with schema metadata.
165///
166/// Types implementing this trait can be validated and introspected at runtime.
167/// The `#[xerv::schema]` macro generates this implementation automatically.
168pub trait Schema {
169    /// Get the type information for this schema.
170    fn type_info() -> TypeInfo;
171
172    /// Get the schema hash.
173    fn schema_hash() -> u64 {
174        Self::type_info().hash
175    }
176
177    /// Validate that this type has a stable layout.
178    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
190/// Registry for schema metadata.
191///
192/// The registry stores type information for all known schemas,
193/// enabling runtime introspection and validation.
194pub struct SchemaRegistry {
195    /// Registered schemas by name.
196    schemas: RwLock<HashMap<String, TypeInfo>>,
197}
198
199impl SchemaRegistry {
200    /// Create a new schema registry.
201    pub fn new() -> Self {
202        Self {
203            schemas: RwLock::new(HashMap::new()),
204        }
205    }
206
207    /// Register a schema.
208    pub fn register(&self, info: TypeInfo) {
209        let mut schemas = self.schemas.write().unwrap();
210        schemas.insert(info.name.clone(), info);
211    }
212
213    /// Get a schema by name.
214    pub fn get(&self, name: &str) -> Option<TypeInfo> {
215        let schemas = self.schemas.read().unwrap();
216        schemas.get(name).cloned()
217    }
218
219    /// Get a schema by hash.
220    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    /// Check if a schema is registered.
226    pub fn contains(&self, name: &str) -> bool {
227        let schemas = self.schemas.read().unwrap();
228        schemas.contains_key(name)
229    }
230
231    /// Get all registered schema names.
232    pub fn names(&self) -> Vec<String> {
233        let schemas = self.schemas.read().unwrap();
234        schemas.keys().cloned().collect()
235    }
236
237    /// Check compatibility between two schemas by name.
238    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/// Global schema registry.
254///
255/// This is intended for use by external code that needs a singleton registry.
256/// Most internal code should create and manage its own `SchemaRegistry` instance.
257#[allow(dead_code)]
258static GLOBAL_REGISTRY: std::sync::OnceLock<SchemaRegistry> = std::sync::OnceLock::new();
259
260/// Get the global schema registry.
261///
262/// Returns a reference to the global singleton `SchemaRegistry`. The registry
263/// is lazily initialized on first access.
264///
265/// # Example
266///
267/// ```ignore
268/// use xerv_core::traits::schema::{global_registry, TypeInfo};
269///
270/// let info = TypeInfo::new("MyType", 1);
271/// global_registry().register(info);
272/// ```
273#[allow(dead_code)]
274pub fn global_registry() -> &'static SchemaRegistry {
275    GLOBAL_REGISTRY.get_or_init(SchemaRegistry::new)
276}
277
278/// Register a schema in the global registry.
279///
280/// Convenience function that registers a `TypeInfo` in the global singleton registry.
281///
282/// # Example
283///
284/// ```ignore
285/// use xerv_core::traits::schema::{register_schema, TypeInfo};
286///
287/// let info = TypeInfo::new("Order", 1).with_hash(0x12345678);
288/// register_schema(info);
289/// ```
290#[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        // v2 adds an optional field - should be compatible
345        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        // v3 removes a required field - not compatible
354        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}