use std::fmt;
use cadmpeg_ir::report::{DecodeReport, ValidationReport};
use serde_json::{json, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefusalCode {
CheckFailed,
DecodeLossRejected,
ExportLossRejected,
EmptyGeometry,
UnsupportedTarget,
BinaryStdoutRejected,
}
impl RefusalCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::CheckFailed => "check_failed",
Self::DecodeLossRejected => "decode_loss_rejected",
Self::ExportLossRejected => "export_loss_rejected",
Self::EmptyGeometry => "empty_geometry",
Self::UnsupportedTarget => "unsupported_target",
Self::BinaryStdoutRejected => "binary_stdout_rejected",
}
}
}
impl fmt::Display for RefusalCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefusalStage {
Plan,
Decode,
Check,
Export,
}
impl RefusalStage {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Plan => "plan",
Self::Decode => "decode",
Self::Check => "check",
Self::Export => "export",
}
}
}
impl fmt::Display for RefusalStage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
pub enum ConversionRefusal {
CheckFailed {
message: String,
decode_report: Option<DecodeReport>,
validation: ValidationReport,
},
DecodeLossRejected {
message: String,
decode_report: DecodeReport,
},
ExportLossRejected {
message: String,
decode_report: Option<DecodeReport>,
validation: Option<ValidationReport>,
},
EmptyGeometry {
message: String,
decode_report: Option<DecodeReport>,
validation: Option<ValidationReport>,
},
UnsupportedTarget {
message: String,
},
BinaryStdoutRejected {
message: String,
},
}
impl ConversionRefusal {
#[must_use]
pub const fn code(&self) -> RefusalCode {
match self {
Self::CheckFailed { .. } => RefusalCode::CheckFailed,
Self::DecodeLossRejected { .. } => RefusalCode::DecodeLossRejected,
Self::ExportLossRejected { .. } => RefusalCode::ExportLossRejected,
Self::EmptyGeometry { .. } => RefusalCode::EmptyGeometry,
Self::UnsupportedTarget { .. } => RefusalCode::UnsupportedTarget,
Self::BinaryStdoutRejected { .. } => RefusalCode::BinaryStdoutRejected,
}
}
#[must_use]
pub const fn stage(&self) -> RefusalStage {
match self {
Self::UnsupportedTarget { .. } | Self::BinaryStdoutRejected { .. } => {
RefusalStage::Plan
}
Self::DecodeLossRejected { .. } => RefusalStage::Decode,
Self::CheckFailed { .. } => RefusalStage::Check,
Self::ExportLossRejected { .. } | Self::EmptyGeometry { .. } => RefusalStage::Export,
}
}
#[must_use]
pub fn message(&self) -> &str {
match self {
Self::CheckFailed { message, .. }
| Self::DecodeLossRejected { message, .. }
| Self::ExportLossRejected { message, .. }
| Self::EmptyGeometry { message, .. }
| Self::UnsupportedTarget { message }
| Self::BinaryStdoutRejected { message } => message,
}
}
#[must_use]
pub fn report_fields(&self) -> Value {
json!({
"status": "refused",
"refusal": {
"stage": self.stage().as_str(),
"code": self.code().as_str(),
"message": self.message(),
},
})
}
#[must_use]
pub const fn may_write_report(&self) -> bool {
match self {
Self::CheckFailed { .. }
| Self::DecodeLossRejected { .. }
| Self::ExportLossRejected { .. }
| Self::EmptyGeometry { .. } => true,
Self::UnsupportedTarget { .. } | Self::BinaryStdoutRejected { .. } => false,
}
}
#[must_use]
pub fn decode_report(&self) -> Option<&DecodeReport> {
match self {
Self::CheckFailed { decode_report, .. } => decode_report.as_ref(),
Self::DecodeLossRejected { decode_report, .. } => Some(decode_report),
Self::ExportLossRejected { decode_report, .. }
| Self::EmptyGeometry { decode_report, .. } => decode_report.as_ref(),
Self::UnsupportedTarget { .. } | Self::BinaryStdoutRejected { .. } => None,
}
}
#[must_use]
pub fn check_report(&self) -> Option<&ValidationReport> {
match self {
Self::CheckFailed { validation, .. } => Some(validation),
Self::ExportLossRejected { validation, .. }
| Self::EmptyGeometry { validation, .. } => validation.as_ref(),
Self::DecodeLossRejected { .. }
| Self::UnsupportedTarget { .. }
| Self::BinaryStdoutRejected { .. } => None,
}
}
#[must_use]
pub const fn exit_code(&self) -> u8 {
match self {
Self::BinaryStdoutRejected { .. } => 2,
_ => 1,
}
}
}
impl fmt::Display for ConversionRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.message())
}
}
impl std::error::Error for ConversionRefusal {}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
#[test]
fn refusal_codes_are_stable_for_tests_and_absent_from_display() {
let refusal = ConversionRefusal::UnsupportedTarget {
message: "--iges-target requires IGES output".into(),
};
assert_eq!(refusal.code(), RefusalCode::UnsupportedTarget);
assert_eq!(refusal.stage(), RefusalStage::Plan);
assert_eq!(refusal.exit_code(), 1);
assert!(!refusal.may_write_report());
assert_eq!(refusal.to_string(), "--iges-target requires IGES output");
let fields = refusal.report_fields();
assert_eq!(fields["status"], "refused");
assert_eq!(fields["refusal"]["code"], "unsupported_target");
assert_eq!(fields["refusal"]["stage"], "plan");
}
#[test]
fn binary_stdout_keeps_operational_exit_two() {
let refusal = ConversionRefusal::BinaryStdoutRejected {
message: "refusing to write binary sldprt to standard output".into(),
};
assert_eq!(refusal.code(), RefusalCode::BinaryStdoutRejected);
assert_eq!(refusal.stage(), RefusalStage::Plan);
assert_eq!(refusal.exit_code(), 2);
assert!(!refusal.may_write_report());
}
#[test]
fn check_refusal_maps_to_check_stage() {
let refusal = ConversionRefusal::CheckFailed {
message: "check found 1 error(s)".into(),
decode_report: None,
validation: ValidationReport {
entity_counts: BTreeMap::new(),
findings: Vec::new(),
losses: Vec::new(),
},
};
assert_eq!(refusal.stage(), RefusalStage::Check);
assert_eq!(refusal.code().as_str(), "check_failed");
}
}