1use crate::parsing::ast::LemmaSpec;
2use crate::parsing::source::Source;
3use crate::registry::RegistryErrorKind;
4use std::fmt;
5use std::sync::Arc;
6
7#[derive(Debug, Clone)]
9pub struct ErrorDetails {
10 pub message: String,
11 pub source: Option<Source>,
12 pub suggestion: Option<String>,
13 pub spec_context: Option<Arc<LemmaSpec>>,
15 pub related_spec: Option<Arc<LemmaSpec>>,
17 pub related_data: Option<String>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ErrorKind {
26 Parsing,
27 Validation,
28 Inversion,
29 Registry,
30 MissingRepository,
31 Request,
32 ResourceLimit,
33}
34
35#[derive(Debug, Clone)]
37pub enum Error {
38 Parsing(Box<ErrorDetails>),
40
41 Inversion(Box<ErrorDetails>),
43
44 Validation(Box<ErrorDetails>),
46
47 Registry {
53 details: Box<ErrorDetails>,
54 identifier: String,
56 kind: RegistryErrorKind,
58 },
59
60 MissingRepository {
65 details: Box<ErrorDetails>,
66 repository: String,
68 },
69
70 ResourceLimitExceeded {
72 details: Box<ErrorDetails>,
73 limit_name: String,
74 limit_value: String,
75 actual_value: String,
76 },
77
78 Request {
81 details: Box<ErrorDetails>,
82 kind: RequestErrorKind,
83 },
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum RequestErrorKind {
89 SpecNotFound,
91 RuleNotFound,
93 InvalidRequest,
95}
96
97impl Error {
98 pub fn parsing(
100 message: impl Into<String>,
101 source: Source,
102 suggestion: Option<impl Into<String>>,
103 ) -> Self {
104 Self::parsing_with_context(message, source, suggestion, None, None)
105 }
106
107 pub fn parsing_with_context(
109 message: impl Into<String>,
110 source: Source,
111 suggestion: Option<impl Into<String>>,
112 spec_context: Option<Arc<LemmaSpec>>,
113 related_spec: Option<Arc<LemmaSpec>>,
114 ) -> Self {
115 Self::Parsing(Box::new(ErrorDetails {
116 message: message.into(),
117 source: Some(source),
118 suggestion: suggestion.map(Into::into),
119 spec_context,
120 related_spec,
121 related_data: None,
122 }))
123 }
124
125 pub fn parsing_with_suggestion(
127 message: impl Into<String>,
128 source: Source,
129 suggestion: impl Into<String>,
130 ) -> Self {
131 Self::parsing_with_context(message, source, Some(suggestion), None, None)
132 }
133
134 pub fn inversion(
136 message: impl Into<String>,
137 source: Option<Source>,
138 suggestion: Option<impl Into<String>>,
139 ) -> Self {
140 Self::inversion_with_context(message, source, suggestion, None, None)
141 }
142
143 pub fn inversion_with_context(
145 message: impl Into<String>,
146 source: Option<Source>,
147 suggestion: Option<impl Into<String>>,
148 spec_context: Option<Arc<LemmaSpec>>,
149 related_spec: Option<Arc<LemmaSpec>>,
150 ) -> Self {
151 Self::Inversion(Box::new(ErrorDetails {
152 message: message.into(),
153 source,
154 suggestion: suggestion.map(Into::into),
155 spec_context,
156 related_spec,
157 related_data: None,
158 }))
159 }
160
161 pub fn inversion_with_suggestion(
163 message: impl Into<String>,
164 source: Option<Source>,
165 suggestion: impl Into<String>,
166 spec_context: Option<Arc<LemmaSpec>>,
167 related_spec: Option<Arc<LemmaSpec>>,
168 ) -> Self {
169 Self::inversion_with_context(
170 message,
171 source,
172 Some(suggestion),
173 spec_context,
174 related_spec,
175 )
176 }
177
178 pub fn validation(
180 message: impl Into<String>,
181 source: Option<Source>,
182 suggestion: Option<impl Into<String>>,
183 ) -> Self {
184 Self::validation_with_context(message, source, suggestion, None, None)
185 }
186
187 pub fn validation_with_context(
189 message: impl Into<String>,
190 source: Option<Source>,
191 suggestion: Option<impl Into<String>>,
192 spec_context: Option<Arc<LemmaSpec>>,
193 related_spec: Option<Arc<LemmaSpec>>,
194 ) -> Self {
195 Self::Validation(Box::new(ErrorDetails {
196 message: message.into(),
197 source,
198 suggestion: suggestion.map(Into::into),
199 spec_context,
200 related_spec,
201 related_data: None,
202 }))
203 }
204
205 pub fn request(message: impl Into<String>, suggestion: Option<impl Into<String>>) -> Self {
208 Self::request_with_kind(message, suggestion, RequestErrorKind::InvalidRequest)
209 }
210
211 pub fn request_not_found(
213 message: impl Into<String>,
214 suggestion: Option<impl Into<String>>,
215 ) -> Self {
216 Self::request_with_kind(message, suggestion, RequestErrorKind::SpecNotFound)
217 }
218
219 pub fn rule_not_found(rule_name: &str, suggestion: Option<impl Into<String>>) -> Self {
221 Self::request_with_kind(
222 format!("Rule '{}' not found", rule_name),
223 suggestion,
224 RequestErrorKind::RuleNotFound,
225 )
226 }
227
228 fn request_with_kind(
229 message: impl Into<String>,
230 suggestion: Option<impl Into<String>>,
231 kind: RequestErrorKind,
232 ) -> Self {
233 Self::Request {
234 details: Box::new(ErrorDetails {
235 message: message.into(),
236 source: None,
237 suggestion: suggestion.map(Into::into),
238 spec_context: None,
239 related_spec: None,
240 related_data: None,
241 }),
242 kind,
243 }
244 }
245
246 pub fn resource_limit_exceeded(
248 limit_name: impl Into<String>,
249 limit_value: impl Into<String>,
250 actual_value: impl Into<String>,
251 suggestion: impl Into<String>,
252 source: Option<Source>,
253 spec_context: Option<Arc<LemmaSpec>>,
254 related_spec: Option<Arc<LemmaSpec>>,
255 ) -> Self {
256 let limit_name = limit_name.into();
257 let limit_value = limit_value.into();
258 let actual_value = actual_value.into();
259 let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
260 Self::ResourceLimitExceeded {
261 details: Box::new(ErrorDetails {
262 message,
263 source,
264 suggestion: Some(suggestion.into()),
265 spec_context,
266 related_spec,
267 related_data: None,
268 }),
269 limit_name,
270 limit_value,
271 actual_value,
272 }
273 }
274
275 pub fn registry(
277 message: impl Into<String>,
278 source: Source,
279 identifier: impl Into<String>,
280 kind: RegistryErrorKind,
281 suggestion: Option<impl Into<String>>,
282 spec_context: Option<Arc<LemmaSpec>>,
283 related_spec: Option<Arc<LemmaSpec>>,
284 ) -> Self {
285 Self::Registry {
286 details: Box::new(ErrorDetails {
287 message: message.into(),
288 source: Some(source),
289 suggestion: suggestion.map(Into::into),
290 spec_context,
291 related_spec,
292 related_data: None,
293 }),
294 identifier: identifier.into(),
295 kind,
296 }
297 }
298
299 pub fn missing_repository(
301 message: impl Into<String>,
302 source: Option<Source>,
303 repository: impl Into<String>,
304 suggestion: Option<impl Into<String>>,
305 spec_context: Option<Arc<LemmaSpec>>,
306 ) -> Self {
307 Self::MissingRepository {
308 details: Box::new(ErrorDetails {
309 message: message.into(),
310 source,
311 suggestion: suggestion.map(Into::into),
312 spec_context,
313 related_spec: None,
314 related_data: None,
315 }),
316 repository: repository.into(),
317 }
318 }
319
320 pub fn with_spec_context(self, spec: Arc<LemmaSpec>) -> Self {
322 self.map_details(|d| d.spec_context = Some(spec))
323 }
324
325 pub fn with_related_data(self, name: impl Into<String>) -> Self {
329 let name = name.into();
330 self.map_details(|d| d.related_data = Some(name))
331 }
332
333 fn map_details(self, f: impl FnOnce(&mut ErrorDetails)) -> Self {
335 match self {
336 Error::Parsing(details) => {
337 let mut d = *details;
338 f(&mut d);
339 Error::Parsing(Box::new(d))
340 }
341 Error::Inversion(details) => {
342 let mut d = *details;
343 f(&mut d);
344 Error::Inversion(Box::new(d))
345 }
346 Error::Validation(details) => {
347 let mut d = *details;
348 f(&mut d);
349 Error::Validation(Box::new(d))
350 }
351 Error::Registry {
352 details,
353 identifier,
354 kind,
355 } => {
356 let mut d = *details;
357 f(&mut d);
358 Error::Registry {
359 details: Box::new(d),
360 identifier,
361 kind,
362 }
363 }
364 Error::MissingRepository {
365 details,
366 repository,
367 } => {
368 let mut d = *details;
369 f(&mut d);
370 Error::MissingRepository {
371 details: Box::new(d),
372 repository,
373 }
374 }
375 Error::ResourceLimitExceeded {
376 details,
377 limit_name,
378 limit_value,
379 actual_value,
380 } => {
381 let mut d = *details;
382 f(&mut d);
383 Error::ResourceLimitExceeded {
384 details: Box::new(d),
385 limit_name,
386 limit_value,
387 actual_value,
388 }
389 }
390 Error::Request { details, kind } => {
391 let mut d = *details;
392 f(&mut d);
393 Error::Request {
394 details: Box::new(d),
395 kind,
396 }
397 }
398 }
399 }
400}
401
402fn format_related_spec(spec: &LemmaSpec) -> String {
403 let effective_from_str = spec
404 .effective_from()
405 .map(|d| d.to_string())
406 .unwrap_or_else(|| "beginning".to_string());
407 format!(
408 "See spec '{}' (effective from {}).",
409 spec.name, effective_from_str
410 )
411}
412
413fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
414 if let Some(src) = source {
415 write!(
416 f,
417 " at {}:{}:{}",
418 src.source_type, src.span.line, src.span.col
419 )
420 } else {
421 Ok(())
422 }
423}
424
425fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
426 if let Some(ref related) = details.related_spec {
427 write!(f, " {}", format_related_spec(related))?;
428 }
429 Ok(())
430}
431
432fn write_spec_context(f: &mut fmt::Formatter<'_>, spec: &LemmaSpec) -> fmt::Result {
433 write!(f, "In spec '{}': ", spec.name)
434}
435
436impl fmt::Display for Error {
437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438 match self {
439 Error::Parsing(details) => {
440 if let Some(ref spec) = details.spec_context {
441 write_spec_context(f, spec)?;
442 }
443 write!(f, "Parse error: {}", details.message)?;
444 if let Some(suggestion) = &details.suggestion {
445 write!(f, " (suggestion: {suggestion})")?;
446 }
447 write_related_spec(f, details)?;
448 write_source_location(f, &details.source)
449 }
450 Error::Inversion(details) => {
451 if let Some(ref spec) = details.spec_context {
452 write_spec_context(f, spec)?;
453 }
454 write!(f, "Inversion error: {}", details.message)?;
455 if let Some(suggestion) = &details.suggestion {
456 write!(f, " (suggestion: {suggestion})")?;
457 }
458 write_related_spec(f, details)?;
459 write_source_location(f, &details.source)
460 }
461 Error::Validation(details) => {
462 if let Some(ref spec) = details.spec_context {
463 write_spec_context(f, spec)?;
464 }
465 write!(f, "Validation error: ")?;
466 if let Some(ref name) = details.related_data {
467 write!(f, "Failed to parse data '{}': ", name)?;
468 }
469 write!(f, "{}", details.message)?;
470 if let Some(suggestion) = &details.suggestion {
471 write!(f, " (suggestion: {suggestion})")?;
472 }
473 write_related_spec(f, details)?;
474 write_source_location(f, &details.source)
475 }
476 Error::Registry {
477 details,
478 identifier,
479 kind,
480 } => {
481 if let Some(ref spec) = details.spec_context {
482 write_spec_context(f, spec)?;
483 }
484 write!(
485 f,
486 "Registry error ({}): {}: {}",
487 kind, identifier, details.message
488 )?;
489 if let Some(suggestion) = &details.suggestion {
490 write!(f, " (suggestion: {suggestion})")?;
491 }
492 write_related_spec(f, details)?;
493 write_source_location(f, &details.source)
494 }
495 Error::MissingRepository {
496 details,
497 repository,
498 } => {
499 if let Some(ref spec) = details.spec_context {
500 write_spec_context(f, spec)?;
501 }
502 write!(f, "Missing repository: {}: {}", repository, details.message)?;
503 if let Some(suggestion) = &details.suggestion {
504 write!(f, " (suggestion: {suggestion})")?;
505 }
506 write_related_spec(f, details)?;
507 write_source_location(f, &details.source)
508 }
509 Error::ResourceLimitExceeded {
510 details,
511 limit_name,
512 limit_value,
513 actual_value,
514 } => {
515 if let Some(ref spec) = details.spec_context {
516 write_spec_context(f, spec)?;
517 }
518 write!(
519 f,
520 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
521 )?;
522 if let Some(suggestion) = &details.suggestion {
523 write!(f, ". {suggestion}")?;
524 }
525 write_source_location(f, &details.source)
526 }
527 Error::Request { details, .. } => {
528 if let Some(ref spec) = details.spec_context {
529 write_spec_context(f, spec)?;
530 }
531 write!(f, "Request error: {}", details.message)?;
532 if let Some(suggestion) = &details.suggestion {
533 write!(f, " (suggestion: {suggestion})")?;
534 }
535 write_related_spec(f, details)?;
536 write_source_location(f, &details.source)
537 }
538 }
539 }
540}
541
542impl std::error::Error for Error {}
543
544impl From<std::fmt::Error> for Error {
545 fn from(err: std::fmt::Error) -> Self {
546 Error::validation(format!("Format error: {err}"), None, None::<String>)
547 }
548}
549
550impl Error {
551 pub fn kind(&self) -> ErrorKind {
554 match self {
555 Error::Parsing(_) => ErrorKind::Parsing,
556 Error::Validation(_) => ErrorKind::Validation,
557 Error::Inversion(_) => ErrorKind::Inversion,
558 Error::Registry { .. } => ErrorKind::Registry,
559 Error::MissingRepository { .. } => ErrorKind::MissingRepository,
560 Error::Request { .. } => ErrorKind::Request,
561 Error::ResourceLimitExceeded { .. } => ErrorKind::ResourceLimit,
562 }
563 }
564
565 fn details(&self) -> &ErrorDetails {
567 match self {
568 Error::Parsing(d) | Error::Inversion(d) | Error::Validation(d) => d,
569 Error::Registry { details, .. }
570 | Error::MissingRepository { details, .. }
571 | Error::ResourceLimitExceeded { details, .. }
572 | Error::Request { details, .. } => details,
573 }
574 }
575
576 #[must_use]
580 pub fn repository(&self) -> Option<&str> {
581 match self {
582 Error::MissingRepository { repository, .. } => Some(repository.as_str()),
583 Error::Registry { identifier, .. } => Some(identifier.as_str()),
584 _ => None,
585 }
586 }
587
588 pub fn message(&self) -> &str {
590 &self.details().message
591 }
592
593 pub fn location(&self) -> Option<&Source> {
595 self.details().source.as_ref()
596 }
597
598 pub fn source_location(&self) -> Option<&Source> {
600 self.location()
601 }
602
603 pub fn source_text(
605 &self,
606 sources: &std::collections::HashMap<crate::parsing::source::SourceType, String>,
607 ) -> Option<String> {
608 self.location()
609 .and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
610 }
611
612 pub fn suggestion(&self) -> Option<&str> {
614 self.details().suggestion.as_deref()
615 }
616
617 pub fn related_data(&self) -> Option<&str> {
619 self.details().related_data.as_deref()
620 }
621
622 pub fn spec_context_name(&self) -> Option<&str> {
624 self.details()
625 .spec_context
626 .as_ref()
627 .map(|s| s.name.as_str())
628 }
629
630 pub fn related_spec(&self) -> Option<&str> {
632 self.details()
633 .related_spec
634 .as_ref()
635 .map(|s| s.name.as_str())
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642 use crate::parsing::ast::Span;
643
644 fn test_source() -> Source {
645 Source::new(
646 crate::parsing::source::SourceType::Path(std::sync::Arc::new(
647 std::path::PathBuf::from("test.lemma"),
648 )),
649 Span {
650 start: 14,
651 end: 21,
652 line: 1,
653 col: 15,
654 },
655 )
656 }
657
658 #[test]
659 fn test_error_creation_and_display() {
660 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
661 let parse_error_display = format!("{parse_error}");
662 assert!(parse_error_display.contains("Parse error: Invalid currency"));
663 assert!(parse_error_display.contains("test.lemma:1:15"));
664
665 let suggestion_source = Source::new(
666 crate::parsing::source::SourceType::Volatile,
667 Span {
668 start: 5,
669 end: 10,
670 line: 1,
671 col: 6,
672 },
673 );
674
675 let parse_error_with_suggestion = Error::parsing_with_suggestion(
676 "Typo in data name",
677 suggestion_source,
678 "Did you mean 'amount'?",
679 );
680 let parse_error_with_suggestion_display = format!("{parse_error_with_suggestion}");
681 assert!(parse_error_with_suggestion_display.contains("Typo in data name"));
682 assert!(parse_error_with_suggestion_display.contains("Did you mean 'amount'?"));
683
684 let engine_error = Error::validation("Something went wrong", None, None::<String>);
685 assert!(format!("{engine_error}").contains("Validation error: Something went wrong"));
686 assert!(!format!("{engine_error}").contains(" at "));
687
688 let validation_error =
689 Error::validation("Circular dependency: a -> b -> a", None, None::<String>);
690 assert!(format!("{validation_error}")
691 .contains("Validation error: Circular dependency: a -> b -> a"));
692 }
693
694 #[test]
695 fn test_error_kind_accessor() {
696 assert_eq!(
697 Error::parsing("x", test_source(), None::<String>).kind(),
698 ErrorKind::Parsing
699 );
700 assert_eq!(
701 Error::validation("x", None, None::<String>).kind(),
702 ErrorKind::Validation
703 );
704 assert_eq!(
705 Error::inversion("x", None, None::<String>).kind(),
706 ErrorKind::Inversion
707 );
708 assert_eq!(
709 Error::request("x", None::<String>).kind(),
710 ErrorKind::Request
711 );
712 assert_eq!(
713 Error::resource_limit_exceeded("cap", "1", "2", "try less", None, None, None).kind(),
714 ErrorKind::ResourceLimit
715 );
716 }
717
718 #[test]
719 fn test_missing_repository_kind_display_and_repository() {
720 let err = Error::missing_repository(
721 "'main' references 'x' from '@org/pkg', but repository '@org/pkg' is not loaded",
722 Some(test_source()),
723 "@org/pkg",
724 Some("Run `lemma fetch @org/pkg` to download the repository.".to_string()),
725 None,
726 );
727 assert_eq!(err.kind(), ErrorKind::MissingRepository);
728 assert_eq!(err.repository(), Some("@org/pkg"));
729 let display = format!("{err}");
730 assert!(
731 display.contains("Missing repository: @org/pkg:"),
732 "unexpected display: {display}"
733 );
734 }
735
736 #[test]
737 fn test_registry_repository_accessor() {
738 let err = Error::registry(
739 "HTTP 404",
740 test_source(),
741 "@x/y",
742 crate::registry::RegistryErrorKind::NotFound,
743 None::<String>,
744 None,
745 None,
746 );
747 assert_eq!(err.kind(), ErrorKind::Registry);
748 assert_eq!(err.repository(), Some("@x/y"));
749 }
750
751 #[test]
752 fn test_related_data_attribution_and_display() {
753 let err = Error::validation(
754 "Unknown unit 'mete' for this scale type",
755 Some(test_source()),
756 None::<String>,
757 )
758 .with_related_data("bridge_height");
759
760 assert_eq!(err.related_data(), Some("bridge_height"));
761 assert_eq!(err.kind(), ErrorKind::Validation);
762 assert_eq!(err.message(), "Unknown unit 'mete' for this scale type");
763
764 let display = format!("{err}");
765 assert!(
766 display.contains(
767 "Validation error: Failed to parse data 'bridge_height': Unknown unit 'mete'"
768 ),
769 "unexpected display: {display}"
770 );
771
772 let at_occurrences = display.matches(" at ").count();
773 assert_eq!(
774 at_occurrences, 1,
775 "expected exactly one ` at ` in display, got {at_occurrences}: {display}"
776 );
777 }
778
779 #[test]
780 fn test_related_data_none_by_default() {
781 let err = Error::validation("x", None, None::<String>);
782 assert!(err.related_data().is_none());
783 assert!(err.spec_context_name().is_none());
784 assert!(err.related_spec().is_none());
785 }
786
787 #[test]
788 fn test_related_data_builder_preserves_other_variants() {
789 let err = Error::resource_limit_exceeded(
790 "max_data_value_bytes",
791 "100",
792 "200",
793 "reduce size",
794 Some(test_source()),
795 None,
796 None,
797 )
798 .with_related_data("big_blob");
799
800 assert_eq!(err.kind(), ErrorKind::ResourceLimit);
801 assert_eq!(err.related_data(), Some("big_blob"));
802 }
803}