Skip to main content

sigmf/
annotation.rs

1//! SigMF annotation segments.
2//!
3//! A SigMF metadata file has an `annotations` array whose entries describe
4//! features of interest in the recording, such as a detected burst, a
5//! labeled emitter, or a free-form comment. Each entry covers a range of
6//! samples starting at `core:sample_start` and may optionally be bounded in
7//! frequency. This module models a single entry as [`Annotation`] and
8//! provides [`AnnotationBuilder`] for constructing one with validated
9//! frequency bounds.
10//!
11//! Keys in the `core` namespace map to typed fields on [`Annotation`]; any
12//! other keys (for example, extension fields) are preserved verbatim and
13//! exposed through [`Annotation::extra_fields`].
14
15use super::{Integer, Number};
16use serde_derive::{Deserialize, Serialize};
17use serde_json::{Map, Value};
18
19/// A single entry of the SigMF `annotations` array.
20///
21/// Optional `core` fields are stored as [`Option`]s and are omitted from
22/// serialized JSON when `None`. Unknown keys encountered during
23/// deserialization (typically extension fields) are retained and serialized
24/// back out unchanged; see [`Annotation::extra_fields`] and
25/// [`Annotation::insert_extra`].
26///
27/// Prefer [`Annotation::builder`], which checks frequency bounds, over
28/// [`Annotation::new`], which performs no validation.
29#[derive(Debug, Deserialize, Serialize, Clone)]
30pub struct Annotation {
31    /// Index of the first sample covered by this annotation (`core:sample_start`).
32    #[serde(rename = "core:sample_start")]
33    pub sample_start: Integer,
34    /// Number of samples covered by this annotation (`core:sample_count`).
35    ///
36    /// Per the SigMF specification, an absent value means the annotation
37    /// extends to the end of the recording.
38    #[serde(rename = "core:sample_count", skip_serializing_if = "Option::is_none")]
39    pub sample_count: Option<Integer>,
40    /// Lower edge of the annotated feature's frequency range in Hz
41    /// (`core:freq_lower_edge`).
42    #[serde(
43        rename = "core:freq_lower_edge",
44        skip_serializing_if = "Option::is_none"
45    )]
46    pub freq_lower_edge: Option<Number>,
47    /// Upper edge of the annotated feature's frequency range in Hz
48    /// (`core:freq_upper_edge`).
49    #[serde(
50        rename = "core:freq_upper_edge",
51        skip_serializing_if = "Option::is_none"
52    )]
53    pub freq_upper_edge: Option<Number>,
54    /// Short human-readable label for the annotation (`core:label`).
55    #[serde(rename = "core:label", skip_serializing_if = "Option::is_none")]
56    pub label: Option<String>,
57    /// Free-text human-readable comment (`core:comment`).
58    #[serde(rename = "core:comment", skip_serializing_if = "Option::is_none")]
59    pub comment: Option<String>,
60    /// Name of the entity (person or software) that created the annotation
61    /// (`core:generator`).
62    #[serde(rename = "core:generator", skip_serializing_if = "Option::is_none")]
63    pub generator: Option<String>,
64    /// RFC 4122 UUID identifying this annotation (`core:uuid`).
65    #[serde(rename = "core:uuid", skip_serializing_if = "Option::is_none")]
66    pub uuid: Option<String>,
67    #[serde(flatten)]
68    extra_fields: Map<String, Value>,
69}
70
71impl Annotation {
72    /// Starts building an [`Annotation`] that begins at `sample_start`
73    /// (`core:sample_start`).
74    ///
75    /// Equivalent to [`AnnotationBuilder::new`].
76    ///
77    /// # Examples
78    ///
79    /// ```rust
80    /// use sigmf::annotation::Annotation;
81    ///
82    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
83    /// let annotation = Annotation::builder(4096)
84    ///     .sample_count(1024)
85    ///     .frequency_range(2.400e9, 2.402e9)
86    ///     .label("burst")
87    ///     .build()?;
88    /// assert_eq!(annotation.get_sample_start(), 4096);
89    /// assert_eq!(annotation.get_sample_count(), Some(1024));
90    ///
91    /// let json = serde_json::to_value(&annotation)?;
92    /// assert_eq!(json["core:label"], "burst");
93    /// assert!(json.get("core:comment").is_none());
94    /// # Ok(())
95    /// # }
96    /// ```
97    pub fn builder(sample_start: Integer) -> AnnotationBuilder {
98        AnnotationBuilder::new(sample_start)
99    }
100
101    #[allow(clippy::too_many_arguments)]
102    /// Creates an [`Annotation`] directly from all of its `core` fields, with no
103    /// extra fields.
104    ///
105    /// Unlike [`AnnotationBuilder::build`], this performs no validation; in
106    /// particular, frequency bounds are not checked for finiteness, ordering, or
107    /// being supplied together.
108    ///
109    /// # Examples
110    ///
111    /// ```rust
112    /// use sigmf::annotation::Annotation;
113    ///
114    /// let annotation = Annotation::new(
115    ///     0,
116    ///     Some(1000),
117    ///     None,
118    ///     None,
119    ///     Some("preamble".to_string()),
120    ///     None,
121    ///     Some("my-detector".to_string()),
122    ///     None,
123    /// );
124    /// assert_eq!(annotation.get_label().as_deref(), Some("preamble"));
125    /// assert!(annotation.extra_fields().is_empty());
126    /// ```
127    pub fn new(
128        sample_start: Integer,
129        sample_count: Option<Integer>,
130        freq_lower_edge: Option<Number>,
131        freq_upper_edge: Option<Number>,
132        label: Option<String>,
133        comment: Option<String>,
134        generator: Option<String>,
135        uuid: Option<String>,
136    ) -> Self {
137        Annotation {
138            sample_start,
139            sample_count,
140            freq_lower_edge,
141            freq_upper_edge,
142            label,
143            comment,
144            generator,
145            uuid,
146            extra_fields: Map::new(),
147        }
148    }
149
150    /// Returns the fields of this annotation that are not modeled by a typed
151    /// field, keyed by their full JSON key.
152    ///
153    /// These are the keys that did not map to a typed field during
154    /// deserialization (typically extension fields), plus any added via
155    /// [`Annotation::insert_extra`]. They are serialized alongside the typed
156    /// `core` fields.
157    pub fn extra_fields(&self) -> &Map<String, Value> {
158        &self.extra_fields
159    }
160
161    pub(crate) fn extension_fields_mut(&mut self) -> &mut Map<String, Value> {
162        &mut self.extra_fields
163    }
164
165    /// Inserts or replaces an extra (non-typed) field, returning the previous
166    /// value for `key` if one was present.
167    ///
168    /// The key is stored as given; namespace syntax and extension declarations
169    /// are not checked here (see [`crate::validation`]).
170    ///
171    /// # Errors
172    ///
173    /// Returns an error message if `key` is one of the `core:` annotation keys
174    /// modeled by a typed field on [`Annotation`] (for example
175    /// `core:sample_start`); set those through the corresponding field or setter
176    /// instead.
177    pub fn insert_extra(
178        &mut self,
179        key: impl Into<String>,
180        value: Value,
181    ) -> Result<Option<Value>, String> {
182        let key = key.into();
183        if matches!(
184            key.as_str(),
185            "core:sample_start"
186                | "core:sample_count"
187                | "core:freq_lower_edge"
188                | "core:freq_upper_edge"
189                | "core:label"
190                | "core:comment"
191                | "core:generator"
192                | "core:uuid"
193        ) {
194            return Err(format!("cannot insert known field {key:?} as an extension"));
195        }
196        Ok(self.extra_fields.insert(key, value))
197    }
198
199    // Setters
200    /// Sets the index of the first annotated sample (`core:sample_start`).
201    pub fn set_sample_start(&mut self, sample_start: Integer) {
202        self.sample_start = sample_start;
203    }
204
205    /// Sets the number of annotated samples (`core:sample_count`).
206    pub fn set_sample_count(&mut self, sample_count: Integer) {
207        self.sample_count = Some(sample_count);
208    }
209
210    /// Sets the lower frequency edge in Hz (`core:freq_lower_edge`).
211    ///
212    /// The value is not validated against the upper edge.
213    pub fn set_freq_lower_edge(&mut self, freq_lower_edge: Number) {
214        self.freq_lower_edge = Some(freq_lower_edge);
215    }
216
217    /// Sets the upper frequency edge in Hz (`core:freq_upper_edge`).
218    ///
219    /// The value is not validated against the lower edge.
220    pub fn set_freq_upper_edge(&mut self, freq_upper_edge: Number) {
221        self.freq_upper_edge = Some(freq_upper_edge);
222    }
223
224    /// Sets the short human-readable label (`core:label`).
225    pub fn set_label(&mut self, label: String) {
226        self.label = Some(label);
227    }
228
229    /// Sets the free-text comment (`core:comment`).
230    pub fn set_comment(&mut self, comment: String) {
231        self.comment = Some(comment);
232    }
233
234    /// Sets the name of the annotation's creator (`core:generator`).
235    pub fn set_generator(&mut self, generator: String) {
236        self.generator = Some(generator);
237    }
238
239    /// Sets the annotation UUID (`core:uuid`).
240    ///
241    /// The value is stored as given; it is not checked to be a valid UUID.
242    pub fn set_uuid(&mut self, uuid: String) {
243        self.uuid = Some(uuid);
244    }
245
246    // Getters
247    /// Returns the index of the first annotated sample (`core:sample_start`).
248    pub fn get_sample_start(&self) -> Integer {
249        self.sample_start
250    }
251
252    /// Returns the number of annotated samples (`core:sample_count`), if set.
253    pub fn get_sample_count(&self) -> Option<Integer> {
254        self.sample_count
255    }
256
257    /// Returns the lower frequency edge in Hz (`core:freq_lower_edge`), if set.
258    pub fn get_freq_lower_edge(&self) -> Option<Number> {
259        self.freq_lower_edge
260    }
261
262    /// Returns the upper frequency edge in Hz (`core:freq_upper_edge`), if set.
263    pub fn get_freq_upper_edge(&self) -> Option<Number> {
264        self.freq_upper_edge
265    }
266
267    /// Returns a copy of the label (`core:label`), if set.
268    pub fn get_label(&self) -> Option<String> {
269        self.label.clone()
270    }
271
272    /// Returns a copy of the comment (`core:comment`), if set.
273    pub fn get_comment(&self) -> Option<String> {
274        self.comment.clone()
275    }
276
277    /// Returns a copy of the creator name (`core:generator`), if set.
278    pub fn get_generator(&self) -> Option<String> {
279        self.generator.clone()
280    }
281
282    /// Returns a copy of the annotation UUID (`core:uuid`), if set.
283    pub fn get_uuid(&self) -> Option<String> {
284        self.uuid.clone()
285    }
286}
287
288#[derive(Debug, Clone)]
289/// Builder for [`Annotation`] that validates frequency bounds on
290/// [`build`](AnnotationBuilder::build).
291///
292/// Create one with [`Annotation::builder`] or [`AnnotationBuilder::new`].
293/// All fields other than `core:sample_start` start unset. The built
294/// annotation has no extra fields.
295pub struct AnnotationBuilder {
296    sample_start: Integer,
297    sample_count: Option<Integer>,
298    freq_lower_edge: Option<Number>,
299    freq_upper_edge: Option<Number>,
300    label: Option<String>,
301    comment: Option<String>,
302    generator: Option<String>,
303    uuid: Option<String>,
304}
305
306impl AnnotationBuilder {
307    /// Creates a builder for an annotation beginning at `sample_start`
308    /// (`core:sample_start`), with all other fields unset.
309    pub fn new(sample_start: Integer) -> Self {
310        Self {
311            sample_start,
312            sample_count: None,
313            freq_lower_edge: None,
314            freq_upper_edge: None,
315            label: None,
316            comment: None,
317            generator: None,
318            uuid: None,
319        }
320    }
321
322    /// Sets the number of annotated samples (`core:sample_count`).
323    ///
324    /// Zero is accepted.
325    pub fn sample_count(mut self, sample_count: Integer) -> Self {
326        self.sample_count = Some(sample_count);
327        self
328    }
329
330    /// Sets the lower and upper frequency edges in Hz
331    /// (`core:freq_lower_edge` and `core:freq_upper_edge`).
332    ///
333    /// The bounds are validated by [`build`](AnnotationBuilder::build).
334    pub fn frequency_range(mut self, lower: Number, upper: Number) -> Self {
335        self.freq_lower_edge = Some(lower);
336        self.freq_upper_edge = Some(upper);
337        self
338    }
339
340    /// Sets the short human-readable label (`core:label`).
341    pub fn label(mut self, label: impl Into<String>) -> Self {
342        self.label = Some(label.into());
343        self
344    }
345
346    /// Sets the free-text comment (`core:comment`).
347    pub fn comment(mut self, comment: impl Into<String>) -> Self {
348        self.comment = Some(comment.into());
349        self
350    }
351
352    /// Sets the name of the annotation's creator (`core:generator`).
353    pub fn generator(mut self, generator: impl Into<String>) -> Self {
354        self.generator = Some(generator.into());
355        self
356    }
357
358    /// Sets the annotation UUID (`core:uuid`).
359    ///
360    /// The value is not checked to be a valid UUID.
361    pub fn uuid(mut self, uuid: impl Into<String>) -> Self {
362        self.uuid = Some(uuid.into());
363        self
364    }
365
366    /// Validates the frequency bounds and builds the [`Annotation`].
367    ///
368    /// # Errors
369    ///
370    /// Returns [`crate::SigmfError::InvalidMetadata`] if frequency bounds were
371    /// supplied and either bound is not finite, or the lower bound exceeds the
372    /// upper bound.
373    pub fn build(self) -> Result<Annotation, crate::SigmfError> {
374        match (self.freq_lower_edge, self.freq_upper_edge) {
375            (Some(lower), Some(upper))
376                if lower.is_finite() && upper.is_finite() && lower <= upper => {}
377            (None, None) => {}
378            (Some(lower), Some(upper)) if !lower.is_finite() || !upper.is_finite() => {
379                return Err(crate::SigmfError::InvalidMetadata(
380                    "annotation frequency bounds must be finite".into(),
381                ));
382            }
383            (Some(_), Some(_)) => {
384                return Err(crate::SigmfError::InvalidMetadata(
385                    "annotation lower frequency bound exceeds its upper bound".into(),
386                ));
387            }
388            _ => {
389                return Err(crate::SigmfError::InvalidMetadata(
390                    "annotation frequency bounds must be supplied together".into(),
391                ));
392            }
393        }
394        Ok(Annotation {
395            sample_start: self.sample_start,
396            sample_count: self.sample_count,
397            freq_lower_edge: self.freq_lower_edge,
398            freq_upper_edge: self.freq_upper_edge,
399            label: self.label,
400            comment: self.comment,
401            generator: self.generator,
402            uuid: self.uuid,
403            extra_fields: Map::new(),
404        })
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn builder_checks_frequency_bounds_and_accepts_zero_sample_count() {
414        let annotation = Annotation::builder(4096)
415            .sample_count(0)
416            .frequency_range(100.0, 200.0)
417            .label("burst")
418            .build()
419            .unwrap();
420        assert_eq!(annotation.sample_start, 4096);
421        assert_eq!(annotation.sample_count, Some(0));
422        assert_eq!(annotation.label.as_deref(), Some("burst"));
423        assert!(
424            Annotation::builder(0)
425                .frequency_range(200.0, 100.0)
426                .build()
427                .is_err()
428        );
429    }
430}