bones_schema/
registry.rs

1//! Global schema registry.
2
3use std::{
4    alloc::Layout,
5    sync::atomic::{AtomicU32, Ordering::SeqCst},
6};
7
8use append_only_vec::AppendOnlyVec;
9use bones_utils::Deref;
10
11use crate::prelude::*;
12
13/// A unique identifier for a schema registered in the [`SCHEMA_REGISTRY`].
14#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
15pub struct SchemaId {
16    id: u32,
17}
18
19// Note: The schema type is here in the registry module to prevent modification of registered
20// schemas by other modules. The idea is that once a schema is registered, it is unchangable and
21// "certified" so to speak.
22#[doc(hidden)]
23/// A schema registered with the [`SCHEMA_REGISTRY`].
24///
25/// ## Known Limitations
26///
27/// Currently there isn't a known-safe way to construct a schema for a recursive struct. For
28/// example, this struct is troublesome:
29///
30/// ```rust
31/// struct Data {
32///     others: Vec<Data>,
33/// }
34/// ```
35///
36/// This is because nested schemas are required to be a `&'static Schema`, and it would probalby
37/// require unsafe code to create a schema that references itself.
38///
39/// If this is a problem for your use-case, please open an issue. We would like to remove this
40/// limitation or find a suitable workaround in the future.
41#[derive(Deref, Clone, Debug)]
42pub struct Schema {
43    id: SchemaId,
44    #[deref]
45    data: SchemaData,
46    layout: Layout,
47    field_offsets: &'static [(Option<String>, usize)],
48}
49
50impl PartialEq for Schema {
51    fn eq(&self, other: &Self) -> bool {
52        self.id == other.id
53    }
54}
55impl Eq for Schema {}
56
57impl Schema {
58    /// Get the registered, unique ID of the [`Schema`].
59    #[inline]
60    pub fn id(&self) -> SchemaId {
61        self.id
62    }
63
64    /// Get a static reference to the [`Schema`] that was registered.
65    #[inline]
66    pub fn schema(&self) -> &SchemaData {
67        &self.data
68    }
69
70    /// Get the [`Layout`] of the [`Schema`].
71    #[inline]
72    pub fn layout(&self) -> Layout {
73        self.layout
74    }
75
76    /// If this schema represents a struct, this returns the list of fields, with the names of the
77    /// fields, and their byte offsets from the beginning of the struct.
78    #[inline]
79    pub fn field_offsets(&self) -> &'static [(Option<String>, usize)] {
80        self.field_offsets
81    }
82
83    /// Helper function to make sure that this schema matches another or return a
84    /// [`SchemaMismatchError`].
85    pub fn ensure_match(&self, other: &Self) -> Result<(), SchemaMismatchError> {
86        if self == other {
87            Ok(())
88        } else {
89            Err(SchemaMismatchError)
90        }
91    }
92}
93
94/// A schema registry that alloates [`SchemaId`]s for [`SchemaData`]s and returns a registered
95/// [`&'static Schema`][Schema].
96pub struct SchemaRegistry {
97    next_id: AtomicU32,
98    /// The registered schemas.
99    pub schemas: AppendOnlyVec<Schema>,
100}
101
102impl SchemaRegistry {
103    /// Register a schema with the registry.
104    #[track_caller]
105    pub fn register(&self, schema_data: SchemaData) -> &Schema {
106        // Allocate a new schema ID
107        let id = SchemaId {
108            id: self.next_id.fetch_add(1, SeqCst),
109        };
110        assert_ne!(id.id, u32::MAX, "Exhausted all {} schema IDs", u32::MAX);
111
112        // Compute the schema layout info so we can cache it with the Schema.
113        let SchemaLayoutInfo {
114            layout,
115            field_offsets,
116        } = schema_data.kind.compute_layout_info();
117
118        // Leak the field offsets to get static references
119        let field_offsets: Box<_> = field_offsets
120            .into_iter()
121            .map(|(name, offset)| (name.map(|n| n.to_string()), offset))
122            .collect();
123        let field_offsets = Box::leak(field_offsets);
124
125        // Create the schema struct.
126        let schema = Schema {
127            id,
128            data: schema_data,
129            layout,
130            field_offsets,
131        };
132
133        // Insert the schema into the registry.
134        let idx = self.schemas.push(schema);
135
136        &self.schemas[idx]
137    }
138}
139
140/// Global [`SchemaRegistry`] used to register [`SchemaData`]s and produce [`Schema`]s.
141pub static SCHEMA_REGISTRY: SchemaRegistry = SchemaRegistry {
142    next_id: AtomicU32::new(0),
143    schemas: AppendOnlyVec::new(),
144};
145
146#[cfg(test)]
147mod test {
148    use bones_utils::default;
149
150    use super::*;
151
152    #[test]
153    fn registry_smoke() {
154        let mut schemas = Vec::new();
155        for i in 0..100 {
156            let data = SchemaData {
157                name: format!("data{i}").into(),
158                full_name: format!("data{i}").into(),
159                kind: SchemaKind::Primitive(Primitive::U8),
160                type_data: default(),
161                type_id: None,
162                clone_fn: None,
163                drop_fn: None,
164                default_fn: None,
165                hash_fn: None,
166                eq_fn: None,
167            };
168
169            let schema = SCHEMA_REGISTRY.register(data.clone());
170            schemas.push(schema);
171        }
172
173        for (i, schema) in schemas.iter().enumerate() {
174            assert_eq!(schema.data.name, format!("data{i}"));
175        }
176    }
177}