Skip to main content

sigmf/
global.rs

1//! The SigMF `global` object.
2//!
3//! Every SigMF metadata file contains exactly one `global` object describing
4//! properties that apply to the whole recording: the specification version,
5//! the sample [`Datatype`], the sample rate, provenance information such as
6//! author and hardware, and the list of extensions in use. This module models
7//! that object as [`Global`], and the entries of its `core:extensions` array
8//! as [`ExtensionDeclaration`].
9//!
10//! Keys in the `core` namespace map to typed fields on [`Global`]; any other
11//! keys (for example, fields defined by extensions) are preserved verbatim and
12//! exposed through [`Global::extra_fields`].
13
14use super::{Integer, Number, SigmfError};
15use geojson::GeoJson;
16use serde_derive::{Deserialize, Serialize};
17use serde_json::{Map, Value};
18
19/// Re-exported datatype types used by [`Global::datatype`].
20pub use crate::datatype::{ByteType, ComponentType, Datatype, Endianness, SampleDomain};
21
22/// The SigMF `global` object of a metadata file.
23///
24/// Required fields (`core:version` and `core:datatype`) are stored directly;
25/// optional `core` fields are stored as [`Option`]s and are omitted from
26/// serialized JSON when `None`. Unknown keys encountered during
27/// deserialization (typically extension fields) are retained and serialized
28/// back out unchanged; see [`Global::extra_fields`] and
29/// [`Global::insert_extra`].
30///
31/// # Examples
32///
33/// ```rust
34/// use sigmf::global::{Datatype, Global};
35///
36/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
37/// let datatype: Datatype = "cf32_le".parse()?;
38/// let mut global = Global::new("1.2.6", datatype);
39/// global.set_sample_rate(1_000_000.0);
40/// global.set_description("Example capture".to_string());
41///
42/// let json = serde_json::to_value(&global)?;
43/// assert_eq!(json["core:version"], "1.2.6");
44/// assert_eq!(json["core:datatype"], "cf32_le");
45/// assert_eq!(json["core:sample_rate"], 1_000_000.0);
46/// assert!(json.get("core:author").is_none());
47/// # Ok(())
48/// # }
49/// ```
50#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct Global {
52    // Required fields
53    /// Version of the SigMF specification the metadata conforms to (`core:version`).
54    #[serde(rename = "core:version")]
55    pub version: String,
56    /// Format of the samples stored in the dataset file (`core:datatype`).
57    #[serde(rename = "core:datatype")]
58    pub datatype: Datatype,
59
60    // Optional fields
61    /// Sample rate of the recording in samples per second (`core:sample_rate`).
62    #[serde(rename = "core:sample_rate", skip_serializing_if = "Option::is_none")]
63    pub sample_rate: Option<Number>,
64    /// Name and optionally email address of the recording's author (`core:author`).
65    #[serde(rename = "core:author", skip_serializing_if = "Option::is_none")]
66    pub author: Option<String>,
67    /// Base filename of the SigMF collection this recording belongs to (`core:collection`).
68    #[serde(rename = "core:collection", skip_serializing_if = "Option::is_none")]
69    pub collection: Option<String>,
70    /// Digital Object Identifier (DOI) of the dataset file (`core:data_doi`).
71    #[serde(rename = "core:data_doi", skip_serializing_if = "Option::is_none")]
72    pub data_doi: Option<String>,
73    /// Free-text description of the recording (`core:description`).
74    #[serde(rename = "core:description", skip_serializing_if = "Option::is_none")]
75    pub description: Option<String>,
76    /// Description of the hardware used to make the recording (`core:hw`).
77    #[serde(rename = "core:hw", skip_serializing_if = "Option::is_none")]
78    pub hw: Option<String>,
79    /// URL of the license under which the recording is distributed (`core:license`).
80    #[serde(rename = "core:license", skip_serializing_if = "Option::is_none")]
81    pub license: Option<String>,
82    /// Whether the metadata is intentionally distributed without a dataset file
83    /// (`core:metadata_only`).
84    ///
85    /// Must not be used together with Non-Conforming Dataset fields.
86    #[serde(rename = "core:metadata_only", skip_serializing_if = "Option::is_none")]
87    pub metadata_only: Option<bool>, // Cannot be used in Non-Conforming Datasets
88    /// Digital Object Identifier (DOI) of the metadata file (`core:meta_doi`).
89    #[serde(rename = "core:meta_doi", skip_serializing_if = "Option::is_none")]
90    pub meta_doi: Option<String>,
91    /// Number of interleaved channels in the dataset file (`core:num_channels`).
92    ///
93    /// The SigMF specification treats an absent value as one channel.
94    #[serde(rename = "core:num_channels", skip_serializing_if = "Option::is_none")]
95    pub num_channels: Option<Integer>,
96    /// Index offset of the first sample in the dataset file (`core:offset`).
97    #[serde(rename = "core:offset", skip_serializing_if = "Option::is_none")]
98    pub offset: Option<Integer>,
99    /// Name of the software used to make the recording (`core:recorder`).
100    #[serde(rename = "core:recorder", skip_serializing_if = "Option::is_none")]
101    pub recorder: Option<String>,
102    /// Hex-encoded SHA-512 hash of the dataset file (`core:sha512`).
103    #[serde(rename = "core:sha512", skip_serializing_if = "Option::is_none")]
104    pub sha512: Option<String>,
105    /// GeoJSON location of the recording system (`core:geolocation`).
106    #[serde(rename = "core:geolocation", skip_serializing_if = "Option::is_none")]
107    pub geolocation: Option<GeoJson>,
108    /// Extensions used by this recording's metadata (`core:extensions`).
109    #[serde(rename = "core:extensions", skip_serializing_if = "Option::is_none")]
110    pub extensions: Option<Vec<ExtensionDeclaration>>,
111
112    // For Non-Conforming Datasets ONLY
113    /// Full filename of a Non-Conforming Dataset file (`core:dataset`).
114    #[serde(rename = "core:dataset", skip_serializing_if = "Option::is_none")]
115    pub dataset: Option<String>,
116    /// Number of bytes to ignore at the end of a Non-Conforming Dataset file
117    /// (`core:trailing_bytes`).
118    #[serde(
119        rename = "core:trailing_bytes",
120        skip_serializing_if = "Option::is_none"
121    )]
122    pub trailing_bytes: Option<Integer>,
123    #[serde(flatten)]
124    extra_fields: Map<String, Value>,
125}
126
127impl Global {
128    /// Creates a [`Global`] with the required `core:version` and
129    /// `core:datatype` fields set and every optional field unset.
130    ///
131    /// No validation of `version` is performed here.
132    ///
133    /// # Examples
134    ///
135    /// ```rust
136    /// use sigmf::global::{ComponentType, Datatype, Endianness, Global, SampleDomain};
137    ///
138    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
139    /// let datatype = Datatype::new(
140    ///     SampleDomain::Complex,
141    ///     Some(ComponentType::I16),
142    ///     Some(Endianness::LE),
143    ///     None,
144    /// )?;
145    /// let global = Global::new("1.2.6", datatype);
146    /// assert_eq!(global.get_version(), "1.2.6");
147    /// assert_eq!(global.get_datatype().as_str(), "ci16_le");
148    /// assert!(global.get_sample_rate().is_none());
149    /// assert!(global.extra_fields().is_empty());
150    /// # Ok(())
151    /// # }
152    /// ```
153    pub fn new(version: impl Into<String>, datatype: Datatype) -> Self {
154        Self {
155            version: version.into(),
156            datatype,
157            sample_rate: None,
158            author: None,
159            collection: None,
160            data_doi: None,
161            description: None,
162            hw: None,
163            license: None,
164            metadata_only: None,
165            meta_doi: None,
166            num_channels: None,
167            offset: None,
168            recorder: None,
169            sha512: None,
170            geolocation: None,
171            extensions: None,
172            dataset: None,
173            trailing_bytes: None,
174            extra_fields: Map::new(),
175        }
176    }
177
178    /// Returns the fields of this object that are not modeled by a typed field,
179    /// keyed by their full JSON key.
180    ///
181    /// These are the keys that did not map to a typed field during
182    /// deserialization (typically extension fields such as `antenna:gain`), plus
183    /// any added via [`Global::insert_extra`]. They are serialized alongside the
184    /// typed `core` fields.
185    pub fn extra_fields(&self) -> &Map<String, Value> {
186        &self.extra_fields
187    }
188
189    pub(crate) fn extension_fields_mut(&mut self) -> &mut Map<String, Value> {
190        &mut self.extra_fields
191    }
192
193    pub(crate) fn update_extension_fields(
194        &mut self,
195        known_fields: &[&str],
196        fields: Map<String, Value>,
197    ) {
198        for field in known_fields {
199            self.extra_fields.remove(*field);
200        }
201        self.extra_fields.extend(fields);
202    }
203
204    /// Inserts or replaces an extra (non-typed) field, returning the previous value for
205    /// `key` if one was present.
206    ///
207    /// The key is stored as given; namespace syntax and extension declarations
208    /// are not checked here (see [`crate::validation`]).
209    ///
210    /// # Errors
211    ///
212    /// Returns [`SigmfError::InvalidMetadata`] if `key` is one of the
213    /// `core:` global keys modeled by a typed field on [`Global`] (for example
214    /// `core:sample_rate`); set those through the corresponding field or setter
215    /// instead.
216    pub fn insert_extra(
217        &mut self,
218        key: impl Into<String>,
219        value: Value,
220    ) -> Result<Option<Value>, SigmfError> {
221        let key = key.into();
222        if is_core_global_key(&key) {
223            return Err(SigmfError::InvalidMetadata(format!(
224                "cannot insert known field {key:?} as an extension"
225            )));
226        }
227        Ok(self.extra_fields.insert(key, value))
228    }
229
230    // Setters
231    /// Sets the SigMF specification version (`core:version`).
232    pub fn set_version(&mut self, version: String) {
233        self.version = version;
234    }
235
236    /// Sets the sample format of the dataset (`core:datatype`).
237    pub fn set_datatype(&mut self, datatype: Datatype) {
238        self.datatype = datatype;
239    }
240
241    /// Sets the sample rate in samples per second (`core:sample_rate`).
242    pub fn set_sample_rate(&mut self, sample_rate: Number) {
243        self.sample_rate = Some(sample_rate);
244    }
245
246    /// Sets the recording author (`core:author`).
247    pub fn set_author(&mut self, author: String) {
248        self.author = Some(author);
249    }
250
251    /// Sets the base filename of the associated collection (`core:collection`).
252    pub fn set_collection(&mut self, collection_str: String) {
253        self.collection = Some(collection_str);
254    }
255
256    /// Sets the DOI of the dataset file (`core:data_doi`).
257    pub fn set_data_doi(&mut self, data_doi: String) {
258        self.data_doi = Some(data_doi);
259    }
260
261    /// Sets the free-text description (`core:description`).
262    pub fn set_description(&mut self, description: String) {
263        self.description = Some(description);
264    }
265
266    /// Sets the hardware description (`core:hw`).
267    pub fn set_hw(&mut self, hw: String) {
268        self.hw = Some(hw);
269    }
270
271    /// Sets the license URL (`core:license`).
272    pub fn set_license(&mut self, license: String) {
273        self.license = Some(license);
274    }
275
276    /// Sets whether the metadata is distributed without a dataset (`core:metadata_only`).
277    pub fn set_metadata_only(&mut self, metadata_only: bool) {
278        self.metadata_only = Some(metadata_only);
279    }
280
281    /// Sets the DOI of the metadata file (`core:meta_doi`).
282    pub fn set_meta_doi(&mut self, meta_doi: String) {
283        self.meta_doi = Some(meta_doi);
284    }
285
286    /// Sets the number of interleaved channels (`core:num_channels`).
287    pub fn set_num_channels(&mut self, num_channels: Integer) {
288        self.num_channels = Some(num_channels);
289    }
290
291    /// Sets the index offset of the first sample (`core:offset`).
292    pub fn set_offset(&mut self, offset: Integer) {
293        self.offset = Some(offset);
294    }
295
296    /// Sets the name of the recording software (`core:recorder`).
297    pub fn set_recorder(&mut self, recorder: String) {
298        self.recorder = Some(recorder);
299    }
300
301    /// Sets the SHA-512 hash of the dataset file (`core:sha512`).
302    ///
303    /// The value is stored as given; it is not computed or verified here.
304    pub fn set_sha512(&mut self, hash: String) {
305        self.sha512 = Some(hash);
306    }
307
308    /// Sets the GeoJSON location of the recording system (`core:geolocation`).
309    pub fn set_geolocation(&mut self, geolocation: GeoJson) {
310        self.geolocation = Some(geolocation);
311    }
312
313    /// Replaces the list of declared extensions (`core:extensions`).
314    pub fn set_extensions(&mut self, extensions: Vec<ExtensionDeclaration>) {
315        self.extensions = Some(extensions);
316    }
317
318    /// Sets the Non-Conforming Dataset filename (`core:dataset`).
319    pub fn set_dataset(&mut self, dataset: String) {
320        self.dataset = Some(dataset);
321    }
322
323    /// Sets the number of trailing bytes to ignore in a Non-Conforming Dataset
324    /// (`core:trailing_bytes`).
325    pub fn set_trailing_bytes(&mut self, trailing_bytes: Integer) {
326        self.trailing_bytes = Some(trailing_bytes);
327    }
328
329    // Getters
330    /// Returns a copy of the SigMF specification version (`core:version`).
331    pub fn get_version(&self) -> String {
332        self.version.clone()
333    }
334
335    /// Returns a copy of the dataset sample format (`core:datatype`).
336    pub fn get_datatype(&self) -> Datatype {
337        self.datatype.clone()
338    }
339
340    /// Returns the sample rate in samples per second (`core:sample_rate`), if set.
341    pub fn get_sample_rate(&self) -> Option<Number> {
342        self.sample_rate
343    }
344
345    /// Returns a copy of the recording author (`core:author`), if set.
346    pub fn get_author(&self) -> Option<String> {
347        self.author.clone()
348    }
349
350    /// Returns a copy of the associated collection's base filename (`core:collection`), if set.
351    pub fn get_collection(&self) -> Option<String> {
352        self.collection.clone()
353    }
354
355    /// Returns a copy of the dataset DOI (`core:data_doi`), if set.
356    pub fn get_data_doi(&self) -> Option<String> {
357        self.data_doi.clone()
358    }
359
360    /// Returns a copy of the free-text description (`core:description`), if set.
361    pub fn get_description(&self) -> Option<String> {
362        self.description.clone()
363    }
364
365    /// Returns a copy of the hardware description (`core:hw`), if set.
366    pub fn get_hw(&self) -> Option<String> {
367        self.hw.clone()
368    }
369
370    /// Returns a copy of the license URL (`core:license`), if set.
371    pub fn get_license(&self) -> Option<String> {
372        self.license.clone()
373    }
374
375    /// Returns the metadata-only flag (`core:metadata_only`), if set.
376    pub fn get_metadata_only(&self) -> Option<bool> {
377        self.metadata_only
378    }
379
380    /// Returns a copy of the metadata DOI (`core:meta_doi`), if set.
381    pub fn get_meta_doi(&self) -> Option<String> {
382        self.meta_doi.clone()
383    }
384
385    /// Returns the number of interleaved channels (`core:num_channels`), if set.
386    ///
387    /// This returns `None` when the key is absent; it does not apply the
388    /// specification's default of one channel.
389    pub fn get_num_channels(&self) -> Option<Integer> {
390        self.num_channels
391    }
392
393    /// Returns the index offset of the first sample (`core:offset`), if set.
394    pub fn get_offset(&self) -> Option<Integer> {
395        self.offset
396    }
397
398    /// Returns a copy of the recording software name (`core:recorder`), if set.
399    pub fn get_recorder(&self) -> Option<String> {
400        self.recorder.clone()
401    }
402
403    /// Returns a copy of the dataset SHA-512 hash (`core:sha512`), if set.
404    pub fn get_sha512(&self) -> Option<String> {
405        self.sha512.clone()
406    }
407
408    /// Returns a copy of the GeoJSON location (`core:geolocation`), if set.
409    pub fn get_geolocation(&self) -> Option<GeoJson> {
410        self.geolocation.clone()
411    }
412
413    /// Returns a copy of the declared extensions (`core:extensions`), if set.
414    pub fn get_extensions(&self) -> Option<Vec<ExtensionDeclaration>> {
415        self.extensions.clone()
416    }
417
418    /// Returns a copy of the Non-Conforming Dataset filename (`core:dataset`), if set.
419    pub fn get_dataset(&self) -> Option<String> {
420        self.dataset.clone()
421    }
422
423    /// Returns the number of trailing bytes to ignore (`core:trailing_bytes`), if set.
424    pub fn get_trailing_bytes(&self) -> Option<Integer> {
425        self.trailing_bytes
426    }
427}
428
429fn is_core_global_key(key: &str) -> bool {
430    matches!(
431        key,
432        "core:version"
433            | "core:datatype"
434            | "core:sample_rate"
435            | "core:author"
436            | "core:collection"
437            | "core:data_doi"
438            | "core:description"
439            | "core:hw"
440            | "core:license"
441            | "core:metadata_only"
442            | "core:meta_doi"
443            | "core:num_channels"
444            | "core:offset"
445            | "core:recorder"
446            | "core:sha512"
447            | "core:geolocation"
448            | "core:extensions"
449            | "core:dataset"
450            | "core:trailing_bytes"
451    )
452}
453
454/// An entry in the `core:extensions` array declaring a SigMF extension.
455///
456/// Each extension whose namespace is used in a metadata file should be
457/// declared so readers know which extension specification (and version) to
458/// apply and whether they may safely ignore it.
459#[derive(Debug, Clone, Deserialize, Serialize)]
460pub struct ExtensionDeclaration {
461    /// Extension namespace name, e.g. `antenna` (`name`).
462    pub name: String,
463    /// Version of the extension specification in use (`version`).
464    pub version: String,
465    /// Whether the extension may be ignored by readers that do not support it
466    /// (`optional`); `false` means the extension is required to interpret the
467    /// recording correctly.
468    pub optional: bool,
469}