Skip to main content

dial9_trace_format/
schema.rs

1//! Schema types describing event layouts.
2//!
3//! A [`SchemaEntry`] defines the name and fields of an event type. The
4//! [`SchemaRegistry`] tracks all registered schemas and assigns wire type IDs.
5
6use crate::codec::WireTypeId;
7use crate::encoder::FxHashMap;
8use crate::types::FieldType;
9use std::borrow::Cow;
10
11/// A per-field annotation carrying arbitrary key-value metadata.
12///
13/// Annotations are emitted in a separate frame (`TAG_SCHEMA_ANNOTATIONS`)
14/// after the schema frame they belong to. They carry metadata such as units,
15/// display hints, or semantic-convention labels.
16#[non_exhaustive]
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct FieldAnnotation {
19    field_index: u16,
20    key: Cow<'static, str>,
21    value: Cow<'static, str>,
22}
23
24impl FieldAnnotation {
25    /// Create a new field annotation.
26    pub fn new(field_index: u16, key: impl Into<String>, value: impl Into<String>) -> Self {
27        Self {
28            field_index,
29            key: Cow::Owned(key.into()),
30            value: Cow::Owned(value.into()),
31        }
32    }
33
34    /// Index of the field this annotation applies to (0-based, matching the
35    /// field order in [`SchemaEntry::fields`]).
36    pub fn field_index(&self) -> u16 {
37        self.field_index
38    }
39
40    /// Annotation key (e.g. `"metrique.unit"`).
41    pub fn key(&self) -> &str {
42        &self.key
43    }
44
45    /// Annotation value (e.g. `"microseconds"`).
46    pub fn value(&self) -> &str {
47        &self.value
48    }
49}
50
51/// A single field within a schema: a name and a [`FieldType`].
52#[non_exhaustive]
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct FieldDef {
55    pub(crate) name: String,
56    pub(crate) field_type: FieldType,
57}
58
59impl FieldDef {
60    /// Construct a field definition with the given name and type.
61    ///
62    /// ```
63    /// # use dial9_trace_format::schema::FieldDef;
64    /// # use dial9_trace_format::types::FieldType;
65    /// FieldDef::new("worker_id", FieldType::Varint);
66    /// FieldDef::new("tags", FieldType::DynamicList);
67    /// ```
68    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
69        Self {
70            name: name.into(),
71            field_type,
72        }
73    }
74
75    /// Field name (e.g. `"worker_id"`).
76    pub fn name(&self) -> &str {
77        &self.name
78    }
79
80    /// Wire type used to encode this field.
81    pub fn field_type(&self) -> FieldType {
82        self.field_type
83    }
84}
85
86/// Describes the layout of an event type. Does not carry a wire type ID —
87/// the ID is assigned by the encoder and tracked externally by the registry.
88#[non_exhaustive]
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct SchemaEntry {
91    pub(crate) name: String,
92    pub(crate) has_timestamp: bool,
93    pub(crate) fields: Vec<FieldDef>,
94    pub(crate) annotations: Vec<FieldAnnotation>,
95}
96
97impl SchemaEntry {
98    /// Construct a new schema entry.
99    pub fn new(
100        name: impl Into<String>,
101        has_timestamp: bool,
102        fields: impl IntoIterator<Item = FieldDef>,
103    ) -> Self {
104        Self {
105            name: name.into(),
106            has_timestamp,
107            fields: fields.into_iter().collect(),
108            annotations: Vec::new(),
109        }
110    }
111
112    /// Construct a schema entry with annotations.
113    pub fn with_annotations(
114        name: impl Into<String>,
115        has_timestamp: bool,
116        fields: impl IntoIterator<Item = FieldDef>,
117        annotations: impl IntoIterator<Item = FieldAnnotation>,
118    ) -> Self {
119        Self {
120            name: name.into(),
121            has_timestamp,
122            fields: fields.into_iter().collect(),
123            annotations: annotations.into_iter().collect(),
124        }
125    }
126
127    /// Event type name (e.g. `"PollStart"`).
128    pub fn name(&self) -> &str {
129        &self.name
130    }
131
132    /// Whether events of this type carry a packed timestamp in the event header.
133    pub fn has_timestamp(&self) -> bool {
134        self.has_timestamp
135    }
136
137    /// Ordered list of fields (excluding the timestamp).
138    pub fn fields(&self) -> &[FieldDef] {
139        &self.fields
140    }
141
142    /// Per-field annotations.
143    pub fn annotations(&self) -> &[FieldAnnotation] {
144        &self.annotations
145    }
146}
147
148#[derive(Debug, Clone)]
149pub struct SchemaRegistry {
150    pub(crate) schemas: FxHashMap<WireTypeId, SchemaEntry>,
151    pub(crate) next_id: u16,
152}
153
154impl Default for SchemaRegistry {
155    fn default() -> Self {
156        Self {
157            schemas: FxHashMap::default(),
158            // `0..STATIC_WIRE_ID_LIMIT` is reserved for fast-path slot ids,
159            // dynamic registration starts here.
160            next_id: crate::STATIC_WIRE_ID_LIMIT,
161        }
162    }
163}
164
165impl SchemaRegistry {
166    pub fn new() -> Self {
167        Self::default()
168    }
169
170    /// Resets the schema registry to a blank slate without releasing the allocations
171    pub fn clear(&mut self) {
172        self.next_id = crate::STATIC_WIRE_ID_LIMIT;
173        self.schemas.clear();
174    }
175
176    /// Register a schema under the given wire type ID.
177    pub fn register(&mut self, type_id: WireTypeId, entry: SchemaEntry) -> Result<(), String> {
178        if let Some(existing) = self.schemas.get(&type_id) {
179            if *existing == entry {
180                return Ok(());
181            }
182            return Err(format!(
183                "type_id {:?} already registered with different schema",
184                type_id
185            ));
186        }
187        self.schemas.insert(type_id, entry);
188        Ok(())
189    }
190
191    pub fn get(&self, type_id: WireTypeId) -> Option<&SchemaEntry> {
192        self.schemas.get(&type_id)
193    }
194
195    pub fn entries(&self) -> impl Iterator<Item = (WireTypeId, &SchemaEntry)> {
196        self.schemas.iter().map(|(&id, entry)| (id, entry))
197    }
198
199    /// Allocate the next wire type ID.
200    pub fn next_type_id(&mut self) -> WireTypeId {
201        let id = WireTypeId(self.next_id);
202        self.next_id += 1;
203        id
204    }
205
206    /// Advance `next_id` past all registered type IDs.
207    ///
208    /// Call this after bulk-inserting schemas (e.g. from a decoded trace) so
209    /// that [`next_type_id`](Self::next_type_id) won't collide.
210    pub fn sync_next_id(&mut self) {
211        self.next_id = crate::STATIC_WIRE_ID_LIMIT;
212        for &id in self.schemas.keys() {
213            if id.0 >= self.next_id {
214                self.next_id = id.0 + 1;
215            }
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn register_and_lookup() {
226        let mut reg = SchemaRegistry::new();
227        let id = reg.next_type_id();
228        let entry = SchemaEntry {
229            name: "PollStart".into(),
230            has_timestamp: true,
231            fields: vec![
232                FieldDef {
233                    name: "timestamp_ns".into(),
234                    field_type: FieldType::Varint,
235                },
236                FieldDef {
237                    name: "worker".into(),
238                    field_type: FieldType::Varint,
239                },
240            ],
241            annotations: Vec::new(),
242        };
243        reg.register(id, entry.clone()).unwrap();
244        assert_eq!(reg.get(id), Some(&entry));
245        assert_eq!(reg.get(WireTypeId(99)), None);
246    }
247
248    #[test]
249    fn duplicate_type_id_same_schema_ok() {
250        let mut reg = SchemaRegistry::new();
251        let id = reg.next_type_id();
252        let entry = SchemaEntry {
253            name: "A".into(),
254            has_timestamp: true,
255            fields: vec![],
256            annotations: Vec::new(),
257        };
258        reg.register(id, entry.clone()).unwrap();
259        reg.register(id, entry).unwrap();
260    }
261
262    #[test]
263    fn duplicate_type_id_different_schema_rejected() {
264        let mut reg = SchemaRegistry::new();
265        let id = reg.next_type_id();
266        reg.register(
267            id,
268            SchemaEntry {
269                name: "A".into(),
270                has_timestamp: true,
271                fields: vec![],
272                annotations: Vec::new(),
273            },
274        )
275        .unwrap();
276        assert!(
277            reg.register(
278                id,
279                SchemaEntry {
280                    name: "B".into(),
281                    has_timestamp: true,
282                    fields: vec![],
283                    annotations: Vec::new(),
284                }
285            )
286            .is_err()
287        );
288    }
289
290    #[test]
291    fn multiple_schemas() {
292        let mut reg = SchemaRegistry::new();
293        let id1 = reg.next_type_id();
294        reg.register(
295            id1,
296            SchemaEntry {
297                name: "A".into(),
298                has_timestamp: true,
299                fields: vec![],
300                annotations: Vec::new(),
301            },
302        )
303        .unwrap();
304        let id2 = reg.next_type_id();
305        reg.register(
306            id2,
307            SchemaEntry {
308                name: "B".into(),
309                has_timestamp: true,
310                fields: vec![],
311                annotations: Vec::new(),
312            },
313        )
314        .unwrap();
315        assert_eq!(reg.entries().count(), 2);
316    }
317
318    #[test]
319    fn next_type_id_auto_increments() {
320        let mut reg = SchemaRegistry::new();
321        let id1 = reg.next_type_id();
322        let id2 = reg.next_type_id();
323        assert_ne!(id1, id2);
324        assert_eq!(id1, WireTypeId(crate::STATIC_WIRE_ID_LIMIT));
325        assert_eq!(id2, WireTypeId(crate::STATIC_WIRE_ID_LIMIT + 1));
326    }
327}