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}