lp_parser_rs/
error.rs

1//! Comprehensive error types for LP parsing operations.
2//!
3//! This module provides a structured error hierarchy with detailed context
4//! information for debugging and error handling in LP file parsing.
5
6use thiserror::Error;
7
8/// Comprehensive error type for LP parsing operations.
9///
10/// This error type provides detailed context about parsing failures,
11/// including location information and specific error conditions.
12#[derive(Error, Debug, Clone, PartialEq, Eq)]
13pub enum LpParseError {
14    /// Invalid or malformed constraint syntax
15    #[error("Invalid constraint syntax at position {position}: {context}")]
16    ConstraintSyntax { position: usize, context: String },
17
18    /// Invalid or malformed objective syntax
19    #[error("Invalid objective syntax at position {position}: {context}")]
20    ObjectiveSyntax { position: usize, context: String },
21
22    /// Unknown or invalid variable type specification
23    #[error("Unknown variable type '{var_type}' for variable '{variable}'")]
24    UnknownVariableType { variable: String, var_type: String },
25
26    /// Reference to an undefined variable
27    #[error("Undefined variable '{variable}' referenced in {context}")]
28    UndefinedVariable { variable: String, context: String },
29
30    /// Duplicate definition of a component
31    #[error("Duplicate {component_type} '{name}' defined")]
32    DuplicateDefinition { component_type: String, name: String },
33
34    /// Invalid numerical value or format
35    #[error("Invalid number format '{value}' at position {position}")]
36    InvalidNumber { value: String, position: usize },
37
38    /// Missing required section in LP file
39    #[error("Missing required section: {section}")]
40    MissingSection { section: String },
41
42    /// Invalid bound specification
43    #[error("Invalid bounds for variable '{variable}': {details}")]
44    InvalidBounds { variable: String, details: String },
45
46    /// Invalid SOS constraint specification
47    #[error("Invalid SOS constraint '{constraint}': {details}")]
48    InvalidSosConstraint { constraint: String, details: String },
49
50    /// Validation error for logical consistency
51    #[error("Validation error: {message}")]
52    ValidationError { message: String },
53
54    /// Generic parsing error with context
55    #[error("Parse error at position {position}: {message}")]
56    ParseError { position: usize, message: String },
57
58    /// File I/O related errors
59    #[error("File I/O error: {message}")]
60    IoError { message: String },
61
62    /// Internal parser state errors
63    #[error("Internal parser error: {message}")]
64    InternalError { message: String },
65}
66
67impl LpParseError {
68    /// Create a new constraint syntax error
69    pub fn constraint_syntax(position: usize, context: impl Into<String>) -> Self {
70        Self::ConstraintSyntax { position, context: context.into() }
71    }
72
73    /// Create a new objective syntax error
74    pub fn objective_syntax(position: usize, context: impl Into<String>) -> Self {
75        Self::ObjectiveSyntax { position, context: context.into() }
76    }
77
78    /// Create a new unknown variable type error
79    pub fn unknown_variable_type(variable: impl Into<String>, var_type: impl Into<String>) -> Self {
80        Self::UnknownVariableType { variable: variable.into(), var_type: var_type.into() }
81    }
82
83    /// Create a new undefined variable error
84    pub fn undefined_variable(variable: impl Into<String>, context: impl Into<String>) -> Self {
85        Self::UndefinedVariable { variable: variable.into(), context: context.into() }
86    }
87
88    /// Create a new duplicate definition error
89    pub fn duplicate_definition(component_type: impl Into<String>, name: impl Into<String>) -> Self {
90        Self::DuplicateDefinition { component_type: component_type.into(), name: name.into() }
91    }
92
93    /// Create a new invalid number error
94    pub fn invalid_number(value: impl Into<String>, position: usize) -> Self {
95        Self::InvalidNumber { value: value.into(), position }
96    }
97
98    /// Create a new missing section error
99    pub fn missing_section(section: impl Into<String>) -> Self {
100        Self::MissingSection { section: section.into() }
101    }
102
103    /// Create a new invalid bounds error
104    pub fn invalid_bounds(variable: impl Into<String>, details: impl Into<String>) -> Self {
105        Self::InvalidBounds { variable: variable.into(), details: details.into() }
106    }
107
108    /// Create a new invalid SOS constraint error
109    pub fn invalid_sos_constraint(constraint: impl Into<String>, details: impl Into<String>) -> Self {
110        Self::InvalidSosConstraint { constraint: constraint.into(), details: details.into() }
111    }
112
113    /// Create a new validation error
114    pub fn validation_error(message: impl Into<String>) -> Self {
115        Self::ValidationError { message: message.into() }
116    }
117
118    /// Create a new parse error
119    pub fn parse_error(position: usize, message: impl Into<String>) -> Self {
120        Self::ParseError { position, message: message.into() }
121    }
122
123    /// Create a new I/O error
124    pub fn io_error(message: impl Into<String>) -> Self {
125        Self::IoError { message: message.into() }
126    }
127
128    /// Create a new internal error
129    pub fn internal_error(message: impl Into<String>) -> Self {
130        Self::InternalError { message: message.into() }
131    }
132}
133
134/// Convert from nom parsing errors to our custom error type
135impl<'a> From<nom::Err<nom::error::Error<&'a str>>> for LpParseError {
136    fn from(err: nom::Err<nom::error::Error<&'a str>>) -> Self {
137        match err {
138            nom::Err::Incomplete(_) => Self::parse_error(0, "Incomplete input"),
139            nom::Err::Error(e) | nom::Err::Failure(e) => Self::parse_error(e.input.as_ptr() as usize, format!("Parse error: {:?}", e.code)),
140        }
141    }
142}
143
144/// Convert from standard I/O errors
145impl From<std::io::Error> for LpParseError {
146    fn from(err: std::io::Error) -> Self {
147        Self::io_error(err.to_string())
148    }
149}
150
151/// Convert from boxed errors (used by CSV module)
152impl From<Box<dyn std::error::Error>> for LpParseError {
153    fn from(err: Box<dyn std::error::Error>) -> Self {
154        Self::io_error(err.to_string())
155    }
156}
157
158/// Result type alias for LP parsing operations
159pub type LpResult<T> = Result<T, LpParseError>;
160
161/// Context extension trait for adding location information to errors
162pub trait ErrorContext<T> {
163    /// Add position context to an error
164    fn with_position(self, position: usize) -> LpResult<T>;
165
166    /// Add general context to an error
167    fn with_context(self, context: &str) -> LpResult<T>;
168}
169
170impl<T> ErrorContext<T> for Result<T, LpParseError> {
171    fn with_position(self, position: usize) -> Self {
172        self.map_err(|mut err| {
173            if let LpParseError::ParseError { position: ref mut pos, .. } = &mut err {
174                *pos = position;
175            }
176            err
177        })
178    }
179
180    fn with_context(self, context: &str) -> Self {
181        self.map_err(|err| match err {
182            LpParseError::ParseError { position, message } => LpParseError::parse_error(position, format!("{context}: {message}")),
183            other => other,
184        })
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_error_creation() {
194        let err = LpParseError::constraint_syntax(42, "missing operator");
195        assert_eq!(err.to_string(), "Invalid constraint syntax at position 42: missing operator");
196    }
197
198    #[test]
199    fn test_error_context() {
200        let result: LpResult<()> = Err(LpParseError::parse_error(10, "test error"));
201        let with_context = result.with_context("parsing constraint");
202
203        assert!(with_context.is_err());
204        assert!(with_context.unwrap_err().to_string().contains("parsing constraint"));
205    }
206
207    #[test]
208    fn test_io_error_conversion() {
209        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
210        let lp_err: LpParseError = io_err.into();
211
212        match lp_err {
213            LpParseError::IoError { message } => assert!(message.contains("file not found")),
214            _ => panic!("Expected IoError"),
215        }
216    }
217}