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