Skip to main content

sigmf/
extensions.rs

1//! Typed access to SigMF extension namespaces.
2//!
3//! SigMF lets applications add fields to the `global`, `captures`,
4//! `annotations`, and `collection` objects under their own namespace prefix
5//! (for example `antenna:gain`), declaring each namespace and version in
6//! `core:extensions`. This crate preserves such fields as untyped JSON; this
7//! module adds a typed layer on top:
8//!
9//! - [`SigmfExtension`] describes a Rust type that maps to a set of namespaced
10//!   fields in one [`ExtensionScope`]. Implement it by hand, or with
11//!   `#[derive(SigmfExtension)]` when the `derive` feature is enabled.
12//! - Typed getters and setters read and write those fields:
13//!   [`Metadata::global_extension`](crate::meta::Metadata::global_extension),
14//!   [`Capture::extension`](crate::capture::Capture::extension),
15//!   [`Annotation::extension`](crate::annotation::Annotation::extension),
16//!   [`CollectionMetadata::extension`](crate::collection::CollectionMetadata::extension),
17//!   and their `set_*`/`remove_*` counterparts.
18//! - [`ExtensionRegistry`] records which extensions an application understands
19//!   so that validation (e.g.
20//!   [`Metadata::validate_with_registry`](crate::meta::Metadata::validate_with_registry))
21//!   can verify declared extensions and their fields.
22//! - [`CropExtensionHandler`] lets applications adjust extension fields when a
23//!   recording is cropped.
24//!
25//! With the `schema` feature, [`extension_schema`] and
26//! [`validate_json_schema`] generate and apply JSON Schemas for extension types.
27//!
28//! Setting extension fields does not modify `core:extensions`; declare the
29//! extension separately, e.g. with [`SigmfExtension::declaration`].
30//!
31//! # Example
32//!
33//! ```rust
34//! use serde::{Deserialize, Serialize};
35//! use sigmf::capture::Capture;
36//! use sigmf::extensions::{ExtensionScope, SigmfExtension};
37//!
38//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
39//! struct Antenna {
40//!     #[serde(rename = "antenna:port")]
41//!     port: u8,
42//! }
43//!
44//! impl SigmfExtension for Antenna {
45//!     const NAMESPACE: &'static str = "antenna";
46//!     const VERSION: &'static str = "1.0.0";
47//!     const SCOPE: ExtensionScope = ExtensionScope::Capture;
48//!     const FIELDS: &'static [&'static str] = &["antenna:port"];
49//! }
50//!
51//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
52//! let mut capture = Capture::new(0);
53//! capture.set_extension(&Antenna { port: 2 })?;
54//! assert_eq!(capture.extra_fields()["antenna:port"], 2);
55//! assert_eq!(capture.extension::<Antenna>()?, Antenna { port: 2 });
56//!
57//! capture.remove_extension::<Antenna>()?;
58//! assert!(capture.extra_fields().get("antenna:port").is_none());
59//! # Ok(())
60//! # }
61//! ```
62
63use crate::SigmfError;
64use crate::global::ExtensionDeclaration;
65use serde::Serialize;
66use serde::de::DeserializeOwned;
67use serde_json::{Map, Value};
68use std::collections::{BTreeMap, BTreeSet};
69
70/// The SigMF object in which an extension's fields may appear.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
72pub enum ExtensionScope {
73    /// The `global` object of a `.sigmf-meta` file.
74    Global,
75    /// An entry of the `captures` array.
76    Capture,
77    /// An entry of the `annotations` array.
78    Annotation,
79    /// The `collection` object of a `.sigmf-collection` file.
80    Collection,
81}
82
83/// Transforms application-defined fields while a recording is cropped.
84pub trait CropExtensionHandler {
85    /// Receives each field map that contains extension data.
86    ///
87    /// `sample_start` and `sample_count` refer to the unmodified capture or
88    /// annotation; both are `None` for global extension fields. The crop interval
89    /// uses absolute, half-open sample indices.
90    fn transform_for_crop(
91        &mut self,
92        scope: ExtensionScope,
93        sample_start: Option<u64>,
94        sample_count: Option<u64>,
95        crop_start: u64,
96        crop_count: u64,
97        fields: &mut Map<String, Value>,
98    ) -> Result<(), SigmfError>;
99}
100
101/// A typed view of one SigMF extension's fields within a single scope.
102///
103/// The type is serialized with Serde to a JSON object whose keys are the
104/// namespaced wire names (e.g. `"antenna:port"`), so fields normally use
105/// `#[serde(rename = "namespace:field")]`. Every key it serializes must be
106/// listed in [`FIELDS`](Self::FIELDS).
107///
108/// With the `derive` feature, `#[derive(sigmf::SigmfExtension)]` together with
109/// `#[sigmf(namespace = "...", version = "...", scope = "...")]` implements
110/// this trait, filling in the associated constants and checking at compile
111/// time that every field's Serde name uses the namespace prefix. See the
112/// [module documentation](self) for a manual implementation.
113pub trait SigmfExtension: Serialize + DeserializeOwned {
114    /// The extension namespace, i.e. the prefix before `:` in field names.
115    ///
116    /// Must be non-empty and must not be `"core"`. The derive macro takes it
117    /// from `#[sigmf(namespace = "...")]` and additionally requires ASCII
118    /// alphanumerics, `_`, or `-`.
119    const NAMESPACE: &'static str;
120    /// The extension version written to `core:extensions`.
121    ///
122    /// Must be non-empty. The derive macro takes it from
123    /// `#[sigmf(version = "...")]`.
124    const VERSION: &'static str;
125    /// The object in which this extension's fields appear.
126    ///
127    /// The derive macro maps `#[sigmf(scope = "...")]` values `"global"`,
128    /// `"capture"`, `"annotation"`, and `"collection"` to the corresponding
129    /// [`ExtensionScope`] variant.
130    const SCOPE: ExtensionScope;
131    /// The complete set of namespaced wire field names this type owns.
132    ///
133    /// Only these keys are read by the typed getters and replaced or removed
134    /// by the typed setters; other fields are left untouched. The derive macro
135    /// fills this with each struct field's Serde wire name.
136    const FIELDS: &'static [&'static str];
137
138    /// Serializes `self` to a JSON value.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`SigmfError::Serde`] if serialization fails.
143    fn to_extension_value(&self) -> Result<Value, SigmfError> {
144        serde_json::to_value(self).map_err(SigmfError::Serde)
145    }
146
147    /// Deserializes an extension from a JSON value.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`SigmfError::InvalidMetadata`] if `value` does not match the
152    /// type, e.g. because a required field is missing.
153    fn from_extension_value(value: Value) -> Result<Self, SigmfError>
154    where
155        Self: Sized,
156    {
157        serde_json::from_value(value)
158            .map_err(|error| SigmfError::InvalidMetadata(error.to_string()))
159    }
160
161    /// Builds the `core:extensions` entry declaring this extension.
162    ///
163    /// `optional` indicates whether readers may ignore the extension.
164    fn declaration(optional: bool) -> ExtensionDeclaration {
165        ExtensionDeclaration {
166            name: Self::NAMESPACE.to_string(),
167            version: Self::VERSION.to_string(),
168            optional,
169        }
170    }
171}
172
173/// The set of extensions, by namespace and version, an application supports.
174///
175/// Pass a registry to
176/// [`Metadata::validate_with_registry`](crate::meta::Metadata::validate_with_registry)
177/// or
178/// [`Collection::validate_with_registry`](crate::collection::Collection::validate_with_registry)
179/// so declared extensions are treated as verified and their fields are
180/// checked against each extension's [`SCOPE`](SigmfExtension::SCOPE) and
181/// [`FIELDS`](SigmfExtension::FIELDS).
182///
183/// # Example
184///
185/// ```rust
186/// use serde::{Deserialize, Serialize};
187/// use sigmf::extensions::{ExtensionRegistry, ExtensionScope, SigmfExtension};
188///
189/// #[derive(Serialize, Deserialize)]
190/// struct Lab {
191///     #[serde(rename = "lab:gain_db")]
192///     gain_db: f64,
193/// }
194///
195/// impl SigmfExtension for Lab {
196///     const NAMESPACE: &'static str = "lab";
197///     const VERSION: &'static str = "1.0.0";
198///     const SCOPE: ExtensionScope = ExtensionScope::Global;
199///     const FIELDS: &'static [&'static str] = &["lab:gain_db"];
200/// }
201///
202/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
203/// let mut registry = ExtensionRegistry::new();
204/// registry.register::<Lab>()?;
205/// assert!(registry.supports("lab", "1.0.0"));
206/// assert!(!registry.supports("lab", "2.0.0"));
207/// assert!(registry.supports_field("lab", "1.0.0", ExtensionScope::Global, "lab:gain_db"));
208/// assert!(!registry.supports_scope("lab", "1.0.0", ExtensionScope::Capture));
209///
210/// // Registering the same namespace and version twice is an error.
211/// assert!(registry.register::<Lab>().is_err());
212/// # Ok(())
213/// # }
214/// ```
215#[derive(Debug, Clone, Default)]
216pub struct ExtensionRegistry {
217    supported: BTreeMap<(String, String), RegisteredExtension>,
218}
219
220#[derive(Debug, Clone)]
221struct RegisteredExtension {
222    scope: ExtensionScope,
223    fields: BTreeSet<String>,
224}
225
226impl ExtensionRegistry {
227    /// Creates an empty registry.
228    pub fn new() -> Self {
229        Self::default()
230    }
231
232    /// Registers support for extension `T` at its namespace and version.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s namespace is empty or
237    /// `"core"`, its version is empty, or the same namespace and version are
238    /// already registered.
239    pub fn register<T: SigmfExtension>(&mut self) -> Result<(), SigmfError> {
240        let namespace = T::NAMESPACE;
241        if namespace.is_empty() || namespace == "core" || T::VERSION.is_empty() {
242            return Err(SigmfError::InvalidMetadata(
243                "extension namespace and version must be non-empty and namespace cannot be core"
244                    .into(),
245            ));
246        }
247        let key = (namespace.to_string(), T::VERSION.to_string());
248        if self.supported.contains_key(&key) {
249            return Err(SigmfError::InvalidMetadata(format!(
250                "extension {namespace:?} version {:?} is already registered",
251                T::VERSION
252            )));
253        }
254        self.supported.insert(
255            key,
256            RegisteredExtension {
257                scope: T::SCOPE,
258                fields: T::FIELDS.iter().map(|field| (*field).to_string()).collect(),
259            },
260        );
261        Ok(())
262    }
263
264    /// Returns `true` if the extension `namespace` at `version` is registered.
265    pub fn supports(&self, namespace: &str, version: &str) -> bool {
266        self.supported
267            .contains_key(&(namespace.to_string(), version.to_string()))
268    }
269
270    /// Returns `true` if the registered extension `namespace` at `version`
271    /// applies to `scope`.
272    ///
273    /// Returns `false` if the extension is not registered.
274    pub fn supports_scope(&self, namespace: &str, version: &str, scope: ExtensionScope) -> bool {
275        self.supported
276            .get(&(namespace.to_string(), version.to_string()))
277            .is_some_and(|extension| extension.scope == scope)
278    }
279
280    /// Returns `true` if the registered extension `namespace` at `version`
281    /// applies to `scope` and defines the namespaced wire name `field`.
282    ///
283    /// Returns `false` if the extension is not registered.
284    pub fn supports_field(
285        &self,
286        namespace: &str,
287        version: &str,
288        scope: ExtensionScope,
289        field: &str,
290    ) -> bool {
291        self.supported
292            .get(&(namespace.to_string(), version.to_string()))
293            .is_some_and(|extension| extension.scope == scope && extension.fields.contains(field))
294    }
295
296    /// Returns a `core:extensions` declaration for each registered extension.
297    ///
298    /// Declarations are sorted by namespace, then version, and are all marked
299    /// as required (`optional: false`).
300    pub fn declarations(&self) -> Vec<ExtensionDeclaration> {
301        self.supported
302            .iter()
303            .map(|((name, version), _)| ExtensionDeclaration {
304                name: name.clone(),
305                version: version.clone(),
306                optional: false,
307            })
308            .collect()
309    }
310}
311
312/// Generates the JSON Schema for extension type `T`.
313///
314/// The schema describes the JSON object `T` serializes to, keyed by its
315/// namespaced wire names.
316///
317/// # Errors
318///
319/// Returns [`SigmfError::Serde`] if the generated schema cannot be converted
320/// to a [`Value`].
321///
322/// # Example
323///
324/// ```rust
325/// # #[cfg(feature = "schema")]
326/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
327/// use serde::{Deserialize, Serialize};
328/// use serde_json::json;
329/// use sigmf::extensions::{
330///     extension_schema, validate_json_schema, ExtensionScope, SigmfExtension,
331/// };
332///
333/// #[derive(Serialize, Deserialize, schemars::JsonSchema)]
334/// struct Lab {
335///     #[serde(rename = "lab:gain_db")]
336///     gain_db: f64,
337/// }
338///
339/// impl SigmfExtension for Lab {
340///     const NAMESPACE: &'static str = "lab";
341///     const VERSION: &'static str = "1.0.0";
342///     const SCOPE: ExtensionScope = ExtensionScope::Global;
343///     const FIELDS: &'static [&'static str] = &["lab:gain_db"];
344/// }
345///
346/// let schema = extension_schema::<Lab>()?;
347/// validate_json_schema(&schema, &json!({ "lab:gain_db": 30.0 }))?;
348/// assert!(validate_json_schema(&schema, &json!({ "lab:gain_db": "high" })).is_err());
349/// # Ok(())
350/// # }
351/// # #[cfg(not(feature = "schema"))]
352/// # fn main() {}
353/// ```
354#[cfg(feature = "schema")]
355pub fn extension_schema<T>() -> Result<Value, SigmfError>
356where
357    T: SigmfExtension + schemars::JsonSchema,
358{
359    serde_json::to_value(schemars::schema_for!(T)).map_err(SigmfError::Serde)
360}
361
362/// Validates `instance` against the JSON Schema `schema`.
363///
364/// For safety, schemas containing `$ref`, `$dynamicRef`, or `$recursiveRef`
365/// values that are not local (`#/...`) references are rejected rather than
366/// fetched.
367///
368/// # Errors
369///
370/// Returns [`SigmfError::InvalidMetadata`] if the schema contains a remote
371/// reference, is not a valid JSON Schema, or `instance` does not conform to
372/// it (the message lists every validation error).
373#[cfg(feature = "schema")]
374pub fn validate_json_schema(schema: &Value, instance: &Value) -> Result<(), SigmfError> {
375    if schema_contains_remote_reference(schema) {
376        return Err(SigmfError::InvalidMetadata(
377            "remote JSON Schema references are disabled".into(),
378        ));
379    }
380    let validator = jsonschema::validator_for(schema)
381        .map_err(|error| SigmfError::InvalidMetadata(format!("invalid JSON Schema: {error}")))?;
382    let errors = validator
383        .iter_errors(instance)
384        .map(|error| error.to_string())
385        .collect::<Vec<_>>();
386    if !errors.is_empty() {
387        return Err(SigmfError::InvalidMetadata(format!(
388            "JSON Schema validation failed: {}",
389            errors.join("; ")
390        )));
391    }
392    Ok(())
393}
394
395#[cfg(feature = "schema")]
396fn schema_contains_remote_reference(value: &Value) -> bool {
397    match value {
398        Value::Object(object) => object.iter().any(|(key, value)| {
399            (matches!(key.as_str(), "$ref" | "$dynamicRef" | "$recursiveRef")
400                && value
401                    .as_str()
402                    .is_some_and(|reference| !reference.starts_with("#/")))
403                || schema_contains_remote_reference(value)
404        }),
405        Value::Array(values) => values.iter().any(schema_contains_remote_reference),
406        _ => false,
407    }
408}
409
410impl crate::meta::Metadata {
411    /// Reads extension `T` from the fields of the `global` object.
412    ///
413    /// Only the keys listed in [`SigmfExtension::FIELDS`] are considered;
414    /// absent keys are simply omitted before deserialization.
415    ///
416    /// # Errors
417    ///
418    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
419    /// [`ExtensionScope::Global`] or the fields do not deserialize into `T`.
420    pub fn global_extension<T: SigmfExtension>(&self) -> Result<T, SigmfError> {
421        get_extension_fields::<T>(ExtensionScope::Global, self.global().extra_fields())
422    }
423
424    /// Writes extension `T` into the `global` object.
425    ///
426    /// All of `T`'s declared [`FIELDS`](SigmfExtension::FIELDS) are first
427    /// removed, then the serialized fields are inserted, so fields omitted by
428    /// serialization (e.g. `None` with `skip_serializing_if`) are cleared.
429    /// `core:extensions` is not modified.
430    ///
431    /// # Errors
432    ///
433    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
434    /// [`ExtensionScope::Global`], `extension` does not serialize to a JSON
435    /// object, or it serializes a key not listed in `FIELDS`; returns
436    /// [`SigmfError::Serde`] if serialization fails. The metadata is unchanged
437    /// on error.
438    pub fn set_global_extension<T: SigmfExtension>(
439        &mut self,
440        extension: &T,
441    ) -> Result<(), SigmfError> {
442        let fields = extension_fields::<T>(ExtensionScope::Global, extension)?;
443        self.update_global_extension_fields(T::FIELDS, fields);
444        Ok(())
445    }
446
447    /// Removes all of extension `T`'s declared fields from the `global` object.
448    ///
449    /// `core:extensions` is not modified.
450    ///
451    /// # Errors
452    ///
453    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
454    /// [`ExtensionScope::Global`].
455    pub fn remove_global_extension<T: SigmfExtension>(&mut self) -> Result<(), SigmfError> {
456        ensure_scope::<T>(ExtensionScope::Global)?;
457        self.update_global_extension_fields(T::FIELDS, Map::new());
458        Ok(())
459    }
460}
461
462impl crate::capture::Capture {
463    /// Reads extension `T` from this capture's fields.
464    ///
465    /// Only the keys listed in [`SigmfExtension::FIELDS`] are considered;
466    /// absent keys are simply omitted before deserialization.
467    ///
468    /// # Errors
469    ///
470    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
471    /// [`ExtensionScope::Capture`] or the fields do not deserialize into `T`.
472    pub fn extension<T: SigmfExtension>(&self) -> Result<T, SigmfError> {
473        get_extension_fields::<T>(ExtensionScope::Capture, self.extra_fields())
474    }
475
476    /// Writes extension `T` into this capture.
477    ///
478    /// All of `T`'s declared [`FIELDS`](SigmfExtension::FIELDS) are first
479    /// removed, then the serialized fields are inserted, so fields omitted by
480    /// serialization are cleared. `core:extensions` is not modified.
481    ///
482    /// # Errors
483    ///
484    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
485    /// [`ExtensionScope::Capture`], `extension` does not serialize to a JSON
486    /// object, or it serializes a key not listed in `FIELDS`; returns
487    /// [`SigmfError::Serde`] if serialization fails. Nothing is changed on
488    /// error.
489    pub fn set_extension<T: SigmfExtension>(&mut self, extension: &T) -> Result<(), SigmfError> {
490        let fields = extension_fields::<T>(ExtensionScope::Capture, extension)?;
491        replace_extension_fields(self.extension_fields_mut(), T::FIELDS, fields);
492        Ok(())
493    }
494
495    /// Removes all of extension `T`'s declared fields from this capture.
496    ///
497    /// # Errors
498    ///
499    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
500    /// [`ExtensionScope::Capture`].
501    pub fn remove_extension<T: SigmfExtension>(&mut self) -> Result<(), SigmfError> {
502        ensure_scope::<T>(ExtensionScope::Capture)?;
503        replace_extension_fields(self.extension_fields_mut(), T::FIELDS, Map::new());
504        Ok(())
505    }
506}
507
508impl crate::annotation::Annotation {
509    /// Reads extension `T` from this annotation's fields.
510    ///
511    /// Only the keys listed in [`SigmfExtension::FIELDS`] are considered;
512    /// absent keys are simply omitted before deserialization.
513    ///
514    /// # Errors
515    ///
516    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
517    /// [`ExtensionScope::Annotation`] or the fields do not deserialize into `T`.
518    pub fn extension<T: SigmfExtension>(&self) -> Result<T, SigmfError> {
519        get_extension_fields::<T>(ExtensionScope::Annotation, self.extra_fields())
520    }
521
522    /// Writes extension `T` into this annotation.
523    ///
524    /// All of `T`'s declared [`FIELDS`](SigmfExtension::FIELDS) are first
525    /// removed, then the serialized fields are inserted, so fields omitted by
526    /// serialization are cleared. `core:extensions` is not modified.
527    ///
528    /// # Errors
529    ///
530    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
531    /// [`ExtensionScope::Annotation`], `extension` does not serialize to a JSON
532    /// object, or it serializes a key not listed in `FIELDS`; returns
533    /// [`SigmfError::Serde`] if serialization fails. Nothing is changed on
534    /// error.
535    pub fn set_extension<T: SigmfExtension>(&mut self, extension: &T) -> Result<(), SigmfError> {
536        let fields = extension_fields::<T>(ExtensionScope::Annotation, extension)?;
537        replace_extension_fields(self.extension_fields_mut(), T::FIELDS, fields);
538        Ok(())
539    }
540
541    /// Removes all of extension `T`'s declared fields from this annotation.
542    ///
543    /// # Errors
544    ///
545    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
546    /// [`ExtensionScope::Annotation`].
547    pub fn remove_extension<T: SigmfExtension>(&mut self) -> Result<(), SigmfError> {
548        ensure_scope::<T>(ExtensionScope::Annotation)?;
549        replace_extension_fields(self.extension_fields_mut(), T::FIELDS, Map::new());
550        Ok(())
551    }
552}
553
554impl crate::collection::CollectionMetadata {
555    /// Reads extension `T` from this `collection` object's fields.
556    ///
557    /// Only the keys listed in [`SigmfExtension::FIELDS`] are considered;
558    /// absent keys are simply omitted before deserialization.
559    ///
560    /// # Errors
561    ///
562    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
563    /// [`ExtensionScope::Collection`] or the fields do not deserialize into `T`.
564    pub fn extension<T: SigmfExtension>(&self) -> Result<T, SigmfError> {
565        get_extension_fields::<T>(ExtensionScope::Collection, self.extra_fields())
566    }
567
568    /// Writes extension `T` into this `collection` object.
569    ///
570    /// All of `T`'s declared [`FIELDS`](SigmfExtension::FIELDS) are first
571    /// removed, then the serialized fields are inserted, so fields omitted by
572    /// serialization are cleared. `core:extensions` is not modified.
573    ///
574    /// # Errors
575    ///
576    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
577    /// [`ExtensionScope::Collection`], `extension` does not serialize to a JSON
578    /// object, or it serializes a key not listed in `FIELDS`; returns
579    /// [`SigmfError::Serde`] if serialization fails. Nothing is changed on
580    /// error.
581    pub fn set_extension<T: SigmfExtension>(&mut self, extension: &T) -> Result<(), SigmfError> {
582        let fields = extension_fields::<T>(ExtensionScope::Collection, extension)?;
583        replace_extension_fields(self.extension_fields_mut(), T::FIELDS, fields);
584        Ok(())
585    }
586
587    /// Removes all of extension `T`'s declared fields from this `collection` object.
588    ///
589    /// # Errors
590    ///
591    /// Returns [`SigmfError::InvalidMetadata`] if `T`'s scope is not
592    /// [`ExtensionScope::Collection`].
593    pub fn remove_extension<T: SigmfExtension>(&mut self) -> Result<(), SigmfError> {
594        ensure_scope::<T>(ExtensionScope::Collection)?;
595        replace_extension_fields(self.extension_fields_mut(), T::FIELDS, Map::new());
596        Ok(())
597    }
598}
599
600fn get_extension_fields<T: SigmfExtension>(
601    scope: ExtensionScope,
602    source: &Map<String, Value>,
603) -> Result<T, SigmfError> {
604    ensure_scope::<T>(scope)?;
605    let fields = T::FIELDS
606        .iter()
607        .filter_map(|field| {
608            source
609                .get(*field)
610                .map(|value| ((*field).to_string(), value.clone()))
611        })
612        .collect();
613    T::from_extension_value(Value::Object(fields))
614}
615
616fn extension_fields<T: SigmfExtension>(
617    scope: ExtensionScope,
618    extension: &T,
619) -> Result<Map<String, Value>, SigmfError> {
620    ensure_scope::<T>(scope)?;
621    let Value::Object(fields) = extension.to_extension_value()? else {
622        return Err(SigmfError::InvalidMetadata(
623            "extension must serialize as an object".into(),
624        ));
625    };
626    if fields
627        .keys()
628        .any(|field| !T::FIELDS.contains(&field.as_str()))
629    {
630        return Err(SigmfError::InvalidMetadata(
631            "extension serialization produced an undeclared field".into(),
632        ));
633    }
634    Ok(fields)
635}
636
637fn ensure_scope<T: SigmfExtension>(scope: ExtensionScope) -> Result<(), SigmfError> {
638    if T::SCOPE != scope {
639        return Err(SigmfError::InvalidMetadata(
640            "extension scope does not match the metadata object".into(),
641        ));
642    }
643    Ok(())
644}
645
646fn replace_extension_fields(
647    destination: &mut Map<String, Value>,
648    declared_fields: &[&str],
649    replacement: Map<String, Value>,
650) {
651    for field in declared_fields {
652        destination.remove(*field);
653    }
654    destination.extend(replacement);
655}