use crate::parsing::ast::LemmaSpec;
use crate::parsing::source::Source;
use crate::registry::RegistryErrorKind;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ErrorDetails {
pub message: String,
pub source: Option<Source>,
pub suggestion: Option<String>,
pub spec_context: Option<Arc<LemmaSpec>>,
pub related_spec: Option<Arc<LemmaSpec>>,
}
#[derive(Debug, Clone)]
pub enum Error {
Parsing(Box<ErrorDetails>),
Inversion(Box<ErrorDetails>),
Validation(Box<ErrorDetails>),
Registry {
details: Box<ErrorDetails>,
identifier: String,
kind: RegistryErrorKind,
},
ResourceLimitExceeded {
details: Box<ErrorDetails>,
limit_name: String,
limit_value: String,
actual_value: String,
},
Request {
details: Box<ErrorDetails>,
kind: RequestErrorKind,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestErrorKind {
SpecNotFound,
InvalidRequest,
}
impl Error {
pub fn parsing(
message: impl Into<String>,
source: Source,
suggestion: Option<impl Into<String>>,
) -> Self {
Self::parsing_with_context(message, source, suggestion, None, None)
}
pub fn parsing_with_context(
message: impl Into<String>,
source: Source,
suggestion: Option<impl Into<String>>,
spec_context: Option<Arc<LemmaSpec>>,
related_spec: Option<Arc<LemmaSpec>>,
) -> Self {
Self::Parsing(Box::new(ErrorDetails {
message: message.into(),
source: Some(source),
suggestion: suggestion.map(Into::into),
spec_context,
related_spec,
}))
}
pub fn parsing_with_suggestion(
message: impl Into<String>,
source: Source,
suggestion: impl Into<String>,
) -> Self {
Self::parsing_with_context(message, source, Some(suggestion), None, None)
}
pub fn inversion(
message: impl Into<String>,
source: Option<Source>,
suggestion: Option<impl Into<String>>,
) -> Self {
Self::inversion_with_context(message, source, suggestion, None, None)
}
pub fn inversion_with_context(
message: impl Into<String>,
source: Option<Source>,
suggestion: Option<impl Into<String>>,
spec_context: Option<Arc<LemmaSpec>>,
related_spec: Option<Arc<LemmaSpec>>,
) -> Self {
Self::Inversion(Box::new(ErrorDetails {
message: message.into(),
source,
suggestion: suggestion.map(Into::into),
spec_context,
related_spec,
}))
}
pub fn inversion_with_suggestion(
message: impl Into<String>,
source: Option<Source>,
suggestion: impl Into<String>,
spec_context: Option<Arc<LemmaSpec>>,
related_spec: Option<Arc<LemmaSpec>>,
) -> Self {
Self::inversion_with_context(
message,
source,
Some(suggestion),
spec_context,
related_spec,
)
}
pub fn validation(
message: impl Into<String>,
source: Option<Source>,
suggestion: Option<impl Into<String>>,
) -> Self {
Self::validation_with_context(message, source, suggestion, None, None)
}
pub fn validation_with_context(
message: impl Into<String>,
source: Option<Source>,
suggestion: Option<impl Into<String>>,
spec_context: Option<Arc<LemmaSpec>>,
related_spec: Option<Arc<LemmaSpec>>,
) -> Self {
Self::Validation(Box::new(ErrorDetails {
message: message.into(),
source,
suggestion: suggestion.map(Into::into),
spec_context,
related_spec,
}))
}
pub fn request(message: impl Into<String>, suggestion: Option<impl Into<String>>) -> Self {
Self::request_with_kind(message, suggestion, RequestErrorKind::InvalidRequest)
}
pub fn request_not_found(
message: impl Into<String>,
suggestion: Option<impl Into<String>>,
) -> Self {
Self::request_with_kind(message, suggestion, RequestErrorKind::SpecNotFound)
}
fn request_with_kind(
message: impl Into<String>,
suggestion: Option<impl Into<String>>,
kind: RequestErrorKind,
) -> Self {
Self::Request {
details: Box::new(ErrorDetails {
message: message.into(),
source: None,
suggestion: suggestion.map(Into::into),
spec_context: None,
related_spec: None,
}),
kind,
}
}
pub fn resource_limit_exceeded(
limit_name: impl Into<String>,
limit_value: impl Into<String>,
actual_value: impl Into<String>,
suggestion: impl Into<String>,
source: Option<Source>,
spec_context: Option<Arc<LemmaSpec>>,
related_spec: Option<Arc<LemmaSpec>>,
) -> Self {
let limit_name = limit_name.into();
let limit_value = limit_value.into();
let actual_value = actual_value.into();
let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
Self::ResourceLimitExceeded {
details: Box::new(ErrorDetails {
message,
source,
suggestion: Some(suggestion.into()),
spec_context,
related_spec,
}),
limit_name,
limit_value,
actual_value,
}
}
pub fn registry(
message: impl Into<String>,
source: Source,
identifier: impl Into<String>,
kind: RegistryErrorKind,
suggestion: Option<impl Into<String>>,
spec_context: Option<Arc<LemmaSpec>>,
related_spec: Option<Arc<LemmaSpec>>,
) -> Self {
Self::Registry {
details: Box::new(ErrorDetails {
message: message.into(),
source: Some(source),
suggestion: suggestion.map(Into::into),
spec_context,
related_spec,
}),
identifier: identifier.into(),
kind,
}
}
pub fn with_spec_context(self, spec: Arc<LemmaSpec>) -> Self {
match self {
Error::Parsing(details) => {
let mut d = *details;
d.spec_context = Some(spec.clone());
Error::Parsing(Box::new(d))
}
Error::Inversion(details) => {
let mut d = *details;
d.spec_context = Some(spec.clone());
Error::Inversion(Box::new(d))
}
Error::Validation(details) => {
let mut d = *details;
d.spec_context = Some(spec.clone());
Error::Validation(Box::new(d))
}
Error::Registry {
details,
identifier,
kind,
} => {
let mut d = *details;
d.spec_context = Some(spec.clone());
Error::Registry {
details: Box::new(d),
identifier,
kind,
}
}
Error::ResourceLimitExceeded {
details,
limit_name,
limit_value,
actual_value,
} => {
let mut d = *details;
d.spec_context = Some(spec.clone());
Error::ResourceLimitExceeded {
details: Box::new(d),
limit_name,
limit_value,
actual_value,
}
}
Error::Request { details, kind } => {
let mut d = *details;
d.spec_context = Some(spec);
Error::Request {
details: Box::new(d),
kind,
}
}
}
}
}
fn format_related_spec(spec: &LemmaSpec) -> String {
let effective_from_str = spec
.effective_from()
.map(|d| d.to_string())
.unwrap_or_else(|| "beginning".to_string());
format!(
"See spec '{}' (effective from {}).",
spec.name, effective_from_str
)
}
fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
if let Some(src) = source {
write!(
f,
" at {}:{}:{}",
src.attribute, src.span.line, src.span.col
)
} else {
Ok(())
}
}
fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
if let Some(ref related) = details.related_spec {
write!(f, " {}", format_related_spec(related))?;
}
Ok(())
}
fn write_spec_context(f: &mut fmt::Formatter<'_>, spec: &LemmaSpec) -> fmt::Result {
write!(f, "In spec '{}': ", spec.name)
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Parsing(details) => {
if let Some(ref spec) = details.spec_context {
write_spec_context(f, spec)?;
}
write!(f, "Parse error: {}", details.message)?;
if let Some(suggestion) = &details.suggestion {
write!(f, " (suggestion: {suggestion})")?;
}
write_related_spec(f, details)?;
write_source_location(f, &details.source)
}
Error::Inversion(details) => {
if let Some(ref spec) = details.spec_context {
write_spec_context(f, spec)?;
}
write!(f, "Inversion error: {}", details.message)?;
if let Some(suggestion) = &details.suggestion {
write!(f, " (suggestion: {suggestion})")?;
}
write_related_spec(f, details)?;
write_source_location(f, &details.source)
}
Error::Validation(details) => {
if let Some(ref spec) = details.spec_context {
write_spec_context(f, spec)?;
}
write!(f, "Validation error: {}", details.message)?;
if let Some(suggestion) = &details.suggestion {
write!(f, " (suggestion: {suggestion})")?;
}
write_related_spec(f, details)?;
write_source_location(f, &details.source)
}
Error::Registry {
details,
identifier,
kind,
} => {
if let Some(ref spec) = details.spec_context {
write_spec_context(f, spec)?;
}
write!(
f,
"Registry error ({}): {}: {}",
kind, identifier, details.message
)?;
if let Some(suggestion) = &details.suggestion {
write!(f, " (suggestion: {suggestion})")?;
}
write_related_spec(f, details)?;
write_source_location(f, &details.source)
}
Error::ResourceLimitExceeded {
details,
limit_name,
limit_value,
actual_value,
} => {
if let Some(ref spec) = details.spec_context {
write_spec_context(f, spec)?;
}
write!(
f,
"Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
)?;
if let Some(suggestion) = &details.suggestion {
write!(f, ". {suggestion}")?;
}
write_source_location(f, &details.source)
}
Error::Request { details, .. } => {
if let Some(ref spec) = details.spec_context {
write_spec_context(f, spec)?;
}
write!(f, "Request error: {}", details.message)?;
if let Some(suggestion) = &details.suggestion {
write!(f, " (suggestion: {suggestion})")?;
}
write_related_spec(f, details)?;
write_source_location(f, &details.source)
}
}
}
}
impl std::error::Error for Error {}
impl From<std::fmt::Error> for Error {
fn from(err: std::fmt::Error) -> Self {
Error::validation(format!("Format error: {err}"), None, None::<String>)
}
}
impl Error {
pub fn message(&self) -> &str {
match self {
Error::Parsing(details)
| Error::Inversion(details)
| Error::Validation(details)
| Error::Request { details, .. } => &details.message,
Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
&details.message
}
}
}
pub fn location(&self) -> Option<&Source> {
match self {
Error::Parsing(details)
| Error::Inversion(details)
| Error::Validation(details)
| Error::Request { details, .. } => details.source.as_ref(),
Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
details.source.as_ref()
}
}
}
pub fn source_text(
&self,
sources: &std::collections::HashMap<String, String>,
) -> Option<String> {
self.location()
.and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
}
pub fn suggestion(&self) -> Option<&str> {
match self {
Error::Parsing(details)
| Error::Inversion(details)
| Error::Validation(details)
| Error::Request { details, .. } => details.suggestion.as_deref(),
Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
details.suggestion.as_deref()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::ast::Span;
fn test_source() -> Source {
Source::new(
"test.lemma",
Span {
start: 14,
end: 21,
line: 1,
col: 15,
},
)
}
#[test]
fn test_error_creation_and_display() {
let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
let parse_error_display = format!("{parse_error}");
assert!(parse_error_display.contains("Parse error: Invalid currency"));
assert!(parse_error_display.contains("test.lemma:1:15"));
let suggestion_source = Source::new(
"suggestion.lemma",
Span {
start: 5,
end: 10,
line: 1,
col: 6,
},
);
let parse_error_with_suggestion = Error::parsing_with_suggestion(
"Typo in fact name",
suggestion_source,
"Did you mean 'amount'?",
);
let parse_error_with_suggestion_display = format!("{parse_error_with_suggestion}");
assert!(parse_error_with_suggestion_display.contains("Typo in fact name"));
assert!(parse_error_with_suggestion_display.contains("Did you mean 'amount'?"));
let engine_error = Error::validation("Something went wrong", None, None::<String>);
assert!(format!("{engine_error}").contains("Validation error: Something went wrong"));
assert!(!format!("{engine_error}").contains(" at "));
let validation_error =
Error::validation("Circular dependency: a -> b -> a", None, None::<String>);
assert!(format!("{validation_error}")
.contains("Validation error: Circular dependency: a -> b -> a"));
}
}