Skip to main content

livekit_datatrack/
schema.rs

1// Copyright 2026 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use livekit_protocol as proto;
16use std::sync::Arc;
17use thiserror::Error;
18
19/// Identifier for a data track schema.
20///
21/// A compound identifier with two components: name and encoding.
22///
23/// Two IDs are equal only if both components match; the same name with a
24/// different encoding refers to a distinct schema. Cloning this type is cheap.
25///
26/// # Examples
27///
28/// ```
29/// # use livekit_datatrack::api::{DataTrackSchemaId, DataTrackSchemaEncoding};
30/// let schema = DataTrackSchemaId::new("my_schema", DataTrackSchemaEncoding::Protobuf);
31///
32/// assert_eq!(schema.name(), "my_schema");
33/// assert_eq!(schema.encoding(), &DataTrackSchemaEncoding::Protobuf);
34/// ```
35///
36#[derive(Clone, Hash, PartialEq, Eq)]
37pub struct DataTrackSchemaId {
38    inner: Arc<DataTrackSchemaIdInner>,
39}
40
41#[derive(Hash, PartialEq, Eq)]
42struct DataTrackSchemaIdInner {
43    name: String,
44    encoding: DataTrackSchemaEncoding,
45}
46
47impl std::fmt::Debug for DataTrackSchemaId {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("DataTrackSchemaId")
50            .field("name", &self.inner.name)
51            .field("encoding", &self.inner.encoding)
52            .finish()
53    }
54}
55
56impl DataTrackSchemaId {
57    /// Creates a new schema ID.
58    pub fn new(name: impl Into<String>, encoding: DataTrackSchemaEncoding) -> Self {
59        let inner = DataTrackSchemaIdInner { name: name.into(), encoding }.into();
60        Self { inner }
61    }
62
63    /// Returns the name component of the ID.
64    pub fn name(&self) -> &str {
65        &self.inner.name
66    }
67
68    /// Returns the encoding component of the ID.
69    pub fn encoding(&self) -> &DataTrackSchemaEncoding {
70        &self.inner.encoding
71    }
72}
73
74/// Encoding used for a schema definition.
75///
76/// Identifies the interface definition language the schema is written in (e.g. a
77/// `.proto` file for [`Protobuf`]). This in turn dictates the wire format of the
78/// frames the schema describes, captured by [`DataTrackFrameEncoding`].
79///
80/// [`Protobuf`]: DataTrackSchemaEncoding::Protobuf
81///
82#[non_exhaustive]
83#[derive(Debug, Clone, Hash, PartialEq, Eq)]
84#[cfg_attr(test, derive(fake::Dummy))]
85#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
86pub enum DataTrackSchemaEncoding {
87    /// Protocol Buffer IDL, describes [`Protobuf`] encoded frames.
88    ///
89    /// [`Protobuf`]: DataTrackFrameEncoding::Protobuf
90    Protobuf,
91    /// FlatBuffer IDL, describes [`Flatbuffer`] encoded frames.
92    ///
93    /// [`Flatbuffer`]: DataTrackFrameEncoding::Flatbuffer
94    Flatbuffer,
95    /// ROS 1 Message, describes [`Ros1`] encoded frames.
96    ///
97    /// [`Ros1`]: DataTrackFrameEncoding::Ros1
98    Ros1Msg,
99    /// ROS 2 Message, describes [`Cdr`] encoded frames.
100    ///
101    /// [`Cdr`]: DataTrackFrameEncoding::Cdr
102    Ros2Msg,
103    /// ROS 2 IDL, describes [`Cdr`] encoded frames.
104    ///
105    /// [`Cdr`]: DataTrackFrameEncoding::Cdr
106    Ros2Idl,
107    /// OMG IDL, describes [`Cdr`] encoded frames.
108    ///
109    /// [`Cdr`]: DataTrackFrameEncoding::Cdr
110    OmgIdl,
111    /// JSON Schema, describes [`Json`] encoded frames.
112    ///
113    /// [`Json`]: DataTrackFrameEncoding::Json
114    JsonSchema,
115
116    /// Another well-known encoding not known to this client version.
117    Other,
118    /// An application-specific encoding identified by the contained string.
119    ///
120    /// Prefer using one of the well-known encodings unless the format is not enumerated.
121    /// The identifier must be non-empty and not exceed the server's length limit.
122    ///
123    Custom(String),
124}
125
126/// Encoding used for frames pushed on a data track.
127///
128/// The serialization format of the frame bytes (e.g. [`Protobuf`]); the structure
129/// of those bytes is described by a schema, see [`DataTrackSchemaEncoding`].
130///
131/// [`Protobuf`]: DataTrackFrameEncoding::Protobuf
132///
133#[non_exhaustive]
134#[derive(Debug, Clone, Hash, PartialEq, Eq)]
135#[cfg_attr(test, derive(fake::Dummy))]
136#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
137pub enum DataTrackFrameEncoding {
138    /// ROS 1, must be described by a [`Ros1Msg`] schema.
139    ///
140    /// [`Ros1Msg`]: DataTrackSchemaEncoding::Ros1Msg
141    Ros1,
142    /// CDR, must be described by a [`Ros2Msg`], [`Ros2Idl`], or [`OmgIdl`] schema.
143    ///
144    /// [`Ros2Msg`]: DataTrackSchemaEncoding::Ros2Msg
145    /// [`Ros2Idl`]: DataTrackSchemaEncoding::Ros2Idl
146    /// [`OmgIdl`]: DataTrackSchemaEncoding::OmgIdl
147    Cdr,
148    /// Protocol Buffer, must be described by a [`Protobuf`] schema.
149    ///
150    /// [`Protobuf`]: DataTrackSchemaEncoding::Protobuf
151    Protobuf,
152    /// FlatBuffer, must be described by a [`Flatbuffer`] schema.
153    ///
154    /// [`Flatbuffer`]: DataTrackSchemaEncoding::Flatbuffer
155    Flatbuffer,
156    /// CBOR, self-describing.
157    Cbor,
158    /// MessagePack, self-describing.
159    Msgpack,
160    /// JSON, self-describing or described by a [`JsonSchema`] schema.
161    ///
162    /// [`JsonSchema`]: DataTrackSchemaEncoding::JsonSchema
163    Json,
164
165    /// Another well-known encoding not known to this client version.
166    Other,
167    /// An application-specific encoding identified by the contained string.
168    ///
169    /// Prefer using one of the well-known encodings unless the format is not enumerated.
170    /// The identifier must be non-empty and not exceed the server's length limit.
171    ///
172    Custom(String),
173}
174
175/// An error that can occur when validating data track schema metadata.
176#[derive(Debug, Error, PartialEq)]
177pub enum DataTrackSchemaError {
178    /// Frame encoding is required when providing schema ID.
179    #[error("Frame encoding is required when providing schema ID")]
180    MissingFrameEncoding,
181
182    /// Schema ID is required for frame encoding that is not self-describing.
183    #[error("Schema ID is required for frame encoding that is not self-describing")]
184    MissingSchemaId,
185
186    /// Specified schema and frame encodings are incompatible.
187    #[error("Specified schema and frame encodings are incompatible")]
188    Incompatible,
189}
190
191/// Validates that the given frame and schema encodings are compatible.
192pub(crate) fn validate_schema(
193    frame_encoding: Option<&DataTrackFrameEncoding>,
194    schema_encoding: Option<&DataTrackSchemaEncoding>,
195) -> Result<(), DataTrackSchemaError> {
196    match (frame_encoding, schema_encoding) {
197        (None, Some(_)) => Err(DataTrackSchemaError::MissingFrameEncoding),
198        (Some(frame_encoding), None) => match frame_encoding.is_self_describing() {
199            Some(false) => Err(DataTrackSchemaError::MissingSchemaId),
200            _ => Ok(()),
201        },
202        (Some(frame_encoding), Some(schema_encoding)) => {
203            match frame_encoding.is_described_by(schema_encoding) {
204                Some(false) => Err(DataTrackSchemaError::Incompatible),
205                _ => Ok(()),
206            }
207        }
208        (None, None) => Ok(()), // Not using schema metadata
209    }
210}
211
212impl DataTrackFrameEncoding {
213    /// Returns whether the frame encoding is self-describing (i.e. requires no schema).
214    fn is_self_describing(&self) -> Option<bool> {
215        match self {
216            Self::Cbor | Self::Msgpack | Self::Json => Some(true),
217            Self::Other | Self::Custom(_) => None, // Cannot be determined
218            _ => Some(false),
219        }
220    }
221
222    /// Returns whether the frame encoding can be described by the given schema encoding.
223    fn is_described_by(&self, schema_encoding: &DataTrackSchemaEncoding) -> Option<bool> {
224        use DataTrackSchemaEncoding as SchemaEncoding;
225        match (self, schema_encoding) {
226            (Self::Ros1, SchemaEncoding::Ros1Msg)
227            | (Self::Cdr, SchemaEncoding::Ros2Msg)
228            | (Self::Cdr, SchemaEncoding::Ros2Idl)
229            | (Self::Cdr, SchemaEncoding::OmgIdl)
230            | (Self::Protobuf, SchemaEncoding::Protobuf)
231            | (Self::Flatbuffer, SchemaEncoding::Flatbuffer)
232            | (Self::Json, SchemaEncoding::JsonSchema) => Some(true),
233            (Self::Other, _) | (Self::Custom(_), _) => None, // Cannot be determined
234            _ => Some(false),
235        }
236    }
237}
238
239impl From<proto::DataTrackSchemaId> for DataTrackSchemaId {
240    fn from(msg: proto::DataTrackSchemaId) -> Self {
241        let encoding = msg.encoding.map(Into::into).unwrap_or(DataTrackSchemaEncoding::Other);
242        DataTrackSchemaId::new(msg.name, encoding)
243    }
244}
245
246impl From<DataTrackSchemaId> for proto::DataTrackSchemaId {
247    fn from(value: DataTrackSchemaId) -> Self {
248        Self { name: value.name().to_string(), encoding: Some(value.encoding().clone().into()) }
249    }
250}
251
252impl From<proto::DataTrackSchemaEncoding> for DataTrackSchemaEncoding {
253    fn from(msg: proto::DataTrackSchemaEncoding) -> Self {
254        use proto::data_track_schema_encoding::{Value, WellKnownSchemaEncoding as WellKnown};
255        match msg.value {
256            Some(Value::WellKnown(value)) => match WellKnown::try_from(value) {
257                Ok(WellKnown::Protobuf) => Self::Protobuf,
258                Ok(WellKnown::Flatbuffer) => Self::Flatbuffer,
259                Ok(WellKnown::Ros1Msg) => Self::Ros1Msg,
260                Ok(WellKnown::Ros2Msg) => Self::Ros2Msg,
261                Ok(WellKnown::Ros2Idl) => Self::Ros2Idl,
262                Ok(WellKnown::OmgIdl) => Self::OmgIdl,
263                Ok(WellKnown::JsonSchema) => Self::JsonSchema,
264                // Unspecified or a value introduced after this client version.
265                Ok(WellKnown::Unspecified) | Err(_) => Self::Other,
266            },
267            Some(Value::Custom(name)) => Self::Custom(name),
268            None => Self::Other,
269        }
270    }
271}
272
273impl From<DataTrackSchemaEncoding> for proto::DataTrackSchemaEncoding {
274    fn from(value: DataTrackSchemaEncoding) -> Self {
275        use proto::data_track_schema_encoding::{Value, WellKnownSchemaEncoding as WellKnown};
276        let well_known = match value {
277            DataTrackSchemaEncoding::Protobuf => WellKnown::Protobuf,
278            DataTrackSchemaEncoding::Flatbuffer => WellKnown::Flatbuffer,
279            DataTrackSchemaEncoding::Ros1Msg => WellKnown::Ros1Msg,
280            DataTrackSchemaEncoding::Ros2Msg => WellKnown::Ros2Msg,
281            DataTrackSchemaEncoding::Ros2Idl => WellKnown::Ros2Idl,
282            DataTrackSchemaEncoding::OmgIdl => WellKnown::OmgIdl,
283            DataTrackSchemaEncoding::JsonSchema => WellKnown::JsonSchema,
284            DataTrackSchemaEncoding::Other => WellKnown::Unspecified,
285            DataTrackSchemaEncoding::Custom(name) => {
286                return Self { value: Some(Value::Custom(name)) }
287            }
288        };
289        Self { value: Some(Value::WellKnown(well_known as i32)) }
290    }
291}
292
293impl From<proto::DataTrackFrameEncoding> for DataTrackFrameEncoding {
294    fn from(msg: proto::DataTrackFrameEncoding) -> Self {
295        use proto::data_track_frame_encoding::{Value, WellKnownFrameEncoding as WellKnown};
296        match msg.value {
297            Some(Value::WellKnown(value)) => match WellKnown::try_from(value) {
298                Ok(WellKnown::Ros1) => Self::Ros1,
299                Ok(WellKnown::Cdr) => Self::Cdr,
300                Ok(WellKnown::Protobuf) => Self::Protobuf,
301                Ok(WellKnown::Flatbuffer) => Self::Flatbuffer,
302                Ok(WellKnown::Cbor) => Self::Cbor,
303                Ok(WellKnown::Msgpack) => Self::Msgpack,
304                Ok(WellKnown::Json) => Self::Json,
305                // Unspecified or a value introduced after this client version.
306                Ok(WellKnown::Unspecified) | Err(_) => Self::Other,
307            },
308            Some(Value::Custom(name)) => Self::Custom(name),
309            None => Self::Other,
310        }
311    }
312}
313
314impl From<DataTrackFrameEncoding> for proto::DataTrackFrameEncoding {
315    fn from(value: DataTrackFrameEncoding) -> Self {
316        use proto::data_track_frame_encoding::{Value, WellKnownFrameEncoding as WellKnown};
317        let well_known = match value {
318            DataTrackFrameEncoding::Ros1 => WellKnown::Ros1,
319            DataTrackFrameEncoding::Cdr => WellKnown::Cdr,
320            DataTrackFrameEncoding::Protobuf => WellKnown::Protobuf,
321            DataTrackFrameEncoding::Flatbuffer => WellKnown::Flatbuffer,
322            DataTrackFrameEncoding::Cbor => WellKnown::Cbor,
323            DataTrackFrameEncoding::Msgpack => WellKnown::Msgpack,
324            DataTrackFrameEncoding::Json => WellKnown::Json,
325            DataTrackFrameEncoding::Other => WellKnown::Unspecified,
326            DataTrackFrameEncoding::Custom(name) => {
327                return Self { value: Some(Value::Custom(name)) }
328            }
329        };
330        Self { value: Some(Value::WellKnown(well_known as i32)) }
331    }
332}
333
334impl From<DataTrackSchemaId> for proto::DataBlobKey {
335    fn from(id: DataTrackSchemaId) -> Self {
336        Self { key: Some(proto::data_blob_key::Key::SchemaId(id.into())) }
337    }
338}
339
340#[cfg(test)]
341impl fake::Dummy<fake::Faker> for DataTrackSchemaId {
342    fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &fake::Faker, rng: &mut R) -> Self {
343        use fake::{Fake, Faker};
344        let name: String = Faker.fake_with_rng(rng);
345        let encoding: DataTrackSchemaEncoding = Faker.fake_with_rng(rng);
346        Self::new(name, encoding)
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn test_validate_schema_not_specified() {
356        assert_eq!(validate_schema(None, None), Ok(()));
357    }
358
359    #[test]
360    fn test_validate_schema_self_describing() {
361        assert_eq!(validate_schema(Some(&DataTrackFrameEncoding::Json), None), Ok(()));
362    }
363
364    #[test]
365    fn test_validate_schema_compatible_encodings() {
366        assert_eq!(
367            validate_schema(
368                Some(&DataTrackFrameEncoding::Cdr),
369                Some(&DataTrackSchemaEncoding::Ros2Idl)
370            ),
371            Ok(())
372        );
373    }
374
375    #[test]
376    fn test_validate_schema_custom() {
377        assert_eq!(
378            validate_schema(
379                Some(&DataTrackFrameEncoding::Custom("my-frame-encoding".to_string())),
380                Some(&DataTrackSchemaEncoding::Custom("my-schema-encoding".to_string()))
381            ),
382            Ok(())
383        );
384    }
385
386    #[test]
387    fn test_validate_schema_missing_frame_encoding() {
388        assert_eq!(
389            validate_schema(None, Some(&DataTrackSchemaEncoding::Protobuf)),
390            Err(DataTrackSchemaError::MissingFrameEncoding)
391        );
392    }
393
394    #[test]
395    fn test_validate_schema_missing_schema_id() {
396        assert_eq!(
397            validate_schema(Some(&DataTrackFrameEncoding::Protobuf), None),
398            Err(DataTrackSchemaError::MissingSchemaId)
399        );
400    }
401
402    #[test]
403    fn test_validate_schema_incompatible() {
404        assert_eq!(
405            validate_schema(
406                Some(&DataTrackFrameEncoding::Json),
407                Some(&DataTrackSchemaEncoding::Protobuf)
408            ),
409            Err(DataTrackSchemaError::Incompatible)
410        );
411    }
412}