Skip to main content

cadmpeg_ir/native/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Source-format namespaces retained outside the format-neutral model.
3
4use std::collections::BTreeMap;
5
6use schemars::JsonSchema;
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8use serde_json::{Map, Value};
9
10/// One non-empty native arena reported as an exporter loss.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12pub struct LossCount {
13    /// Source-format namespace this arena belongs to.
14    pub format: String,
15    /// Arena name within that namespace.
16    pub kind: String,
17    /// Number of records in the arena.
18    pub count: usize,
19}
20
21/// Conversion failure between codec-owned typed records and generic records.
22#[derive(Debug, thiserror::Error)]
23pub enum NativeConvertError {
24    /// A serialized typed record has no string `id` field.
25    #[error("native record is missing a string id")]
26    MissingId,
27    /// A typed record did not serialize as a JSON object.
28    #[error("native record did not serialize as an object")]
29    NonObject,
30    /// JSON conversion failed.
31    #[error("native record conversion failed: {0}")]
32    Serde(#[from] serde_json::Error),
33}
34
35/// One source-native record with a stable identity and codec-owned fields.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
37pub struct NativeRecord {
38    /// Globally unique record identity.
39    pub id: String,
40    /// Codec-owned record fields.
41    #[serde(flatten)]
42    pub fields: Map<String, Value>,
43}
44
45/// Independently versioned source-format arena collection.
46#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
47pub struct NativeNamespace {
48    /// Codec-owned namespace schema version.
49    pub version: u32,
50    /// Record arenas keyed by stable arena name.
51    #[serde(default)]
52    pub arenas: BTreeMap<String, Vec<NativeRecord>>,
53}
54
55impl NativeNamespace {
56    /// Replace an arena by serializing codec-owned typed records.
57    pub fn set_arena<T: Serialize>(
58        &mut self,
59        name: impl Into<String>,
60        records: &[T],
61    ) -> Result<(), NativeConvertError> {
62        let mut converted = Vec::with_capacity(records.len());
63        for record in records {
64            let Value::Object(mut fields) = serde_json::to_value(record)? else {
65                return Err(NativeConvertError::NonObject);
66            };
67            let Some(Value::String(id)) = fields.remove("id") else {
68                return Err(NativeConvertError::MissingId);
69            };
70            converted.push(NativeRecord { id, fields });
71        }
72        converted.sort_by(|left, right| left.id.cmp(&right.id));
73        self.arenas.insert(name.into(), converted);
74        Ok(())
75    }
76
77    /// Deserialize an arena into codec-owned typed records.
78    pub fn arena_as<T: DeserializeOwned>(&self, name: &str) -> Result<Vec<T>, NativeConvertError> {
79        self.arenas
80            .get(name)
81            .into_iter()
82            .flatten()
83            .map(|record| {
84                let mut fields = record.fields.clone();
85                fields.insert("id".into(), Value::String(record.id.clone()));
86                Ok(serde_json::from_value(Value::Object(fields))?)
87            })
88            .collect()
89    }
90}
91
92/// Native records grouped by source-format namespace id.
93#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
94#[serde(transparent)]
95pub struct Native(pub BTreeMap<String, NativeNamespace>);
96
97impl Native {
98    /// Return a source-format namespace.
99    pub fn namespace(&self, format: &str) -> Option<&NativeNamespace> {
100        self.0.get(format)
101    }
102
103    /// Return or create a source-format namespace.
104    pub fn namespace_mut(&mut self, format: impl Into<String>) -> &mut NativeNamespace {
105        self.0.entry(format.into()).or_default()
106    }
107
108    /// Sort every arena into canonical identity order.
109    pub(crate) fn finalize(&mut self) {
110        for namespace in self.0.values_mut() {
111            for records in namespace.arenas.values_mut() {
112                records.sort_by(|left, right| left.id.cmp(&right.id));
113            }
114        }
115    }
116
117    /// Return one count for each non-empty native arena.
118    pub fn loss_counts(&self) -> Vec<LossCount> {
119        self.0
120            .iter()
121            .flat_map(|(format, namespace)| {
122                namespace
123                    .arenas
124                    .iter()
125                    .filter(|(_, records)| !records.is_empty())
126                    .map(move |(kind, records)| LossCount {
127                        format: format.clone(),
128                        kind: kind.clone(),
129                        count: records.len(),
130                    })
131            })
132            .collect()
133    }
134}