#![deny(clippy::disallowed_methods)]
use std::collections::BTreeMap;
#[cfg(feature = "schema")]
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
pub mod catalogue;
mod replay;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct LossCount {
pub format: String,
pub kind: String,
pub count: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum NativeConvertError {
#[error(transparent)]
UnsupportedVersion(#[from] catalogue::NativeVersionError),
#[error("native record is missing a string id")]
MissingId,
#[error("native record did not serialize as an object")]
NonObject,
#[error("native record conversion failed: {0}")]
Serde(#[from] serde_json::Error),
#[error("native record has an invalid owner: {0}")]
InvalidOwner(String),
#[error("native unknown record has no retained source record: {0}")]
MissingRetainedSourceRecord(String),
}
impl From<NativeConvertError> for cadmpeg_core::CodecError {
fn from(error: NativeConvertError) -> Self {
Self::Malformed(error.to_string())
}
}
#[derive(Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename = "NativeRecord")]
struct RecordShape<'a> {
id: &'a str,
#[serde(flatten)]
fields: &'a Map<String, Value>,
}
#[derive(Debug, Clone)]
pub struct NativeRecord {
id: String,
json: Box<str>,
}
impl NativeRecord {
#[must_use]
pub fn new(id: impl Into<String>, mut fields: Map<String, Value>) -> Self {
let id = id.into();
fields.remove("id");
let json = Self::canonical_json(&id, &fields);
Self { id, json }
}
fn from_typed<T: Serialize>(record: &T) -> Result<Self, NativeConvertError> {
let Value::Object(mut fields) = serde_json::to_value(record)? else {
return Err(NativeConvertError::NonObject);
};
let Some(Value::String(id)) = fields.remove("id") else {
return Err(NativeConvertError::MissingId);
};
let json = Self::canonical_json(&id, &fields);
Ok(Self { id, json })
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn fields(&self) -> Map<String, Value> {
let mut fields: Map<String, Value> =
serde_json::from_str(&self.json).expect("a native record always holds a JSON object");
fields.remove("id");
fields
}
#[must_use]
pub fn field(&self, name: &str) -> Option<Value> {
if name == "id" {
return None;
}
replay::field(&self.json, name)
}
fn to_typed<T: DeserializeOwned>(&self) -> Result<T, NativeConvertError> {
Ok(serde_json::from_str(&self.json)?)
}
fn canonical_json(id: &str, fields: &Map<String, Value>) -> Box<str> {
serde_json::to_string(&RecordShape { id, fields })
.expect("a JSON object of JSON values always serializes")
.into_boxed_str()
}
}
impl PartialEq for NativeRecord {
fn eq(&self, other: &Self) -> bool {
self.json == other.json
}
}
impl Serialize for NativeRecord {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
replay::emit(&self.json, serializer)
}
}
impl<'de> Deserialize<'de> for NativeRecord {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let mut fields = Map::<String, Value>::deserialize(deserializer)?;
let Some(Value::String(id)) = fields.remove("id") else {
return Err(<D::Error as serde::de::Error>::custom(
NativeConvertError::MissingId,
));
};
let json = Self::canonical_json(&id, &fields);
Ok(Self { id, json })
}
}
#[cfg(feature = "schema")]
impl JsonSchema for NativeRecord {
fn schema_name() -> std::borrow::Cow<'static, str> {
"NativeRecord".into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
concat!(module_path!(), "::NativeRecord").into()
}
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
RecordShape::json_schema(generator)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct NativeNamespace {
pub version: u32,
#[serde(default)]
pub arenas: BTreeMap<String, Vec<NativeRecord>>,
}
impl NativeNamespace {
pub fn set_arena<T: Serialize>(
&mut self,
name: impl Into<String>,
records: &[T],
) -> Result<(), NativeConvertError> {
self.set_arena_from(name, records.iter())
}
pub fn set_arena_from<T: Serialize, I: IntoIterator<Item = T>>(
&mut self,
name: impl Into<String>,
records: I,
) -> Result<(), NativeConvertError> {
let mut converted = records
.into_iter()
.map(|record| NativeRecord::from_typed(&record))
.collect::<Result<Vec<_>, NativeConvertError>>()?;
converted.sort_by(|left, right| left.id().cmp(right.id()));
self.arenas.insert(name.into(), converted);
Ok(())
}
pub fn arena_as<T: DeserializeOwned>(&self, name: &str) -> Result<Vec<T>, NativeConvertError> {
self.arena_iter_as(name).collect()
}
pub fn arena_iter_as<'a, T: DeserializeOwned + 'a>(
&'a self,
name: &str,
) -> impl Iterator<Item = Result<T, NativeConvertError>> + 'a {
self.arenas
.get(name)
.into_iter()
.flatten()
.map(NativeRecord::to_typed)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(transparent)]
pub struct Native(pub BTreeMap<String, NativeNamespace>);
impl Native {
pub fn namespace(&self, format: &str) -> Option<&NativeNamespace> {
self.0.get(format)
}
pub fn namespace_mut(&mut self, format: impl Into<String>) -> &mut NativeNamespace {
self.0.entry(format.into()).or_default()
}
pub(crate) fn finalize(&mut self) {
for namespace in self.0.values_mut() {
for records in namespace.arenas.values_mut() {
records.sort_by(|left, right| left.id().cmp(right.id()));
}
}
}
pub fn loss_counts(&self) -> Vec<LossCount> {
self.0
.iter()
.flat_map(|(format, namespace)| {
namespace
.arenas
.iter()
.filter(|(_, records)| !records.is_empty())
.map(move |(kind, records)| LossCount {
format: format.clone(),
kind: kind.clone(),
count: records.len(),
})
})
.collect()
}
}
#[cfg(test)]
mod tests;