Skip to main content

schema/
field.rs

1//! Field codec and change policy definitions.
2
3use crate::FieldId;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// Fixed-point quantization parameters (all integer-based).
9#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct FixedPoint {
12    /// Minimum quantized value.
13    pub min_q: i64,
14    /// Maximum quantized value.
15    pub max_q: i64,
16    /// Units per 1.0 (e.g., 100 => 0.01 resolution).
17    pub scale: u32,
18}
19
20impl FixedPoint {
21    /// Creates a fixed-point configuration from quantized bounds and scale.
22    #[must_use]
23    pub const fn new(min_q: i64, max_q: i64, scale: u32) -> Self {
24        Self {
25            min_q,
26            max_q,
27            scale,
28        }
29    }
30}
31
32/// The encoding for a field (representation only).
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum FieldCodec {
36    /// Boolean (1 bit).
37    Bool,
38
39    /// Unsigned integer with fixed bit width.
40    UInt { bits: u8 },
41
42    /// Signed integer with fixed bit width.
43    SInt { bits: u8 },
44
45    /// Variable-length unsigned integer.
46    VarUInt,
47
48    /// Variable-length signed integer (zigzag encoded).
49    VarSInt,
50
51    /// Fixed-point number with quantization.
52    FixedPoint(FixedPoint),
53}
54
55impl FieldCodec {
56    /// Creates a boolean field codec.
57    #[must_use]
58    pub const fn bool() -> Self {
59        Self::Bool
60    }
61
62    /// Creates an unsigned integer field codec.
63    #[must_use]
64    pub const fn uint(bits: u8) -> Self {
65        Self::UInt { bits }
66    }
67
68    /// Creates a signed integer field codec.
69    #[must_use]
70    pub const fn sint(bits: u8) -> Self {
71        Self::SInt { bits }
72    }
73
74    /// Creates a variable-length unsigned integer field codec.
75    #[must_use]
76    pub const fn var_uint() -> Self {
77        Self::VarUInt
78    }
79
80    /// Creates a variable-length signed integer field codec.
81    #[must_use]
82    pub const fn var_sint() -> Self {
83        Self::VarSInt
84    }
85
86    /// Creates a fixed-point field codec.
87    #[must_use]
88    pub const fn fixed_point(min_q: i64, max_q: i64, scale: u32) -> Self {
89        Self::FixedPoint(FixedPoint::new(min_q, max_q, scale))
90    }
91}
92
93/// Change detection policy for a field.
94#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum ChangePolicy {
97    /// Always send when present in the component mask.
98    Always,
99    /// Send only if the quantized difference exceeds this threshold.
100    Threshold { threshold_q: u32 },
101}
102
103/// Field definition within a component.
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct FieldDef {
107    pub id: FieldId,
108    pub codec: FieldCodec,
109    pub change: ChangePolicy,
110}
111
112impl FieldDef {
113    /// Creates a field definition with the default change policy.
114    #[must_use]
115    pub const fn new(id: FieldId, codec: FieldCodec) -> Self {
116        Self {
117            id,
118            codec,
119            change: ChangePolicy::Always,
120        }
121    }
122
123    /// Creates a field definition with a threshold policy.
124    #[must_use]
125    pub const fn with_threshold(id: FieldId, codec: FieldCodec, threshold_q: u32) -> Self {
126        Self {
127            id,
128            codec,
129            change: ChangePolicy::Threshold { threshold_q },
130        }
131    }
132
133    /// Sets the change policy for a field definition.
134    #[must_use]
135    pub const fn change(mut self, change: ChangePolicy) -> Self {
136        self.change = change;
137        self
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::FieldId;
145
146    #[test]
147    fn fixed_point_construction() {
148        let fp = FixedPoint::new(-100, 200, 100);
149        assert_eq!(fp.min_q, -100);
150        assert_eq!(fp.max_q, 200);
151        assert_eq!(fp.scale, 100);
152    }
153
154    #[test]
155    fn field_codec_variants() {
156        assert!(matches!(FieldCodec::bool(), FieldCodec::Bool));
157        assert!(matches!(FieldCodec::uint(8), FieldCodec::UInt { bits: 8 }));
158        assert!(matches!(FieldCodec::sint(8), FieldCodec::SInt { bits: 8 }));
159        assert!(matches!(FieldCodec::var_uint(), FieldCodec::VarUInt));
160        assert!(matches!(FieldCodec::var_sint(), FieldCodec::VarSInt));
161        assert!(matches!(
162            FieldCodec::fixed_point(-10, 10, 100),
163            FieldCodec::FixedPoint(_)
164        ));
165    }
166
167    #[test]
168    fn field_def_default_change_policy() {
169        let id = FieldId::new(1).unwrap();
170        let field = FieldDef::new(id, FieldCodec::bool());
171        assert_eq!(field.change, ChangePolicy::Always);
172    }
173
174    #[test]
175    fn field_def_threshold_policy() {
176        let id = FieldId::new(2).unwrap();
177        let field = FieldDef::with_threshold(id, FieldCodec::uint(12), 5);
178        assert_eq!(field.change, ChangePolicy::Threshold { threshold_q: 5 });
179    }
180
181    #[test]
182    fn field_def_change_override() {
183        let id = FieldId::new(3).unwrap();
184        let field = FieldDef::new(id, FieldCodec::uint(8))
185            .change(ChangePolicy::Threshold { threshold_q: 2 });
186        assert_eq!(field.change, ChangePolicy::Threshold { threshold_q: 2 });
187    }
188}