use std::io;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorContext {
pub file: Option<String>,
pub line: Option<usize>,
pub column: Option<usize>,
pub hint: Option<String>,
}
impl ErrorContext {
pub fn new() -> Self {
Self {
file: None,
line: None,
column: None,
hint: None,
}
}
pub fn with_hint(hint: impl Into<String>) -> Self {
Self {
file: None,
line: None,
column: None,
hint: Some(hint.into()),
}
}
pub fn file(mut self, file: impl Into<String>) -> Self {
self.file = Some(file.into());
self
}
pub fn line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
pub fn column(mut self, column: usize) -> Self {
self.column = Some(column);
self
}
pub fn hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
}
impl Default for ErrorContext {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ErrorContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts = Vec::new();
if let Some(ref file) = self.file {
parts.push(format!("File: {}", file));
}
if let (Some(line), Some(column)) = (self.line, self.column) {
parts.push(format!("Location: line {}, column {}", line, column));
} else if let Some(line) = self.line {
parts.push(format!("Line: {}", line));
}
if let Some(ref hint) = self.hint {
parts.push(format!("Hint: {}", hint));
}
if !parts.is_empty() {
write!(f, "\n{}", parts.join("\n"))
} else {
Ok(())
}
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("[E1001] I/O error: {0}")]
Io(#[from] io::Error),
#[error("[E1002] ZIP error: {0}")]
Zip(#[from] zip::result::ZipError),
#[error("[E2001] XML parsing error: {0}")]
Xml(#[from] quick_xml::Error),
#[error("[E2002] XML attribute error: {0}")]
XmlAttr(String),
#[error("[E1003] Missing required file: {0}")]
MissingFile(String),
#[error("[E2004] Invalid 3MF format: {0}")]
InvalidFormat(String),
#[error("[E2003] Invalid XML structure: {0}")]
InvalidXml(String),
#[error("[E3001] Invalid model: {0}")]
InvalidModel(String),
#[error(
"[E3003] Geometry outside positive octant - Object {0}: Min coordinates ({1:.2}, {2:.2}, {3:.2}), all coordinates must be >= 0"
)]
OutsidePositiveOctant(usize, f64, f64, f64),
#[error("[E3002] Parse error: {0}")]
ParseError(String),
#[error("[E4001] Unsupported feature: {0}")]
Unsupported(String),
#[error("[E4002] Required extension not supported: {0}")]
UnsupportedExtension(String),
#[error("[E4003] Invalid SecureContent: {0}")]
InvalidSecureContent(String),
#[error("[E2005] XML writing error: {0}")]
XmlWrite(String),
}
impl From<std::num::ParseFloatError> for Error {
fn from(err: std::num::ParseFloatError) -> Self {
Error::ParseError(format!("Failed to parse floating-point number: {}", err))
}
}
impl From<std::num::ParseIntError> for Error {
fn from(err: std::num::ParseIntError) -> Self {
Error::ParseError(format!("Failed to parse integer: {}", err))
}
}
impl From<quick_xml::events::attributes::AttrError> for Error {
fn from(err: quick_xml::events::attributes::AttrError) -> Self {
Error::XmlAttr(format!("Attribute parsing failed: {}", err))
}
}
impl Error {
pub fn error_type(&self) -> &'static str {
match self {
Error::Io(_) => "Io",
Error::Zip(_) => "Zip",
Error::Xml(_) => "Xml",
Error::XmlAttr(_) => "XmlAttr",
Error::MissingFile(_) => "MissingFile",
Error::InvalidFormat(_) => "InvalidFormat",
Error::InvalidXml(_) => "InvalidXml",
Error::InvalidModel(_) => "InvalidModel",
Error::OutsidePositiveOctant(_, _, _, _) => "OutsidePositiveOctant",
Error::ParseError(_) => "ParseError",
Error::Unsupported(_) => "Unsupported",
Error::UnsupportedExtension(_) => "UnsupportedExtension",
Error::InvalidSecureContent(_) => "InvalidSecureContent",
Error::XmlWrite(_) => "XmlWrite",
}
}
pub fn invalid_xml_element(element: &str, message: &str) -> Self {
Error::InvalidXml(format!("Element '<{}>': {}", element, message))
}
pub fn missing_attribute(element: &str, attribute: &str) -> Self {
Error::InvalidXml(format!(
"Element '<{}>' is missing required attribute '{}'. \
Check the 3MF specification for required attributes.",
element, attribute
))
}
pub fn invalid_format_context(context: &str, message: &str) -> Self {
Error::InvalidFormat(format!("{}: {}", context, message))
}
pub fn parse_error_with_context(field_name: &str, value: &str, expected_type: &str) -> Self {
Error::ParseError(format!(
"Failed to parse '{}': expected {}, got '{}'. \
Verify the value is properly formatted.",
field_name, expected_type, value
))
}
pub fn xml_write(message: String) -> Self {
Error::XmlWrite(message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_codes_in_messages() {
let io_err = Error::Io(io::Error::new(io::ErrorKind::NotFound, "test"));
assert!(io_err.to_string().contains("[E1001]"));
let missing_file = Error::MissingFile("test.model".to_string());
assert!(missing_file.to_string().contains("[E1003]"));
let invalid_model = Error::InvalidModel("test error".to_string());
assert!(invalid_model.to_string().contains("[E3001]"));
let parse_err = Error::ParseError("test".to_string());
assert!(parse_err.to_string().contains("[E3002]"));
let unsupported = Error::Unsupported("test feature".to_string());
assert!(unsupported.to_string().contains("[E4001]"));
}
#[test]
fn test_invalid_xml_element_helper() {
let err = Error::invalid_xml_element("vertex", "Missing required 'x' attribute");
assert!(err.to_string().contains("Element '<vertex>'"));
assert!(err.to_string().contains("Missing required 'x' attribute"));
assert!(err.to_string().contains("[E2003]"));
}
#[test]
fn test_missing_attribute_helper() {
let err = Error::missing_attribute("object", "id");
assert!(err.to_string().contains("Element '<object>'"));
assert!(err.to_string().contains("missing required attribute 'id'"));
assert!(err.to_string().contains("3MF specification"));
assert!(err.to_string().contains("[E2003]"));
}
#[test]
fn test_invalid_format_context_helper() {
let err = Error::invalid_format_context("OPC structure", "Missing relationship");
assert!(err.to_string().contains("OPC structure"));
assert!(err.to_string().contains("Missing relationship"));
assert!(err.to_string().contains("[E2004]"));
}
#[test]
fn test_parse_error_with_context_helper() {
let err =
Error::parse_error_with_context("vertex x coordinate", "abc", "floating-point number");
assert!(err.to_string().contains("vertex x coordinate"));
assert!(err.to_string().contains("floating-point number"));
assert!(err.to_string().contains("'abc'"));
assert!(err.to_string().contains("properly formatted"));
assert!(err.to_string().contains("[E3002]"));
}
#[test]
fn test_parse_float_error_conversion() {
let parse_err: std::num::ParseFloatError = "not_a_number".parse::<f64>().unwrap_err();
let err = Error::from(parse_err);
assert!(
err.to_string()
.contains("Failed to parse floating-point number")
);
assert!(err.to_string().contains("[E3002]"));
}
#[test]
fn test_parse_int_error_conversion() {
let parse_err: std::num::ParseIntError = "not_a_number".parse::<i32>().unwrap_err();
let err = Error::from(parse_err);
assert!(err.to_string().contains("Failed to parse integer"));
assert!(err.to_string().contains("[E3002]"));
}
#[test]
fn test_error_context_with_hint() {
let ctx = ErrorContext::with_hint("Check the 3MF specification");
assert_eq!(ctx.hint, Some("Check the 3MF specification".to_string()));
assert_eq!(ctx.file, None);
assert_eq!(ctx.line, None);
assert_eq!(ctx.column, None);
}
#[test]
fn test_error_context_builder() {
let ctx = ErrorContext::new()
.file("3D/3dmodel.model")
.line(42)
.column(15)
.hint("Check attribute syntax");
assert_eq!(ctx.file, Some("3D/3dmodel.model".to_string()));
assert_eq!(ctx.line, Some(42));
assert_eq!(ctx.column, Some(15));
assert_eq!(ctx.hint, Some("Check attribute syntax".to_string()));
}
#[test]
fn test_error_context_display() {
let ctx = ErrorContext::new()
.file("3D/3dmodel.model")
.line(42)
.column(15)
.hint("Check attribute syntax");
let display = ctx.to_string();
assert!(display.contains("File: 3D/3dmodel.model"));
assert!(display.contains("Location: line 42, column 15"));
assert!(display.contains("Hint: Check attribute syntax"));
}
#[test]
fn test_error_context_display_partial() {
let ctx = ErrorContext::new()
.file("3D/3dmodel.model")
.hint("Check the specification");
let display = ctx.to_string();
assert!(display.contains("File: 3D/3dmodel.model"));
assert!(display.contains("Hint: Check the specification"));
assert!(!display.contains("Location:"));
}
#[test]
fn test_error_context_display_empty() {
let ctx = ErrorContext::new();
let display = ctx.to_string();
assert_eq!(display, "");
}
}