1use 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#[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 #[must_use]
22 pub fn new(id: ComponentId) -> Self {
23 Self {
24 id,
25 fields: Vec::new(),
26 }
27 }
28
29 #[must_use]
31 pub fn with_fields(id: ComponentId, fields: Vec<FieldDef>) -> Self {
32 Self { id, fields }
33 }
34
35 #[must_use]
37 pub fn field(mut self, field: FieldDef) -> Self {
38 self.fields.push(field);
39 self
40 }
41}
42
43#[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 pub fn new(components: Vec<ComponentDef>) -> SchemaResult<Self> {
53 let schema = Self { components };
54 schema.validate()?;
55 Ok(schema)
56 }
57
58 #[must_use]
60 pub fn builder() -> SchemaBuilder {
61 SchemaBuilder {
62 components: Vec::new(),
63 }
64 }
65
66 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#[derive(Debug, Default)]
91pub struct SchemaBuilder {
92 components: Vec<ComponentDef>,
93}
94
95impl SchemaBuilder {
96 #[must_use]
98 pub fn component(mut self, component: ComponentDef) -> Self {
99 self.components.push(component);
100 self
101 }
102
103 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 }
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}