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}