use std::io::Write;
use crate::codec::FormatId;
use crate::document::CadIr;
use crate::report::{
check::{Finding, ValidationReport},
export::{
CensusBasis, EntityCensus, ExportReport, FidelityResolution, ReplayFidelity,
SynthesisFidelity, WritePath as ReportWritePath,
},
loss::LossNote,
Severity,
};
use crate::source_fidelity::SourceFidelity;
use cadmpeg_core::decode::DecodeDepth;
use cadmpeg_core::target::TargetCatalog;
use cadmpeg_core::CodecError;
mod carried;
mod coverage;
mod loss;
pub mod target;
use carried::{EntityLocation, FindingLocator};
pub use coverage::{ArenaCoverage, ArenaDisposition, ArenaDispositions};
use target::{DialectFree, TargetDomain, TargetRequest, TargetResolution};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepthNeed {
AtLeast(DecodeDepth),
Recorded,
}
impl DepthNeed {
#[must_use]
pub const fn request(self) -> DecodeDepth {
match self {
Self::AtLeast(depth) => depth,
Self::Recorded => DecodeDepth::Full,
}
}
#[must_use]
pub fn admits(self, depth: DecodeDepth) -> bool {
match self {
Self::AtLeast(needed) => depth >= needed,
Self::Recorded => true,
}
}
}
pub trait EncoderBackend {
const FORMAT: FormatId;
const NEEDS: DepthNeed;
type Target: TargetDomain;
const TARGET: Self::Target;
fn plan_resolved(
&self,
input: EncodeInput<'_>,
target: <Self::Target as TargetDomain>::Resolved<'_>,
) -> Result<ExportBody, CodecError>;
}
mod encoder_sealed {
pub trait Sealed {}
impl<E: super::EncoderBackend> Sealed for E {}
}
pub trait Encoder: encoder_sealed::Sealed {
fn id(&self) -> FormatId;
fn targets(&self) -> TargetCatalog;
fn needs(&self) -> DepthNeed;
fn plan(
&self,
input: EncodeInput<'_>,
request: TargetRequest<'_>,
) -> Result<ExportPlan, CodecError>;
}
impl<E: EncoderBackend> Encoder for E {
fn id(&self) -> FormatId {
E::FORMAT
}
fn targets(&self) -> TargetCatalog {
E::TARGET.targets(E::FORMAT)
}
fn needs(&self) -> DepthNeed {
E::NEEDS
}
fn plan(
&self,
input: EncodeInput<'_>,
request: TargetRequest<'_>,
) -> Result<ExportPlan, CodecError> {
let recorded = input.ir.decode_depth();
if !E::NEEDS.admits(recorded) {
return Err(CodecError::InsufficientDepth {
format: E::FORMAT.as_str(),
needed: E::NEEDS.request(),
recorded,
});
}
let target = E::TARGET.resolve(input.ir, request, E::FORMAT)?;
let identity = target.export_target().cloned();
let body = self.plan_resolved(input, target)?;
let ExportBody {
bytes,
census,
write_path,
coverage,
mut losses,
notes,
} = body;
losses.extend(coverage.omission_losses(&input.ir.model, E::FORMAT.as_str()));
let write_path = write_path.into_report(input.fidelity.is_some());
let report = match identity {
None => ExportReport::cadir(census, write_path, losses, notes),
Some(target) => ExportReport::native(target, census, write_path, losses, notes),
};
Ok(ExportPlan {
report,
bytes,
format: E::FORMAT,
coverage,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct EncodeInput<'a> {
pub ir: &'a CadIr,
pub fidelity: Option<&'a SourceFidelity>,
}
impl<'a> EncodeInput<'a> {
#[must_use]
pub const fn new(ir: &'a CadIr, fidelity: Option<&'a SourceFidelity>) -> Self {
Self { ir, fidelity }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Consumption {
NotConsumed,
Degraded {
reason: String,
},
}
impl From<Consumption> for FidelityResolution {
fn from(consumption: Consumption) -> Self {
match consumption {
Consumption::NotConsumed => Self::NotConsumed {},
Consumption::Degraded { reason } => Self::Degraded { reason },
}
}
}
impl From<Consumption> for SynthesisFidelity {
fn from(consumption: Consumption) -> Self {
match consumption {
Consumption::NotConsumed => Self::NotConsumed {},
Consumption::Degraded { reason } => Self::Degraded { reason },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatchConsumption {
Replayed,
Independent(Consumption),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WritePath {
Synthesized {
consumption: Consumption,
},
Patched {
consumption: PatchConsumption,
},
VerbatimReplay,
}
impl WritePath {
fn into_report(self, fidelity_provided: bool) -> ReportWritePath {
match self {
Self::Synthesized { consumption } => ReportWritePath::Synthesized {
fidelity: if fidelity_provided {
consumption.into()
} else {
SynthesisFidelity::NotProvided {}
},
},
Self::Patched {
consumption: PatchConsumption::Replayed,
} => ReportWritePath::Patched {
fidelity: if fidelity_provided {
FidelityResolution::Replayed {}
} else {
FidelityResolution::NotProvided {}
},
},
Self::Patched {
consumption: PatchConsumption::Independent(consumption),
} => ReportWritePath::Patched {
fidelity: if fidelity_provided {
consumption.into()
} else {
FidelityResolution::NotProvided {}
},
},
Self::VerbatimReplay => ReportWritePath::VerbatimReplay {
fidelity: if fidelity_provided {
ReplayFidelity::Replayed {}
} else {
ReplayFidelity::NotProvided {}
},
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExportBody {
pub bytes: Vec<u8>,
pub census: EntityCensus,
pub write_path: WritePath,
pub coverage: ArenaCoverage,
pub losses: Vec<LossNote>,
pub notes: Vec<String>,
}
impl ExportBody {
#[must_use]
pub fn synthesized(bytes: Vec<u8>, ir: &CadIr, coverage: ArenaCoverage) -> Self {
Self {
bytes,
census: EntityCensus {
basis: CensusBasis::IrArenas,
counts: ir.census(),
},
write_path: WritePath::Synthesized {
consumption: Consumption::NotConsumed,
},
coverage,
losses: Vec::new(),
notes: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct ExportPlan {
format: FormatId,
coverage: ArenaCoverage,
report: ExportReport,
bytes: Vec<u8>,
}
impl ExportPlan {
pub fn report(&self) -> &ExportReport {
&self.report
}
pub fn carried_errors<'r>(&self, ir: &CadIr, report: &'r ValidationReport) -> Vec<&'r Finding> {
let errors: Vec<_> = report
.findings
.iter()
.filter(|finding| finding.severity >= Severity::Error)
.collect();
if errors.is_empty() {
return errors;
}
let locator = FindingLocator::new(ir, errors.iter().copied());
errors
.into_iter()
.filter(|finding| {
locator.scope(finding).is_none_or(|locations| {
locations
.into_iter()
.any(|location| self.carries_location(location))
})
})
.collect()
}
fn carries_location(&self, location: EntityLocation<'_>) -> bool {
match location {
EntityLocation::Model(kind) => self.coverage.carries(kind),
EntityLocation::Native(namespace) => {
self.coverage == ArenaCoverage::Complete || namespace == self.format.as_str()
}
}
}
pub fn write_to(self, writer: &mut dyn Write) -> Result<ExportReport, CodecError> {
writer.write_all(&self.bytes)?;
Ok(self.report)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CadirEncoder;
impl EncoderBackend for CadirEncoder {
const FORMAT: FormatId = FormatId::new("cadir");
const NEEDS: DepthNeed = DepthNeed::Recorded;
type Target = DialectFree;
const TARGET: DialectFree = DialectFree;
fn plan_resolved(&self, input: EncodeInput<'_>, (): ()) -> Result<ExportBody, CodecError> {
let mut bytes = crate::hash::finite_json::to_canonical_json_string(input.ir)
.map_err(|error| CodecError::Malformed(error.to_string()))?
.into_bytes();
bytes.push(b'\n');
Ok(ExportBody::synthesized(
bytes,
input.ir,
ArenaCoverage::Complete,
))
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod test_support;