Skip to main content

schema/
lib.rs

1//! Replication schema and field codec definitions for the sdec codec.
2//!
3//! This crate defines how game state is represented for replication:
4//! - Schema model for entity types, components, and fields
5//! - Field codecs (bool, integers, fixed-point, varints)
6//! - Quantization and threshold configuration
7//! - Deterministic schema hashing
8//!
9//! # Design Principles
10//!
11//! - **Runtime-first** - the initial release uses runtime schema building; derive macros come later.
12//! - **Explicit schemas** - No reflection on arbitrary Rust types.
13//! - **Deterministic hashing** - Schema hash is stable given the same definition.
14
15mod error;
16mod field;
17mod hash;
18mod schema;
19
20use std::num::NonZeroU16;
21
22#[cfg(feature = "serde")]
23use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
24
25pub use error::{SchemaError, SchemaResult};
26pub use field::{ChangePolicy, FieldCodec, FieldDef, FixedPoint};
27pub use hash::schema_hash;
28pub use schema::{ComponentDef, Schema, SchemaBuilder};
29
30/// A component ID within a schema (non-zero).
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub struct ComponentId(NonZeroU16);
33
34impl ComponentId {
35    /// Creates a new component ID. Returns `None` if `value` is zero.
36    #[must_use]
37    pub const fn new(value: u16) -> Option<Self> {
38        match NonZeroU16::new(value) {
39            Some(value) => Some(Self(value)),
40            None => None,
41        }
42    }
43
44    /// Returns the underlying numeric value.
45    #[must_use]
46    pub const fn get(self) -> u16 {
47        self.0.get()
48    }
49}
50
51#[cfg(feature = "serde")]
52impl Serialize for ComponentId {
53    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
54        serializer.serialize_u16(self.get())
55    }
56}
57
58#[cfg(feature = "serde")]
59impl<'de> Deserialize<'de> for ComponentId {
60    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
61        let value = u16::deserialize(deserializer)?;
62        ComponentId::new(value).ok_or_else(|| D::Error::custom("component id must be non-zero"))
63    }
64}
65
66/// A field ID within a component (non-zero).
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub struct FieldId(NonZeroU16);
69
70impl FieldId {
71    /// Creates a new field ID. Returns `None` if `value` is zero.
72    #[must_use]
73    pub const fn new(value: u16) -> Option<Self> {
74        match NonZeroU16::new(value) {
75            Some(value) => Some(Self(value)),
76            None => None,
77        }
78    }
79
80    /// Returns the underlying numeric value.
81    #[must_use]
82    pub const fn get(self) -> u16 {
83        self.0.get()
84    }
85}
86
87#[cfg(feature = "serde")]
88impl Serialize for FieldId {
89    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
90        serializer.serialize_u16(self.get())
91    }
92}
93
94#[cfg(feature = "serde")]
95impl<'de> Deserialize<'de> for FieldId {
96    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
97        let value = u16::deserialize(deserializer)?;
98        FieldId::new(value).ok_or_else(|| D::Error::custom("field id must be non-zero"))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::mem::size_of;
106
107    #[test]
108    fn public_api_exports() {
109        // Verify all expected items are exported
110        let _ = FieldCodec::bool();
111        let _ = ChangePolicy::Always;
112        let _ = FieldDef::new(FieldId::new(1).unwrap(), FieldCodec::bool());
113        let _ = Schema::builder();
114        let _ = schema_hash(&Schema::new(Vec::new()).unwrap());
115
116        // Type aliases
117        let _: ComponentId = ComponentId::new(1).unwrap();
118        let _: FieldId = FieldId::new(1).unwrap();
119    }
120
121    #[test]
122    fn field_codec_basic_usage() {
123        let codec = FieldCodec::bool();
124        assert!(matches!(codec, FieldCodec::Bool));
125    }
126
127    #[test]
128    fn schema_hash_basic() {
129        let schema = Schema::new(Vec::new()).unwrap();
130        assert_ne!(schema_hash(&schema), 0);
131    }
132
133    #[test]
134    fn component_id_and_field_id_sizes() {
135        // Verify the type sizes match WIRE_FORMAT.md
136        assert_eq!(size_of::<ComponentId>(), 2);
137        assert_eq!(size_of::<FieldId>(), 2);
138    }
139
140    #[test]
141    fn component_id_zero_is_invalid() {
142        assert!(ComponentId::new(0).is_none());
143    }
144
145    #[test]
146    fn field_id_zero_is_invalid() {
147        assert!(FieldId::new(0).is_none());
148    }
149}