use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Location {
pub line: usize,
pub column: usize,
}
impl Location {
pub fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
pub fn start() -> Self {
Self { line: 1, column: 1 }
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
impl Default for Location {
fn default() -> Self {
Self::start()
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub struct ParseError {
pub location: Location,
pub message: String,
pub errors: Vec<ParseError>,
}
impl ParseError {
pub fn new(location: Location, message: String) -> Self {
Self {
location,
message,
errors: Vec::new(),
}
}
#[allow(dead_code)]
pub fn with_error(mut self, error: ParseError) -> Self {
self.errors.push(error);
self
}
#[allow(dead_code)]
pub fn invalid_structure(location: Location, details: String) -> Self {
Self::new(location, format!("Invalid pattern structure: {}", details))
}
#[allow(dead_code)]
pub fn error_count(&self) -> usize {
1 + self.errors.len()
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Parse error at {}:{}: {}",
self.location.line, self.location.column, self.message
)?;
if !self.errors.is_empty() {
write!(f, "\nAdditional errors:")?;
for error in &self.errors {
write!(f, "\n - {}", error)?;
}
}
Ok(())
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq)]
pub enum SerializeError {
InvalidStructure { reason: String },
InvalidValue { value_type: String, reason: String },
InvalidIdentifier { identifier: String, reason: String },
ValidationFailed { gram: String, reason: String },
IoError { message: String },
}
impl SerializeError {
pub fn invalid_structure(reason: impl Into<String>) -> Self {
Self::InvalidStructure {
reason: reason.into(),
}
}
pub fn invalid_value(value_type: impl Into<String>, reason: impl Into<String>) -> Self {
Self::InvalidValue {
value_type: value_type.into(),
reason: reason.into(),
}
}
pub fn invalid_identifier(identifier: impl Into<String>, reason: impl Into<String>) -> Self {
Self::InvalidIdentifier {
identifier: identifier.into(),
reason: reason.into(),
}
}
pub fn validation_failed(gram: impl Into<String>, reason: impl Into<String>) -> Self {
Self::ValidationFailed {
gram: gram.into(),
reason: reason.into(),
}
}
}
impl fmt::Display for SerializeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidStructure { reason } => {
write!(f, "Cannot serialize pattern structure: {}", reason)
}
Self::InvalidValue { value_type, reason } => {
write!(f, "Cannot serialize {} value: {}", value_type, reason)
}
Self::InvalidIdentifier { identifier, reason } => {
write!(f, "Invalid identifier '{}': {}", identifier, reason)
}
Self::ValidationFailed { gram, reason } => {
write!(
f,
"Serialized gram notation failed validation: {}\n{}",
reason, gram
)
}
Self::IoError { message } => {
write!(f, "I/O error: {}", message)
}
}
}
}
impl std::error::Error for SerializeError {}
impl From<std::io::Error> for SerializeError {
fn from(err: std::io::Error) -> Self {
Self::IoError {
message: err.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_location_creation() {
let loc = Location::new(10, 25);
assert_eq!(loc.line, 10);
assert_eq!(loc.column, 25);
}
#[test]
fn test_location_display() {
let loc = Location::new(1, 5);
assert_eq!(loc.to_string(), "1:5");
}
#[test]
fn test_location_start() {
let loc = Location::start();
assert_eq!(loc.line, 1);
assert_eq!(loc.column, 1);
}
#[test]
fn test_parse_error_creation() {
let error = ParseError::new(Location::new(1, 5), "Unexpected token".to_string());
assert_eq!(error.location.line, 1);
assert_eq!(error.location.column, 5);
assert_eq!(error.message, "Unexpected token");
assert_eq!(error.errors.len(), 0);
}
#[test]
fn test_parse_error_display() {
let error = ParseError::new(Location::new(1, 5), "Unexpected token".to_string());
assert_eq!(error.to_string(), "Parse error at 1:5: Unexpected token");
}
#[test]
fn test_parse_error_with_recovery() {
let mut primary = ParseError::new(Location::start(), "Multiple errors".to_string());
primary = primary.with_error(ParseError::new(Location::new(1, 1), "Error 1".to_string()));
primary = primary.with_error(ParseError::new(Location::new(2, 1), "Error 2".to_string()));
assert_eq!(primary.error_count(), 3);
let display = primary.to_string();
assert!(display.contains("Multiple errors"));
assert!(display.contains("Additional errors"));
assert!(display.contains("Error 1"));
assert!(display.contains("Error 2"));
}
#[test]
fn test_serialize_error_invalid_structure() {
let error = SerializeError::invalid_structure("Test reason");
let display = error.to_string();
assert!(display.contains("Test reason"));
assert!(display.contains("Cannot serialize pattern structure"));
}
#[test]
fn test_serialize_error_invalid_value() {
let error = SerializeError::invalid_value("CustomType", "Not supported");
let display = error.to_string();
assert!(display.contains("CustomType"));
assert!(display.contains("Not supported"));
}
#[test]
fn test_serialize_error_invalid_identifier() {
let error = SerializeError::invalid_identifier("hello world", "Contains whitespace");
let display = error.to_string();
assert!(display.contains("hello world"));
assert!(display.contains("Contains whitespace"));
}
#[test]
fn test_serialize_error_validation_failed() {
let error = SerializeError::validation_failed("(unclosed", "gram-lint failed");
let display = error.to_string();
assert!(display.contains("(unclosed"));
assert!(display.contains("gram-lint failed"));
}
#[test]
fn test_serialize_error_io_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let error: SerializeError = io_err.into();
match error {
SerializeError::IoError { message } => {
assert!(message.contains("File not found"));
}
_ => panic!("Expected IoError variant"),
}
}
}