use crate::parsing::ast::{EffectiveDate, LemmaSpec};
use crate::parsing::source::Source;
use crate::registry::RegistryErrorKind;
use std::fmt;
#[derive(Debug, Clone)]
pub struct ErrorDetails {
pub message: String,
pub source: Option<Source>,
pub suggestion: Option<String>,
pub spec_context_name: Option<String>,
pub spec_context_effective_from: Option<EffectiveDate>,
pub related_spec_name: Option<String>,
pub related_spec_effective_from: Option<EffectiveDate>,
pub related_data: Option<String>,
}
fn attribution_fields(spec: Option<&LemmaSpec>) -> (Option<String>, Option<EffectiveDate>) {
match spec {
Some(s) => (Some(s.name.clone()), Some(s.effective_from.clone())),
None => (None, None),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorKind {
Parsing,
Validation,
Inversion,
Registry,
MissingRepository,
Request,
ResourceLimit,
}
#[derive(Debug, Clone)]
pub enum Error {
Parsing(Box<ErrorDetails>),
Inversion(Box<ErrorDetails>),
Validation(Box<ErrorDetails>),
Registry {
details: Box<ErrorDetails>,
identifier: String,
kind: RegistryErrorKind,
},
MissingRepository {
details: Box<ErrorDetails>,
repository: String,
},
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,
RuleNotFound,
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<&LemmaSpec>,
related_spec: Option<&LemmaSpec>,
) -> Self {
let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
Self::Parsing(Box::new(ErrorDetails {
message: message.into(),
source: Some(source),
suggestion: suggestion.map(Into::into),
spec_context_name,
spec_context_effective_from,
related_spec_name,
related_spec_effective_from,
related_data: None,
}))
}
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<&LemmaSpec>,
related_spec: Option<&LemmaSpec>,
) -> Self {
let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
Self::Inversion(Box::new(ErrorDetails {
message: message.into(),
source,
suggestion: suggestion.map(Into::into),
spec_context_name,
spec_context_effective_from,
related_spec_name,
related_spec_effective_from,
related_data: None,
}))
}
pub fn inversion_with_suggestion(
message: impl Into<String>,
source: Option<Source>,
suggestion: impl Into<String>,
spec_context: Option<&LemmaSpec>,
related_spec: Option<&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<&LemmaSpec>,
related_spec: Option<&LemmaSpec>,
) -> Self {
let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
Self::Validation(Box::new(ErrorDetails {
message: message.into(),
source,
suggestion: suggestion.map(Into::into),
spec_context_name,
spec_context_effective_from,
related_spec_name,
related_spec_effective_from,
related_data: None,
}))
}
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)
}
pub fn rule_not_found(rule_name: &str, suggestion: Option<impl Into<String>>) -> Self {
Self::request_with_kind(
format!("Rule '{}' not found", rule_name),
suggestion,
RequestErrorKind::RuleNotFound,
)
}
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_name: None,
spec_context_effective_from: None,
related_spec_name: None,
related_spec_effective_from: None,
related_data: 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<&LemmaSpec>,
related_spec: Option<&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})");
let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
Self::ResourceLimitExceeded {
details: Box::new(ErrorDetails {
message,
source,
suggestion: Some(suggestion.into()),
spec_context_name,
spec_context_effective_from,
related_spec_name,
related_spec_effective_from,
related_data: None,
}),
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<&LemmaSpec>,
related_spec: Option<&LemmaSpec>,
) -> Self {
let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
Self::Registry {
details: Box::new(ErrorDetails {
message: message.into(),
source: Some(source),
suggestion: suggestion.map(Into::into),
spec_context_name,
spec_context_effective_from,
related_spec_name,
related_spec_effective_from,
related_data: None,
}),
identifier: identifier.into(),
kind,
}
}
pub fn missing_repository(
message: impl Into<String>,
source: Option<Source>,
repository: impl Into<String>,
suggestion: Option<impl Into<String>>,
spec_context: Option<&LemmaSpec>,
) -> Self {
let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
Self::MissingRepository {
details: Box::new(ErrorDetails {
message: message.into(),
source,
suggestion: suggestion.map(Into::into),
spec_context_name,
spec_context_effective_from,
related_spec_name: None,
related_spec_effective_from: None,
related_data: None,
}),
repository: repository.into(),
}
}
pub fn with_spec_context(self, spec: &LemmaSpec) -> Self {
self.map_details(|d| {
d.spec_context_name = Some(spec.name.clone());
d.spec_context_effective_from = Some(spec.effective_from.clone());
})
}
pub fn with_related_data(self, name: impl Into<String>) -> Self {
let name = name.into();
self.map_details(|d| d.related_data = Some(name))
}
fn map_details(self, f: impl FnOnce(&mut ErrorDetails)) -> Self {
match self {
Error::Parsing(details) => {
let mut d = *details;
f(&mut d);
Error::Parsing(Box::new(d))
}
Error::Inversion(details) => {
let mut d = *details;
f(&mut d);
Error::Inversion(Box::new(d))
}
Error::Validation(details) => {
let mut d = *details;
f(&mut d);
Error::Validation(Box::new(d))
}
Error::Registry {
details,
identifier,
kind,
} => {
let mut d = *details;
f(&mut d);
Error::Registry {
details: Box::new(d),
identifier,
kind,
}
}
Error::MissingRepository {
details,
repository,
} => {
let mut d = *details;
f(&mut d);
Error::MissingRepository {
details: Box::new(d),
repository,
}
}
Error::ResourceLimitExceeded {
details,
limit_name,
limit_value,
actual_value,
} => {
let mut d = *details;
f(&mut d);
Error::ResourceLimitExceeded {
details: Box::new(d),
limit_name,
limit_value,
actual_value,
}
}
Error::Request { details, kind } => {
let mut d = *details;
f(&mut d);
Error::Request {
details: Box::new(d),
kind,
}
}
}
}
}
fn format_related_spec(name: &str, effective_from: &EffectiveDate) -> String {
let effective_from_str = effective_from
.as_ref()
.map(|d| d.to_string())
.unwrap_or_else(|| "beginning".to_string());
format!(
"See spec '{}' (effective from {}).",
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.source_type, src.span.line, src.span.col
)
} else {
Ok(())
}
}
fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
if let Some(ref name) = details.related_spec_name {
let effective = details
.related_spec_effective_from
.as_ref()
.expect("BUG: related_spec_name set without related_spec_effective_from");
write!(f, " {}", format_related_spec(name, effective))?;
}
Ok(())
}
fn write_spec_context(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
write!(f, "In 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 name) = details.spec_context_name {
write_spec_context(f, name)?;
}
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 name) = details.spec_context_name {
write_spec_context(f, name)?;
}
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 name) = details.spec_context_name {
write_spec_context(f, name)?;
}
write!(f, "Validation error: ")?;
if let Some(ref name) = details.related_data {
write!(f, "Failed to parse data '{}': ", name)?;
}
write!(f, "{}", 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 name) = details.spec_context_name {
write_spec_context(f, name)?;
}
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::MissingRepository {
details,
repository,
} => {
if let Some(ref name) = details.spec_context_name {
write_spec_context(f, name)?;
}
write!(f, "Missing repository: {}: {}", repository, 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 name) = details.spec_context_name {
write_spec_context(f, name)?;
}
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 name) = details.spec_context_name {
write_spec_context(f, name)?;
}
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 kind(&self) -> ErrorKind {
match self {
Error::Parsing(_) => ErrorKind::Parsing,
Error::Validation(_) => ErrorKind::Validation,
Error::Inversion(_) => ErrorKind::Inversion,
Error::Registry { .. } => ErrorKind::Registry,
Error::MissingRepository { .. } => ErrorKind::MissingRepository,
Error::Request { .. } => ErrorKind::Request,
Error::ResourceLimitExceeded { .. } => ErrorKind::ResourceLimit,
}
}
pub(crate) fn details(&self) -> &ErrorDetails {
match self {
Error::Parsing(d) | Error::Inversion(d) | Error::Validation(d) => d,
Error::Registry { details, .. }
| Error::MissingRepository { details, .. }
| Error::ResourceLimitExceeded { details, .. }
| Error::Request { details, .. } => details,
}
}
#[must_use]
pub fn repository(&self) -> Option<&str> {
match self {
Error::MissingRepository { repository, .. } => Some(repository.as_str()),
Error::Registry { identifier, .. } => Some(identifier.as_str()),
_ => None,
}
}
pub fn message(&self) -> &str {
&self.details().message
}
pub fn location(&self) -> Option<&Source> {
self.details().source.as_ref()
}
pub fn source_location(&self) -> Option<&Source> {
self.location()
}
pub fn source_text(
&self,
sources: &std::collections::HashMap<crate::parsing::source::SourceType, String>,
) -> Option<String> {
self.location()
.and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
}
pub fn suggestion(&self) -> Option<&str> {
self.details().suggestion.as_deref()
}
pub fn related_data(&self) -> Option<&str> {
self.details().related_data.as_deref()
}
pub fn spec_context_name(&self) -> Option<&str> {
self.details().spec_context_name.as_deref()
}
pub fn related_spec(&self) -> Option<&str> {
self.details().related_spec_name.as_deref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::ast::Span;
fn test_source() -> Source {
Source::new(
crate::parsing::source::SourceType::Path(std::sync::Arc::new(
std::path::PathBuf::from("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(
crate::parsing::source::SourceType::Volatile,
Span {
start: 5,
end: 10,
line: 2,
col: 3,
},
);
let suggestion_error =
Error::parsing_with_suggestion("typo", suggestion_source, "did you mean X?");
assert!(format!("{suggestion_error}").contains("suggestion: did you mean X?"));
}
#[test]
fn test_request_error_accessors() {
let err = Error::request("bad id", Some("use a valid id"));
assert_eq!(err.kind(), ErrorKind::Request);
assert_eq!(err.message(), "bad id");
assert!(err.location().is_none());
assert_eq!(err.suggestion(), Some("use a valid id"));
assert!(err.spec_context_name().is_none());
assert!(err.related_spec().is_none());
}
#[test]
fn test_missing_repository_display() {
let err = Error::missing_repository(
"not loaded",
None,
"@iso/countries",
Some("load the dependency first"),
None,
);
let display = format!("{err}");
assert!(display.contains("Missing repository"));
assert!(display.contains("@iso/countries"));
assert!(display.contains("not loaded"));
}
#[test]
fn test_with_spec_context_copies_name() {
let spec = LemmaSpec::new("pricing".to_string());
let err = Error::validation("bad", None, None::<String>).with_spec_context(&spec);
assert_eq!(err.spec_context_name(), Some("pricing"));
let display = format!("{err}");
assert!(display.contains("In spec 'pricing':"));
}
}