1#![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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
13pub struct LossCount {
14 pub format: String,
16 pub kind: String,
18 pub count: usize,
20}
21
22#[derive(Debug, thiserror::Error)]
24pub enum NativeConvertError {
25 #[error("native record is missing a string id")]
27 MissingId,
28 #[error("native record did not serialize as an object")]
30 NonObject,
31 #[error("native record conversion failed: {0}")]
33 Serde(#[from] serde_json::Error),
34 #[error("native record has an invalid owner: {0}")]
36 InvalidOwner(String),
37 #[error("native unknown record has no retained source record: {0}")]
39 MissingRetainedSourceRecord(String),
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
44pub struct NativeRecord {
45 pub id: String,
47 #[serde(flatten)]
49 pub fields: Map<String, Value>,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
54pub struct NativeNamespace {
55 pub version: u32,
57 #[serde(default)]
59 pub arenas: BTreeMap<String, Vec<NativeRecord>>,
60}
61
62impl NativeNamespace {
63 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 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#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
103#[serde(transparent)]
104pub struct Native(pub BTreeMap<String, NativeNamespace>);
105
106impl Native {
107 pub fn namespace(&self, format: &str) -> Option<&NativeNamespace> {
109 self.0.get(format)
110 }
111
112 pub fn namespace_mut(&mut self, format: impl Into<String>) -> &mut NativeNamespace {
114 self.0.entry(format.into()).or_default()
115 }
116
117 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 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}