use cadmpeg_core::dialect::{DialectLayers, FormatIdentity};
use cadmpeg_core::ContainerEntry;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::report::loss::LossNote;
use serde::de::Error as _;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContainerKind {
ThreeDmChunks,
Binary,
Cfb,
CompoundFileBinary,
CompressedAscii,
FixedAscii,
Flat,
Iso10303ClearText,
Iso10303Zip,
Psb,
SldprtBlocks,
Splmsstr,
Stream,
V5Cfv2,
Zip,
}
impl ContainerKind {
#[must_use]
pub fn parse(id: &str) -> Option<Self> {
Some(match id {
"3dm-chunks" => Self::ThreeDmChunks,
"binary" => Self::Binary,
"cfb" => Self::Cfb,
"compound-file-binary" => Self::CompoundFileBinary,
"compressed-ascii" => Self::CompressedAscii,
"fixed-ascii" => Self::FixedAscii,
"flat" => Self::Flat,
"iso-10303-21-clear-text" => Self::Iso10303ClearText,
"iso-10303-21-zip" => Self::Iso10303Zip,
"psb" => Self::Psb,
"sldprt-blocks" => Self::SldprtBlocks,
"splmsstr" => Self::Splmsstr,
"stream" => Self::Stream,
"v5-cfv2" => Self::V5Cfv2,
"zip" => Self::Zip,
_ => return None,
})
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ThreeDmChunks => "3dm-chunks",
Self::Binary => "binary",
Self::Cfb => "cfb",
Self::CompoundFileBinary => "compound-file-binary",
Self::CompressedAscii => "compressed-ascii",
Self::FixedAscii => "fixed-ascii",
Self::Flat => "flat",
Self::Iso10303ClearText => "iso-10303-21-clear-text",
Self::Iso10303Zip => "iso-10303-21-zip",
Self::Psb => "psb",
Self::SldprtBlocks => "sldprt-blocks",
Self::Splmsstr => "splmsstr",
Self::Stream => "stream",
Self::V5Cfv2 => "v5-cfv2",
Self::Zip => "zip",
}
}
}
impl fmt::Display for ContainerKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl PartialEq<str> for ContainerKind {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for ContainerKind {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl Serialize for ContainerKind {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ContainerKind {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let id = String::deserialize(deserializer)?;
Self::parse(&id)
.ok_or_else(|| D::Error::custom(format!("container_kind: unknown value {id}")))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ContainerSummary {
identity: FormatIdentity<DialectLayers>,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub container_kind: ContainerKind,
pub entries: Vec<ContainerEntry>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub losses: Vec<LossNote>,
pub notes: Vec<String>,
}
impl ContainerSummary {
#[must_use]
pub fn classified(
dialects: DialectLayers,
container_kind: ContainerKind,
entries: Vec<ContainerEntry>,
losses: Vec<LossNote>,
notes: Vec<String>,
) -> Self {
Self {
identity: FormatIdentity::classified(dialects),
container_kind,
entries,
losses,
notes,
}
}
#[must_use]
pub fn format(&self) -> &str {
self.identity.format()
}
#[must_use]
pub const fn dialects(&self) -> Option<&DialectLayers> {
self.identity.classified_payload()
}
}
#[cfg(test)]
mod tests {
use cadmpeg_core::dialect::{DialectLayers, DialectMatch};
use super::{ContainerKind, ContainerSummary};
#[test]
fn an_unclassified_summary_states_its_format_inside_the_identity() {
let summary = ContainerSummary {
identity: cadmpeg_core::dialect::FormatIdentity::unclassified("rhino"),
container_kind: ContainerKind::Flat,
entries: Vec::new(),
losses: Vec::new(),
notes: Vec::new(),
};
let bare = serde_json::to_value(&summary).expect("a summary serializes");
assert!(bare.get("losses").is_none(), "{bare}");
assert_eq!(bare["identity"]["classification"], "unclassified");
assert_eq!(bare["identity"]["format"], "rhino");
assert!(bare.get("format").is_none(), "{bare}");
assert_eq!(
serde_json::from_value::<ContainerSummary>(bare).expect("a summary round-trips"),
summary
);
}
#[test]
fn a_classified_summary_carries_its_format_once() {
let primary = DialectMatch::admitted(cadmpeg_core::dialect_id!("rhino:archive-80"));
let extra = DialectMatch::admitted(cadmpeg_core::dialect_id!("acis:save-format-217"));
let summary = ContainerSummary::classified(
DialectLayers::of(primary.clone())
.with(extra.clone())
.expect("the test dialect layers have distinct keys"),
ContainerKind::Flat,
Vec::new(),
Vec::new(),
Vec::new(),
);
let classified = serde_json::to_value(&summary).expect("a summary serializes");
assert_eq!(classified["identity"]["classification"], "classified");
assert_eq!(
classified["identity"]["dialects"],
serde_json::json!({"primary": primary, "extra": [extra]})
);
assert!(classified["identity"].get("format").is_none());
let restored: ContainerSummary =
serde_json::from_value(classified.clone()).expect("classified summary reads");
assert_eq!(
restored
.dialects()
.expect("the summary remains classified")
.primary()
.format(),
"rhino"
);
let mut restated = classified;
restated["identity"]["format"] = serde_json::json!("step");
let error = serde_json::from_value::<ContainerSummary>(restated)
.expect_err("a classified identity carries no second format");
assert!(error.to_string().contains("format"), "{error}");
}
#[cfg(feature = "schema")]
#[test]
fn current_summary_schema_requires_its_identity() {
let schema = serde_json::to_value(schemars::schema_for!(ContainerSummary))
.expect("summary schema serializes");
let required = schema["required"]
.as_array()
.expect("summary schema has required fields");
assert!(
required.iter().any(|field| field == "identity"),
"{schema:#}"
);
assert!(
!required.iter().any(|field| field == "losses"),
"{schema:#}"
);
}
}