Skip to main content

schema/
schema.rs

1//! Schema definitions and validation.
2
3use std::collections::HashSet;
4
5use crate::error::{SchemaError, SchemaResult};
6use crate::{ChangePolicy, ComponentId, FieldCodec, FieldDef, FixedPoint};
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// A component definition within a schema.
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ComponentDef {
15    pub id: ComponentId,
16    pub fields: Vec<FieldDef>,
17}
18
19impl ComponentDef {
20    /// Creates a new component with no fields.
21    #[must_use]
22    pub fn new(id: ComponentId) -> Self {
23        Self {
24            id,
25            fields: Vec::new(),
26        }
27    }
28
29    /// Creates a component with the provided fields.
30    #[must_use]
31    pub fn with_fields(id: ComponentId, fields: Vec<FieldDef>) -> Self {
32        Self { id, fields }
33    }
34
35    /// Adds a field to the component.
36    #[must_use]
37    pub fn field(mut self, field: FieldDef) -> Self {
38        self.fields.push(field);
39        self
40    }
41}
42
43/// A schema consisting of ordered components.
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Schema {
47    pub components: Vec<ComponentDef>,
48}
49
50impl Schema {
51    /// Creates a schema from components after validation.
52    pub fn new(components: Vec<ComponentDef>) -> SchemaResult<Self> {
53        let schema = Self { components };
54        schema.validate()?;
55        Ok(schema)
56    }
57
58    /// Creates a schema builder.
59    #[must_use]
60    pub fn builder() -> SchemaBuilder {
61        SchemaBuilder {
62            components: Vec::new(),
63        }
64    }
65
66    /// Validates schema invariants.
67    pub fn validate(&self) -> SchemaResult<()> {
68        let mut component_ids = HashSet::new();
69        for component in &self.components {
70            if !component_ids.insert(component.id) {
71                return Err(SchemaError::DuplicateComponentId { id: component.id });
72            }
73
74            let mut field_ids = HashSet::new();
75            for field in &component.fields {
76                if !field_ids.insert(field.id) {
77                    return Err(SchemaError::DuplicateFieldId {
78                        component: component.id,
79                        field: field.id,
80                    });
81                }
82                validate_field(field)?;
83            }
84        }
85        Ok(())
86    }
87}
88
89/// Builder for `Schema`.
90#[derive(Debug, Default)]
91pub struct SchemaBuilder {
92    components: Vec<ComponentDef>,
93}
94
95impl SchemaBuilder {
96    /// Adds a component definition.
97    #[must_use]
98    pub fn component(mut self, component: ComponentDef) -> Self {
99        self.components.push(component);
100        self
101    }
102
103    /// Builds the schema after validation.
104    pub fn build(self) -> SchemaResult<Schema> {
105        Schema::new(self.components)
106    }
107}
108
109fn validate_field(field: &FieldDef) -> SchemaResult<()> {
110    match field.codec {
111        FieldCodec::UInt { bits } | FieldCodec::SInt { bits } => {
112            if bits == 0 || bits > 64 {
113                return Err(SchemaError::InvalidBitWidth { bits });
114            }
115        }
116        FieldCodec::FixedPoint(fp) => {
117            validate_fixed_point(fp)?;
118        }
119        FieldCodec::Bool | FieldCodec::VarUInt | FieldCodec::VarSInt => {}
120    }
121
122    if let ChangePolicy::Threshold { threshold_q } = field.change {
123        if threshold_q == 0 {
124            // Threshold zero is valid but redundant; allow it for now.
125        }
126    }
127    Ok(())
128}
129
130fn validate_fixed_point(fp: FixedPoint) -> SchemaResult<()> {
131    if fp.scale == 0 {
132        return Err(SchemaError::InvalidFixedPointScale { scale: fp.scale });
133    }
134    if fp.min_q > fp.max_q {
135        return Err(SchemaError::InvalidFixedPointRange {
136            min_q: fp.min_q,
137            max_q: fp.max_q,
138        });
139    }
140    Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::FieldId;
147
148    fn cid(value: u16) -> ComponentId {
149        ComponentId::new(value).unwrap()
150    }
151
152    fn fid(value: u16) -> FieldId {
153        FieldId::new(value).unwrap()
154    }
155
156    #[test]
157    fn schema_builder_roundtrip() {
158        let component = ComponentDef::new(cid(1))
159            .field(FieldDef::new(fid(1), FieldCodec::bool()))
160            .field(FieldDef::new(fid(2), FieldCodec::uint(8)));
161
162        let schema = Schema::builder().component(component).build().unwrap();
163        assert_eq!(schema.components.len(), 1);
164    }
165
166    #[test]
167    fn schema_rejects_duplicate_component_ids() {
168        let c1 = ComponentDef::new(cid(1));
169        let c2 = ComponentDef::new(cid(1));
170        let err = Schema::new(vec![c1, c2]).unwrap_err();
171        assert!(matches!(err, SchemaError::DuplicateComponentId { .. }));
172    }
173
174    #[test]
175    fn schema_rejects_duplicate_field_ids() {
176        let component = ComponentDef::new(cid(1))
177            .field(FieldDef::new(fid(1), FieldCodec::bool()))
178            .field(FieldDef::new(fid(1), FieldCodec::uint(8)));
179        let err = Schema::new(vec![component]).unwrap_err();
180        assert!(matches!(err, SchemaError::DuplicateFieldId { .. }));
181    }
182
183    #[test]
184    fn schema_rejects_invalid_bit_width() {
185        let component = ComponentDef::new(cid(1)).field(FieldDef::new(fid(1), FieldCodec::uint(0)));
186        let err = Schema::new(vec![component]).unwrap_err();
187        assert!(matches!(err, SchemaError::InvalidBitWidth { .. }));
188    }
189
190    #[test]
191    fn schema_rejects_invalid_fixed_point_scale() {
192        let component = ComponentDef::new(cid(1))
193            .field(FieldDef::new(fid(1), FieldCodec::fixed_point(-10, 10, 0)));
194        let err = Schema::new(vec![component]).unwrap_err();
195        assert!(matches!(err, SchemaError::InvalidFixedPointScale { .. }));
196    }
197
198    #[test]
199    fn schema_rejects_invalid_fixed_point_range() {
200        let component = ComponentDef::new(cid(1))
201            .field(FieldDef::new(fid(1), FieldCodec::fixed_point(10, -10, 100)));
202        let err = Schema::new(vec![component]).unwrap_err();
203        assert!(matches!(err, SchemaError::InvalidFixedPointRange { .. }));
204    }
205
206    #[test]
207    fn schema_allows_zero_threshold() {
208        let component = ComponentDef::new(cid(1)).field(FieldDef::with_threshold(
209            fid(1),
210            FieldCodec::uint(8),
211            0,
212        ));
213        let schema = Schema::new(vec![component]).unwrap();
214        assert_eq!(schema.components.len(), 1);
215    }
216}