use std::collections::BTreeMap;
use cadmpeg_core::dialect::{DialectLayers, FormatIdentity};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::report::loss::LossNote;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(into = "DecodeReportWire", from = "DecodeReportWire")]
#[serde(deny_unknown_fields)]
pub struct DecodeReport {
classification: FormatIdentity<DialectLayers>,
transfer: DecodeTransfer,
coverage: Coverage,
pub losses: Vec<LossNote>,
pub notes: Vec<String>,
pub transfer_ledger: TransferLedger,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "transfer", rename_all = "snake_case", deny_unknown_fields)]
#[non_exhaustive]
pub enum DecodeTransfer {
ContainerOnly {},
Full {
geometry_transferred: bool,
},
}
impl DecodeTransfer {
#[must_use]
pub const fn full(geometry_transferred: bool) -> Self {
Self::Full {
geometry_transferred,
}
}
const fn stamp_request_scope(container_only: bool, transfer: Self) -> Self {
if container_only {
Self::ContainerOnly {}
} else {
Self::full(transfer.geometry_transferred())
}
}
#[must_use]
pub const fn container_only(self) -> bool {
matches!(self, Self::ContainerOnly {})
}
#[must_use]
pub const fn geometry_transferred(self) -> bool {
matches!(
self,
Self::Full {
geometry_transferred: true
}
)
}
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct DecodeReportWire {
identity: FormatIdentity<DialectLayers>,
transfer: DecodeTransfer,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
coverage: BTreeMap<String, usize>,
losses: Vec<LossNote>,
notes: Vec<String>,
#[serde(default, skip_serializing_if = "TransferLedger::is_empty")]
transfer_ledger: TransferLedger,
}
impl From<DecodeReport> for DecodeReportWire {
fn from(report: DecodeReport) -> Self {
let DecodeReport {
classification,
transfer,
coverage,
losses,
notes,
transfer_ledger,
} = report;
Self {
identity: classification,
transfer,
coverage: coverage.into_wire(),
losses,
notes,
transfer_ledger,
}
}
}
impl From<DecodeReportWire> for DecodeReport {
fn from(wire: DecodeReportWire) -> Self {
Self {
classification: wire.identity,
transfer: wire.transfer,
coverage: Coverage::from_wire(wire.coverage),
losses: wire.losses,
notes: wire.notes,
transfer_ledger: wire.transfer_ledger,
}
}
}
#[cfg(feature = "schema")]
impl JsonSchema for DecodeReport {
fn schema_name() -> std::borrow::Cow<'static, str> {
"DecodeReport".into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
concat!(module_path!(), "::DecodeReport").into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
DecodeReportWire::json_schema(generator)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum TransferDisposition {
Emitted,
Retained,
Approximated,
Omitted,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "disposition", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum TransferOutcome {
Emitted {
target: String,
},
Retained {
target: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_note"
)]
note: Option<String>,
},
Approximated {
target: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_note"
)]
note: Option<String>,
},
Omitted {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_note"
)]
note: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TransferRecord {
pub source: String,
pub outcome: TransferOutcome,
}
impl TransferRecord {
#[must_use]
pub fn target(&self) -> Option<&str> {
match &self.outcome {
TransferOutcome::Emitted { target }
| TransferOutcome::Retained { target, .. }
| TransferOutcome::Approximated { target, .. } => Some(target),
TransferOutcome::Omitted { .. } => None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TransferLedger {
pub entries: Vec<TransferRecord>,
}
impl TransferLedger {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn record(&mut self, source: impl Into<String>, outcome: TransferOutcome) {
self.entries.push(TransferRecord {
source: source.into(),
outcome,
});
}
pub fn verify(&self, index: &crate::index::ModelIndex<'_>) -> Result<(), String> {
for entry in &self.entries {
if let Some(target) = entry
.target()
.filter(|target| !index.contains(target, crate::index::StandardIndex))
{
return Err(format!(
"transfer source {:?} targets unresolved identity {:?}",
entry.source, target
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CoverageKey(&'static str);
impl CoverageKey {
#[must_use]
pub const fn new(name: &'static str) -> Self {
Self(name)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexedCoverageKey {
prefix: &'static str,
suffix: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HexByteCoverageKey {
prefix: &'static str,
suffix: &'static str,
}
impl IndexedCoverageKey {
#[must_use]
pub const fn decimal(prefix: &'static str, suffix: &'static str) -> Self {
Self { prefix, suffix }
}
}
impl HexByteCoverageKey {
#[must_use]
pub const fn new(prefix: &'static str, suffix: &'static str) -> Self {
Self { prefix, suffix }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Coverage {
entries: BTreeMap<String, usize>,
}
impl Coverage {
pub fn record(
&mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
key: CoverageKey,
count: usize,
) -> Result<(), cadmpeg_core::CodecError> {
ctx.charge_work(1, "decode coverage lookup")?;
if let Some(value) = self.entries.get_mut(key.0) {
*value = count;
return Ok(());
}
let name = ctx.copy_retained_text(key.0, "decode coverage names")?;
self.insert_name(ctx, name, count)
}
pub fn record_indexed(
&mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
key: IndexedCoverageKey,
index: u32,
count: usize,
) -> Result<(), cadmpeg_core::CodecError> {
let (name, bytes) = ctx.format_scoped(
format_args!("{}{index}{}", key.prefix, key.suffix),
"decode indexed coverage name",
)?;
ctx.charge_work(1, "decode coverage lookup")?;
if let Some(value) = self.entries.get_mut(&name) {
*value = count;
return Ok(());
}
bytes.commit()?;
self.insert_name(ctx, name, count)
}
pub fn record_hex_byte(
&mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
key: HexByteCoverageKey,
value: u8,
count: usize,
) -> Result<(), cadmpeg_core::CodecError> {
let (name, bytes) = ctx.format_scoped(
format_args!("{}{:02x}{}", key.prefix, value, key.suffix),
"decode hexadecimal coverage name",
)?;
ctx.charge_work(1, "decode coverage lookup")?;
if let Some(existing) = self.entries.get_mut(&name) {
*existing = count;
return Ok(());
}
bytes.commit()?;
self.insert_name(ctx, name, count)
}
pub fn record_owned(
&mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
key: CoverageKey,
name: String,
count: usize,
) -> Result<(), cadmpeg_core::CodecError> {
ctx.charge_work(
cadmpeg_core::decode::u64_from_index(name.len()),
"decode coverage name admission",
)?;
if name != key.0 {
return Err(cadmpeg_core::CodecError::malformed(
"coverage name does not match its declared key",
));
}
self.insert_name(ctx, name, count)
}
fn insert_name(
&mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
name: String,
count: usize,
) -> Result<(), cadmpeg_core::CodecError> {
ctx.charge_work(1, "decode coverage lookup")?;
if let Some(value) = self.entries.get_mut(&name) {
*value = count;
return Ok(());
}
ctx.charge_work(1, "decode coverage nodes")?;
ctx.insert_btree_map(&mut self.entries, name, count, "decode coverage nodes")?;
Ok(())
}
pub fn from_iter_for_decode(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
entries: impl IntoIterator<Item = (CoverageKey, usize)>,
) -> Result<Self, cadmpeg_core::CodecError> {
let mut coverage = Self::default();
for (key, count) in entries {
coverage.record(ctx, key, count)?;
}
Ok(coverage)
}
#[must_use]
fn as_map(&self) -> &BTreeMap<String, usize> {
&self.entries
}
fn from_wire(entries: BTreeMap<String, usize>) -> Self {
Self { entries }
}
fn into_wire(self) -> BTreeMap<String, usize> {
self.entries
}
}
impl Extend<(CoverageKey, usize)> for Coverage {
fn extend<T: IntoIterator<Item = (CoverageKey, usize)>>(&mut self, iter: T) {
for (key, count) in iter {
self.entries.insert(key.0.to_owned(), count);
}
}
}
impl FromIterator<(CoverageKey, usize)> for Coverage {
fn from_iter<T: IntoIterator<Item = (CoverageKey, usize)>>(iter: T) -> Self {
let mut coverage = Self::default();
coverage.extend(iter);
coverage
}
}
impl std::ops::Deref for Coverage {
type Target = BTreeMap<String, usize>;
fn deref(&self) -> &Self::Target {
self.as_map()
}
}
impl DecodeReport {
#[must_use]
pub(crate) fn from_body(
classification: FormatIdentity<DialectLayers>,
body: crate::codec::DecodeBody,
container_only: bool,
) -> Self {
Self {
classification,
transfer: DecodeTransfer::stamp_request_scope(container_only, body.transfer),
coverage: body.coverage,
losses: body.losses,
notes: body.notes,
transfer_ledger: body.transfer_ledger,
}
}
#[must_use]
#[cfg(test)]
fn classified(
dialects: DialectLayers,
transfer: DecodeTransfer,
coverage: BTreeMap<String, usize>,
losses: Vec<LossNote>,
notes: Vec<String>,
transfer_ledger: TransferLedger,
) -> Self {
Self {
classification: FormatIdentity::classified(dialects),
transfer,
coverage: Coverage::from_wire(coverage),
losses,
notes,
transfer_ledger,
}
}
#[must_use]
#[cfg(test)]
pub(crate) fn unclassified(
format: impl Into<String>,
transfer: DecodeTransfer,
coverage: BTreeMap<String, usize>,
losses: Vec<LossNote>,
notes: Vec<String>,
transfer_ledger: TransferLedger,
) -> Self {
Self {
classification: FormatIdentity::unclassified(format),
transfer,
coverage: Coverage::from_wire(coverage),
losses,
notes,
transfer_ledger,
}
}
#[must_use]
pub fn format(&self) -> &str {
self.classification.format()
}
#[must_use]
pub fn dialects(&self) -> Option<&DialectLayers> {
self.classification.classified_payload()
}
#[must_use]
pub const fn container_only(&self) -> bool {
self.transfer.container_only()
}
#[must_use]
pub const fn geometry_transferred(&self) -> bool {
self.transfer.geometry_transferred()
}
}
cadmpeg_core::named_optional_field!(deserialize_note, String, "note");
#[cfg(test)]
mod tests;