1use crate::parsing::ast::{EffectiveDate, LemmaSpec};
2use crate::parsing::source::Source;
3use crate::registry::RegistryErrorKind;
4use std::fmt;
5
6#[derive(Debug, Clone)]
8pub struct ErrorDetails {
9 pub message: String,
10 pub source: Option<Source>,
11 pub suggestion: Option<String>,
12 pub spec_context_name: Option<String>,
14 pub spec_context_effective_from: Option<EffectiveDate>,
15 pub related_spec_name: Option<String>,
17 pub related_spec_effective_from: Option<EffectiveDate>,
18 pub related_data: Option<String>,
21}
22
23fn attribution_fields(spec: Option<&LemmaSpec>) -> (Option<String>, Option<EffectiveDate>) {
24 match spec {
25 Some(s) => (Some(s.name.clone()), Some(s.effective_from.clone())),
26 None => (None, None),
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum ErrorKind {
34 Parsing,
35 Validation,
36 Inversion,
37 Registry,
38 MissingRepository,
39 Request,
40 ResourceLimit,
41}
42
43#[derive(Debug, Clone)]
45pub enum Error {
46 Parsing(Box<ErrorDetails>),
48
49 Inversion(Box<ErrorDetails>),
51
52 Validation(Box<ErrorDetails>),
54
55 Registry {
61 details: Box<ErrorDetails>,
62 identifier: String,
64 kind: RegistryErrorKind,
66 },
67
68 MissingRepository {
73 details: Box<ErrorDetails>,
74 repository: String,
76 },
77
78 ResourceLimitExceeded {
80 details: Box<ErrorDetails>,
81 limit_name: String,
82 limit_value: String,
83 actual_value: String,
84 },
85
86 Request {
89 details: Box<ErrorDetails>,
90 kind: RequestErrorKind,
91 },
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum RequestErrorKind {
97 SpecNotFound,
99 RuleNotFound,
101 InvalidRequest,
103}
104
105impl Error {
106 pub fn parsing(
108 message: impl Into<String>,
109 source: Source,
110 suggestion: Option<impl Into<String>>,
111 ) -> Self {
112 Self::parsing_with_context(message, source, suggestion, None, None)
113 }
114
115 pub fn parsing_with_context(
117 message: impl Into<String>,
118 source: Source,
119 suggestion: Option<impl Into<String>>,
120 spec_context: Option<&LemmaSpec>,
121 related_spec: Option<&LemmaSpec>,
122 ) -> Self {
123 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
124 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
125 Self::Parsing(Box::new(ErrorDetails {
126 message: message.into(),
127 source: Some(source),
128 suggestion: suggestion.map(Into::into),
129 spec_context_name,
130 spec_context_effective_from,
131 related_spec_name,
132 related_spec_effective_from,
133 related_data: None,
134 }))
135 }
136
137 pub fn parsing_with_suggestion(
139 message: impl Into<String>,
140 source: Source,
141 suggestion: impl Into<String>,
142 ) -> Self {
143 Self::parsing_with_context(message, source, Some(suggestion), None, None)
144 }
145
146 pub fn inversion(
148 message: impl Into<String>,
149 source: Option<Source>,
150 suggestion: Option<impl Into<String>>,
151 ) -> Self {
152 Self::inversion_with_context(message, source, suggestion, None, None)
153 }
154
155 pub fn inversion_with_context(
157 message: impl Into<String>,
158 source: Option<Source>,
159 suggestion: Option<impl Into<String>>,
160 spec_context: Option<&LemmaSpec>,
161 related_spec: Option<&LemmaSpec>,
162 ) -> Self {
163 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
164 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
165 Self::Inversion(Box::new(ErrorDetails {
166 message: message.into(),
167 source,
168 suggestion: suggestion.map(Into::into),
169 spec_context_name,
170 spec_context_effective_from,
171 related_spec_name,
172 related_spec_effective_from,
173 related_data: None,
174 }))
175 }
176
177 pub fn inversion_with_suggestion(
179 message: impl Into<String>,
180 source: Option<Source>,
181 suggestion: impl Into<String>,
182 spec_context: Option<&LemmaSpec>,
183 related_spec: Option<&LemmaSpec>,
184 ) -> Self {
185 Self::inversion_with_context(
186 message,
187 source,
188 Some(suggestion),
189 spec_context,
190 related_spec,
191 )
192 }
193
194 pub fn validation(
196 message: impl Into<String>,
197 source: Option<Source>,
198 suggestion: Option<impl Into<String>>,
199 ) -> Self {
200 Self::validation_with_context(message, source, suggestion, None, None)
201 }
202
203 pub fn validation_with_context(
205 message: impl Into<String>,
206 source: Option<Source>,
207 suggestion: Option<impl Into<String>>,
208 spec_context: Option<&LemmaSpec>,
209 related_spec: Option<&LemmaSpec>,
210 ) -> Self {
211 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
212 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
213 Self::Validation(Box::new(ErrorDetails {
214 message: message.into(),
215 source,
216 suggestion: suggestion.map(Into::into),
217 spec_context_name,
218 spec_context_effective_from,
219 related_spec_name,
220 related_spec_effective_from,
221 related_data: None,
222 }))
223 }
224
225 pub fn request(message: impl Into<String>, suggestion: Option<impl Into<String>>) -> Self {
228 Self::request_with_kind(message, suggestion, RequestErrorKind::InvalidRequest)
229 }
230
231 pub fn request_not_found(
233 message: impl Into<String>,
234 suggestion: Option<impl Into<String>>,
235 ) -> Self {
236 Self::request_with_kind(message, suggestion, RequestErrorKind::SpecNotFound)
237 }
238
239 pub fn rule_not_found(rule_name: &str, suggestion: Option<impl Into<String>>) -> Self {
241 Self::request_with_kind(
242 format!("Rule '{}' not found", rule_name),
243 suggestion,
244 RequestErrorKind::RuleNotFound,
245 )
246 }
247
248 fn request_with_kind(
249 message: impl Into<String>,
250 suggestion: Option<impl Into<String>>,
251 kind: RequestErrorKind,
252 ) -> Self {
253 Self::Request {
254 details: Box::new(ErrorDetails {
255 message: message.into(),
256 source: None,
257 suggestion: suggestion.map(Into::into),
258 spec_context_name: None,
259 spec_context_effective_from: None,
260 related_spec_name: None,
261 related_spec_effective_from: None,
262 related_data: None,
263 }),
264 kind,
265 }
266 }
267
268 pub fn resource_limit_exceeded(
270 limit_name: impl Into<String>,
271 limit_value: impl Into<String>,
272 actual_value: impl Into<String>,
273 suggestion: impl Into<String>,
274 source: Option<Source>,
275 spec_context: Option<&LemmaSpec>,
276 related_spec: Option<&LemmaSpec>,
277 ) -> Self {
278 let limit_name = limit_name.into();
279 let limit_value = limit_value.into();
280 let actual_value = actual_value.into();
281 let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
282 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
283 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
284 Self::ResourceLimitExceeded {
285 details: Box::new(ErrorDetails {
286 message,
287 source,
288 suggestion: Some(suggestion.into()),
289 spec_context_name,
290 spec_context_effective_from,
291 related_spec_name,
292 related_spec_effective_from,
293 related_data: None,
294 }),
295 limit_name,
296 limit_value,
297 actual_value,
298 }
299 }
300
301 pub fn registry(
303 message: impl Into<String>,
304 source: Source,
305 identifier: impl Into<String>,
306 kind: RegistryErrorKind,
307 suggestion: Option<impl Into<String>>,
308 spec_context: Option<&LemmaSpec>,
309 related_spec: Option<&LemmaSpec>,
310 ) -> Self {
311 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
312 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
313 Self::Registry {
314 details: Box::new(ErrorDetails {
315 message: message.into(),
316 source: Some(source),
317 suggestion: suggestion.map(Into::into),
318 spec_context_name,
319 spec_context_effective_from,
320 related_spec_name,
321 related_spec_effective_from,
322 related_data: None,
323 }),
324 identifier: identifier.into(),
325 kind,
326 }
327 }
328
329 pub fn missing_repository(
331 message: impl Into<String>,
332 source: Option<Source>,
333 repository: impl Into<String>,
334 suggestion: Option<impl Into<String>>,
335 spec_context: Option<&LemmaSpec>,
336 ) -> Self {
337 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
338 Self::MissingRepository {
339 details: Box::new(ErrorDetails {
340 message: message.into(),
341 source,
342 suggestion: suggestion.map(Into::into),
343 spec_context_name,
344 spec_context_effective_from,
345 related_spec_name: None,
346 related_spec_effective_from: None,
347 related_data: None,
348 }),
349 repository: repository.into(),
350 }
351 }
352
353 pub fn with_spec_context(self, spec: &LemmaSpec) -> Self {
355 self.map_details(|d| {
356 d.spec_context_name = Some(spec.name.clone());
357 d.spec_context_effective_from = Some(spec.effective_from.clone());
358 })
359 }
360
361 pub fn with_related_data(self, name: impl Into<String>) -> Self {
365 let name = name.into();
366 self.map_details(|d| d.related_data = Some(name))
367 }
368
369 fn map_details(self, f: impl FnOnce(&mut ErrorDetails)) -> Self {
371 match self {
372 Error::Parsing(details) => {
373 let mut d = *details;
374 f(&mut d);
375 Error::Parsing(Box::new(d))
376 }
377 Error::Inversion(details) => {
378 let mut d = *details;
379 f(&mut d);
380 Error::Inversion(Box::new(d))
381 }
382 Error::Validation(details) => {
383 let mut d = *details;
384 f(&mut d);
385 Error::Validation(Box::new(d))
386 }
387 Error::Registry {
388 details,
389 identifier,
390 kind,
391 } => {
392 let mut d = *details;
393 f(&mut d);
394 Error::Registry {
395 details: Box::new(d),
396 identifier,
397 kind,
398 }
399 }
400 Error::MissingRepository {
401 details,
402 repository,
403 } => {
404 let mut d = *details;
405 f(&mut d);
406 Error::MissingRepository {
407 details: Box::new(d),
408 repository,
409 }
410 }
411 Error::ResourceLimitExceeded {
412 details,
413 limit_name,
414 limit_value,
415 actual_value,
416 } => {
417 let mut d = *details;
418 f(&mut d);
419 Error::ResourceLimitExceeded {
420 details: Box::new(d),
421 limit_name,
422 limit_value,
423 actual_value,
424 }
425 }
426 Error::Request { details, kind } => {
427 let mut d = *details;
428 f(&mut d);
429 Error::Request {
430 details: Box::new(d),
431 kind,
432 }
433 }
434 }
435 }
436}
437
438fn format_related_spec(name: &str, effective_from: &EffectiveDate) -> String {
439 let effective_from_str = effective_from
440 .as_ref()
441 .map(|d| d.to_string())
442 .unwrap_or_else(|| "beginning".to_string());
443 format!(
444 "See spec '{}' (effective from {}).",
445 name, effective_from_str
446 )
447}
448
449fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
450 if let Some(src) = source {
451 write!(
452 f,
453 " at {}:{}:{}",
454 src.source_type, src.span.line, src.span.col
455 )
456 } else {
457 Ok(())
458 }
459}
460
461fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
462 if let Some(ref name) = details.related_spec_name {
463 let effective = details
464 .related_spec_effective_from
465 .as_ref()
466 .expect("BUG: related_spec_name set without related_spec_effective_from");
467 write!(f, " {}", format_related_spec(name, effective))?;
468 }
469 Ok(())
470}
471
472fn write_spec_context(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
473 write!(f, "In spec '{}': ", name)
474}
475
476impl fmt::Display for Error {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 match self {
479 Error::Parsing(details) => {
480 if let Some(ref name) = details.spec_context_name {
481 write_spec_context(f, name)?;
482 }
483 write!(f, "Parse error: {}", details.message)?;
484 if let Some(suggestion) = &details.suggestion {
485 write!(f, " (suggestion: {suggestion})")?;
486 }
487 write_related_spec(f, details)?;
488 write_source_location(f, &details.source)
489 }
490 Error::Inversion(details) => {
491 if let Some(ref name) = details.spec_context_name {
492 write_spec_context(f, name)?;
493 }
494 write!(f, "Inversion error: {}", details.message)?;
495 if let Some(suggestion) = &details.suggestion {
496 write!(f, " (suggestion: {suggestion})")?;
497 }
498 write_related_spec(f, details)?;
499 write_source_location(f, &details.source)
500 }
501 Error::Validation(details) => {
502 if let Some(ref name) = details.spec_context_name {
503 write_spec_context(f, name)?;
504 }
505 write!(f, "Validation error: ")?;
506 if let Some(ref name) = details.related_data {
507 write!(f, "Failed to parse data '{}': ", name)?;
508 }
509 write!(f, "{}", details.message)?;
510 if let Some(suggestion) = &details.suggestion {
511 write!(f, " (suggestion: {suggestion})")?;
512 }
513 write_related_spec(f, details)?;
514 write_source_location(f, &details.source)
515 }
516 Error::Registry {
517 details,
518 identifier,
519 kind,
520 } => {
521 if let Some(ref name) = details.spec_context_name {
522 write_spec_context(f, name)?;
523 }
524 write!(
525 f,
526 "Registry error ({}): {}: {}",
527 kind, identifier, details.message
528 )?;
529 if let Some(suggestion) = &details.suggestion {
530 write!(f, " (suggestion: {suggestion})")?;
531 }
532 write_related_spec(f, details)?;
533 write_source_location(f, &details.source)
534 }
535 Error::MissingRepository {
536 details,
537 repository,
538 } => {
539 if let Some(ref name) = details.spec_context_name {
540 write_spec_context(f, name)?;
541 }
542 write!(f, "Missing repository: {}: {}", repository, details.message)?;
543 if let Some(suggestion) = &details.suggestion {
544 write!(f, " (suggestion: {suggestion})")?;
545 }
546 write_related_spec(f, details)?;
547 write_source_location(f, &details.source)
548 }
549 Error::ResourceLimitExceeded {
550 details,
551 limit_name,
552 limit_value,
553 actual_value,
554 } => {
555 if let Some(ref name) = details.spec_context_name {
556 write_spec_context(f, name)?;
557 }
558 write!(
559 f,
560 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
561 )?;
562 if let Some(suggestion) = &details.suggestion {
563 write!(f, ". {suggestion}")?;
564 }
565 write_source_location(f, &details.source)
566 }
567 Error::Request { details, .. } => {
568 if let Some(ref name) = details.spec_context_name {
569 write_spec_context(f, name)?;
570 }
571 write!(f, "Request error: {}", details.message)?;
572 if let Some(suggestion) = &details.suggestion {
573 write!(f, " (suggestion: {suggestion})")?;
574 }
575 write_related_spec(f, details)?;
576 write_source_location(f, &details.source)
577 }
578 }
579 }
580}
581
582impl std::error::Error for Error {}
583
584impl From<std::fmt::Error> for Error {
585 fn from(err: std::fmt::Error) -> Self {
586 Error::validation(format!("Format error: {err}"), None, None::<String>)
587 }
588}
589
590impl Error {
591 pub fn kind(&self) -> ErrorKind {
594 match self {
595 Error::Parsing(_) => ErrorKind::Parsing,
596 Error::Validation(_) => ErrorKind::Validation,
597 Error::Inversion(_) => ErrorKind::Inversion,
598 Error::Registry { .. } => ErrorKind::Registry,
599 Error::MissingRepository { .. } => ErrorKind::MissingRepository,
600 Error::Request { .. } => ErrorKind::Request,
601 Error::ResourceLimitExceeded { .. } => ErrorKind::ResourceLimit,
602 }
603 }
604
605 pub(crate) fn details(&self) -> &ErrorDetails {
607 match self {
608 Error::Parsing(d) | Error::Inversion(d) | Error::Validation(d) => d,
609 Error::Registry { details, .. }
610 | Error::MissingRepository { details, .. }
611 | Error::ResourceLimitExceeded { details, .. }
612 | Error::Request { details, .. } => details,
613 }
614 }
615
616 #[must_use]
620 pub fn repository(&self) -> Option<&str> {
621 match self {
622 Error::MissingRepository { repository, .. } => Some(repository.as_str()),
623 Error::Registry { identifier, .. } => Some(identifier.as_str()),
624 _ => None,
625 }
626 }
627
628 pub fn message(&self) -> &str {
630 &self.details().message
631 }
632
633 pub fn location(&self) -> Option<&Source> {
635 self.details().source.as_ref()
636 }
637
638 pub fn source_location(&self) -> Option<&Source> {
640 self.location()
641 }
642
643 pub fn source_text(
645 &self,
646 sources: &std::collections::HashMap<crate::parsing::source::SourceType, String>,
647 ) -> Option<String> {
648 self.location()
649 .and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
650 }
651
652 pub fn suggestion(&self) -> Option<&str> {
654 self.details().suggestion.as_deref()
655 }
656
657 pub fn related_data(&self) -> Option<&str> {
659 self.details().related_data.as_deref()
660 }
661
662 pub fn spec_context_name(&self) -> Option<&str> {
664 self.details().spec_context_name.as_deref()
665 }
666
667 pub fn related_spec(&self) -> Option<&str> {
669 self.details().related_spec_name.as_deref()
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use crate::parsing::ast::Span;
677
678 fn test_source() -> Source {
679 Source::new(
680 crate::parsing::source::SourceType::Path(std::sync::Arc::new(
681 std::path::PathBuf::from("test.lemma"),
682 )),
683 Span {
684 start: 14,
685 end: 21,
686 line: 1,
687 col: 15,
688 },
689 )
690 }
691
692 #[test]
693 fn test_error_creation_and_display() {
694 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
695 let parse_error_display = format!("{parse_error}");
696 assert!(parse_error_display.contains("Parse error: Invalid currency"));
697 assert!(parse_error_display.contains("test.lemma:1:15"));
698
699 let suggestion_source = Source::new(
700 crate::parsing::source::SourceType::Volatile,
701 Span {
702 start: 5,
703 end: 10,
704 line: 2,
705 col: 3,
706 },
707 );
708 let suggestion_error =
709 Error::parsing_with_suggestion("typo", suggestion_source, "did you mean X?");
710 assert!(format!("{suggestion_error}").contains("suggestion: did you mean X?"));
711 }
712
713 #[test]
714 fn test_request_error_accessors() {
715 let err = Error::request("bad id", Some("use a valid id"));
716 assert_eq!(err.kind(), ErrorKind::Request);
717 assert_eq!(err.message(), "bad id");
718 assert!(err.location().is_none());
719 assert_eq!(err.suggestion(), Some("use a valid id"));
720 assert!(err.spec_context_name().is_none());
721 assert!(err.related_spec().is_none());
722 }
723
724 #[test]
725 fn test_missing_repository_display() {
726 let err = Error::missing_repository(
727 "not loaded",
728 None,
729 "@iso/countries",
730 Some("load the dependency first"),
731 None,
732 );
733 let display = format!("{err}");
734 assert!(display.contains("Missing repository"));
735 assert!(display.contains("@iso/countries"));
736 assert!(display.contains("not loaded"));
737 }
738
739 #[test]
740 fn test_with_spec_context_copies_name() {
741 let spec = LemmaSpec::new("pricing".to_string());
742 let err = Error::validation("bad", None, None::<String>).with_spec_context(&spec);
743 assert_eq!(err.spec_context_name(), Some("pricing"));
744 let display = format!("{err}");
745 assert!(display.contains("In spec 'pricing':"));
746 }
747}