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