use std::fmt;
use crate::provenance::SourceProvenance;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::report::Severity;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum LossCategory {
Geometry,
Topology,
Material,
Metadata,
Attribute,
DesignIntent,
Product,
Other,
}
impl fmt::Display for LossCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Geometry => "geometry",
Self::Topology => "topology",
Self::Material => "material",
Self::Metadata => "metadata",
Self::Attribute => "attribute",
Self::DesignIntent => "design_intent",
Self::Product => "product",
Self::Other => "other",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum StrictConsequence {
Reject,
Tolerate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub enum LossTaxonomy {
MissingGeometryStream,
TopologyNotTransferred,
SourceTopologyInvalid,
GeometryNotTransferred,
ReferenceGraphNotClosed,
TopologyGaugeSubstituted,
CarrierAxisInferred,
CarrierSummary,
MaterialNotTransferred,
MetadataNotTransferred,
AttributesNotTransferred,
FeatureHistoryRetained,
AssemblyComponentsExternal,
AssemblyPlacementsNotTransferred,
RecordNotTyped,
DecodeDiagnostic,
IntegrityFailure,
NoncanonicalSourceSyntax,
SourceDialectUnverified,
SourceDialectDisplaced,
MeshVertexPrecision,
ObjectRecordsUntransferred,
UnsupportedObjectFamily,
AssetNotTransferred,
NoExportableSolids,
HiddenBodyOmitted,
BodyTransformNotApplied,
AnalyticSurfaceNormalized,
EllipticalConeReduced,
CurvelessEdgeOmitted,
UnknownSurfaceFaceOmitted,
PcurveOmitted,
SubdOmitted,
TessellationOmitted,
PmiOmitted,
SourceAssociationOmitted,
PassthroughRecordOmitted,
ProceduralReduced,
ParametricRecordOmitted,
AppearanceReduced,
PreservedSourceUnavailable,
}
impl LossTaxonomy {
pub const fn as_str(self) -> &'static str {
match self {
Self::MissingGeometryStream => "missing_geometry_stream",
Self::TopologyNotTransferred => "topology_not_transferred",
Self::SourceTopologyInvalid => "source_topology_invalid",
Self::GeometryNotTransferred => "geometry_not_transferred",
Self::ReferenceGraphNotClosed => "reference_graph_not_closed",
Self::TopologyGaugeSubstituted => "topology_gauge_substituted",
Self::CarrierAxisInferred => "carrier_axis_inferred",
Self::CarrierSummary => "carrier_summary",
Self::MaterialNotTransferred => "material_not_transferred",
Self::MetadataNotTransferred => "metadata_not_transferred",
Self::AttributesNotTransferred => "attributes_not_transferred",
Self::FeatureHistoryRetained => "feature_history_retained",
Self::AssemblyComponentsExternal => "assembly_components_external",
Self::AssemblyPlacementsNotTransferred => "assembly_placements_not_transferred",
Self::RecordNotTyped => "record_not_typed",
Self::DecodeDiagnostic => "decode_diagnostic",
Self::IntegrityFailure => "integrity_failure",
Self::NoncanonicalSourceSyntax => "noncanonical_source_syntax",
Self::SourceDialectUnverified => "source_dialect_unverified",
Self::SourceDialectDisplaced => "source_dialect_displaced",
Self::MeshVertexPrecision => "mesh_vertex_precision",
Self::ObjectRecordsUntransferred => "object_records_untransferred",
Self::UnsupportedObjectFamily => "unsupported_object_family",
Self::AssetNotTransferred => "asset_not_transferred",
Self::NoExportableSolids => "no_exportable_solids",
Self::HiddenBodyOmitted => "hidden_body_omitted",
Self::BodyTransformNotApplied => "body_transform_not_applied",
Self::AnalyticSurfaceNormalized => "analytic_surface_normalized",
Self::EllipticalConeReduced => "elliptical_cone_reduced",
Self::CurvelessEdgeOmitted => "curveless_edge_omitted",
Self::UnknownSurfaceFaceOmitted => "unknown_surface_face_omitted",
Self::PcurveOmitted => "pcurve_omitted",
Self::SubdOmitted => "subd_omitted",
Self::TessellationOmitted => "tessellation_omitted",
Self::PmiOmitted => "pmi_omitted",
Self::SourceAssociationOmitted => "source_association_omitted",
Self::PassthroughRecordOmitted => "passthrough_record_omitted",
Self::ProceduralReduced => "procedural_reduced",
Self::ParametricRecordOmitted => "parametric_record_omitted",
Self::AppearanceReduced => "appearance_reduced",
Self::PreservedSourceUnavailable => "preserved_source_unavailable",
}
}
pub const fn category(self) -> LossCategory {
match self {
Self::TopologyNotTransferred
| Self::SourceTopologyInvalid
| Self::ReferenceGraphNotClosed
| Self::TopologyGaugeSubstituted
| Self::NoExportableSolids
| Self::HiddenBodyOmitted => LossCategory::Topology,
Self::MaterialNotTransferred | Self::AppearanceReduced => LossCategory::Material,
Self::MetadataNotTransferred | Self::SourceAssociationOmitted => LossCategory::Metadata,
Self::AttributesNotTransferred | Self::PmiOmitted => LossCategory::Attribute,
Self::FeatureHistoryRetained | Self::ParametricRecordOmitted => {
LossCategory::DesignIntent
}
Self::AssemblyComponentsExternal | Self::AssemblyPlacementsNotTransferred => {
LossCategory::Product
}
Self::RecordNotTyped
| Self::DecodeDiagnostic
| Self::IntegrityFailure
| Self::NoncanonicalSourceSyntax
| Self::SourceDialectUnverified
| Self::SourceDialectDisplaced
| Self::AssetNotTransferred
| Self::PassthroughRecordOmitted
| Self::PreservedSourceUnavailable => LossCategory::Other,
Self::MissingGeometryStream
| Self::GeometryNotTransferred
| Self::CarrierAxisInferred
| Self::CarrierSummary
| Self::MeshVertexPrecision
| Self::ObjectRecordsUntransferred
| Self::UnsupportedObjectFamily
| Self::BodyTransformNotApplied
| Self::AnalyticSurfaceNormalized
| Self::EllipticalConeReduced
| Self::CurvelessEdgeOmitted
| Self::UnknownSurfaceFaceOmitted
| Self::PcurveOmitted
| Self::SubdOmitted
| Self::TessellationOmitted
| Self::ProceduralReduced => LossCategory::Geometry,
}
}
pub const fn default_severity(self) -> Severity {
match self {
Self::CarrierSummary | Self::PassthroughRecordOmitted => Severity::Info,
Self::MissingGeometryStream | Self::NoExportableSolids | Self::IntegrityFailure => {
Severity::Error
}
_ => Severity::Warning,
}
}
pub const fn strict_floor(self) -> Option<Severity> {
match self {
Self::MissingGeometryStream
| Self::TopologyNotTransferred
| Self::GeometryNotTransferred
| Self::ReferenceGraphNotClosed
| Self::CurvelessEdgeOmitted
| Self::UnknownSurfaceFaceOmitted
| Self::SubdOmitted
| Self::NoncanonicalSourceSyntax
| Self::SourceDialectUnverified
| Self::SourceDialectDisplaced
| Self::IntegrityFailure
| Self::NoExportableSolids => Some(Severity::Warning),
_ => None,
}
}
}
impl fmt::Display for LossTaxonomy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub const SHARED_LOSS_NAMESPACE: &str = "shared";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "scope", rename_all = "snake_case", deny_unknown_fields)]
pub enum LossKind {
Shared {
kind: LossTaxonomy,
},
Namespaced(NamespacedLossKind),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct LossNamespace<'a>(&'a str);
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("LossKind.namespace cannot be shared for a namespaced loss")]
pub struct LossNamespaceError;
impl<'a> LossNamespace<'a> {
pub const fn new(namespace: &'a str) -> Result<Self, LossNamespaceError> {
if matches!(namespace.as_bytes(), b"shared") {
Err(LossNamespaceError)
} else {
Ok(Self(namespace))
}
}
pub fn as_str(&self) -> &str {
self.0
}
}
#[macro_export]
macro_rules! loss_namespace {
($value:literal $(,)?) => {{
const STATIC_LOSS_NAMESPACE: $crate::report::loss::LossNamespace<'static> =
match $crate::report::loss::LossNamespace::new($value) {
Ok(namespace) => namespace,
Err(_) => panic!("loss namespace literal is reserved"),
};
STATIC_LOSS_NAMESPACE
}};
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "String", into = "String")]
struct LossNamespaceName(String);
impl LossNamespaceName {
fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for LossNamespaceName {
type Error = LossNamespaceError;
fn try_from(namespace: String) -> Result<Self, Self::Error> {
LossNamespace::new(namespace.as_str())?;
Ok(Self(namespace))
}
}
impl From<LossNamespaceName> for String {
fn from(namespace: LossNamespaceName) -> Self {
namespace.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct NamespacedLossKind {
namespace: LossNamespaceName,
code: String,
#[serde(rename = "kind")]
taxonomy: LossTaxonomy,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_strict_floor"
)]
strict_floor: Option<Severity>,
}
impl LossKind {
pub fn try_clone_for_decode(
&self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
operation: &'static str,
) -> Result<Self, cadmpeg_core::CodecError> {
ctx.charge_work(1, operation)?;
match self {
Self::Shared { kind } => Ok(Self::Shared { kind: *kind }),
Self::Namespaced(kind) => Ok(Self::Namespaced(NamespacedLossKind {
namespace: LossNamespaceName(
ctx.copy_retained_text(kind.namespace.as_str(), operation)?,
),
code: ctx.copy_retained_text(&kind.code, operation)?,
taxonomy: kind.taxonomy,
strict_floor: kind.strict_floor,
})),
}
}
pub fn shared(taxonomy: LossTaxonomy) -> Self {
Self::Shared { kind: taxonomy }
}
pub fn namespaced(
namespace: LossNamespace<'_>,
code: impl Into<String>,
taxonomy: LossTaxonomy,
) -> Self {
NamespacedLossKind::new(namespace, code, taxonomy).into()
}
pub fn namespace(&self) -> &str {
match self {
Self::Shared { .. } => SHARED_LOSS_NAMESPACE,
Self::Namespaced(kind) => kind.namespace.as_str(),
}
}
pub fn local_code(&self) -> &str {
match self {
Self::Shared { kind } => kind.as_str(),
Self::Namespaced(kind) => &kind.code,
}
}
pub const fn taxonomy(&self) -> LossTaxonomy {
match self {
Self::Shared { kind } => *kind,
Self::Namespaced(kind) => kind.taxonomy,
}
}
pub const fn category(&self) -> LossCategory {
self.taxonomy().category()
}
pub const fn default_severity(&self) -> Severity {
self.taxonomy().default_severity()
}
pub const fn strict_floor(&self) -> Option<Severity> {
match self {
Self::Shared { kind } => kind.strict_floor(),
Self::Namespaced(kind) => kind.strict_floor,
}
}
}
impl NamespacedLossKind {
pub fn new(
namespace: LossNamespace<'_>,
code: impl Into<String>,
taxonomy: LossTaxonomy,
) -> Self {
Self {
namespace: LossNamespaceName(namespace.as_str().to_owned()),
code: code.into(),
taxonomy,
strict_floor: taxonomy.strict_floor(),
}
}
pub fn new_owned(
namespace: String,
code: String,
taxonomy: LossTaxonomy,
) -> Result<Self, LossNamespaceError> {
Ok(Self {
namespace: LossNamespaceName::try_from(namespace)?,
code,
taxonomy,
strict_floor: taxonomy.strict_floor(),
})
}
#[must_use]
pub fn with_strict_floor(mut self, floor: Option<Severity>) -> Self {
self.strict_floor = floor;
self
}
}
impl From<NamespacedLossKind> for LossKind {
fn from(kind: NamespacedLossKind) -> Self {
Self::Namespaced(kind)
}
}
impl From<LossTaxonomy> for LossKind {
fn from(taxonomy: LossTaxonomy) -> Self {
Self::shared(taxonomy)
}
}
impl fmt::Display for LossKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.namespace(), self.local_code())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LossNote {
pub code: LossKind,
pub severity: Severity,
pub message: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_provenance"
)]
pub provenance: Option<SourceProvenance>,
}
impl LossNote {
pub fn try_clone_for_decode(
&self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
operation: &'static str,
) -> Result<Self, cadmpeg_core::CodecError> {
ctx.charge_work(1, operation)?;
let message = ctx.copy_retained_text(&self.message, operation)?;
Ok(Self {
code: self.code.try_clone_for_decode(ctx, operation)?,
severity: self.severity,
message,
provenance: self
.provenance
.as_ref()
.map(|value| value.try_clone_for_decode(ctx, operation))
.transpose()?,
})
}
pub fn new(code: impl Into<LossKind>, message: impl Into<String>) -> Self {
let code = code.into();
Self {
severity: code.default_severity(),
code,
message: message.into(),
provenance: None,
}
}
#[must_use]
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
#[must_use]
pub fn with_provenance(mut self, provenance: SourceProvenance) -> Self {
self.provenance = Some(provenance);
self
}
pub fn strict_consequence(&self) -> StrictConsequence {
match self.code.strict_floor() {
Some(floor) if self.severity >= floor => StrictConsequence::Reject,
_ => StrictConsequence::Tolerate,
}
}
}
cadmpeg_core::named_optional_field!(deserialize_strict_floor, Severity, "strict_floor");
cadmpeg_core::named_optional_field!(deserialize_provenance, SourceProvenance, "provenance");
#[cfg(test)]
mod tests;