1use crate::parsing::ast::{EffectiveDate, LemmaSpec};
2use crate::parsing::source::Source;
3use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
11#[serde(rename_all = "snake_case")]
12pub enum RegistryErrorKind {
13 NotFound,
15 Unauthorized,
17 NetworkError,
19 ServerError,
21 Other,
23}
24
25impl fmt::Display for RegistryErrorKind {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::NotFound => write!(f, "not found"),
29 Self::Unauthorized => write!(f, "unauthorized"),
30 Self::NetworkError => write!(f, "network error"),
31 Self::ServerError => write!(f, "server error"),
32 Self::Other => write!(f, "error"),
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct ErrorDetails {
40 pub message: String,
41 pub source: Option<Source>,
42 pub suggestion: Option<String>,
43 pub spec_context_name: Option<String>,
45 pub spec_context_effective_from: Option<EffectiveDate>,
46 pub related_spec_name: Option<String>,
48 pub related_spec_effective_from: Option<EffectiveDate>,
49 pub related_data: Option<String>,
52}
53
54fn attribution_fields(spec: Option<&LemmaSpec>) -> (Option<String>, Option<EffectiveDate>) {
55 match spec {
56 Some(s) => (Some(s.name.clone()), Some(s.effective_from.clone())),
57 None => (None, None),
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
63#[serde(rename_all = "snake_case")]
64pub enum ErrorKind {
65 Parsing,
66 Validation,
67 Registry,
68 MissingRepository,
69 Request,
70 ResourceLimit,
71}
72
73#[derive(Debug, Clone)]
75pub enum Error {
76 Parsing(Box<ErrorDetails>),
78
79 Validation(Box<ErrorDetails>),
81
82 Registry {
88 details: Box<ErrorDetails>,
89 identifier: String,
91 kind: RegistryErrorKind,
93 },
94
95 MissingRepository {
100 details: Box<ErrorDetails>,
101 repository: String,
103 },
104
105 ResourceLimitExceeded {
107 details: Box<ErrorDetails>,
108 limit_name: String,
109 limit_value: String,
110 actual_value: String,
111 },
112
113 Request {
116 details: Box<ErrorDetails>,
117 kind: RequestErrorKind,
118 },
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
123#[serde(rename_all = "snake_case")]
124pub enum RequestErrorKind {
125 SpecNotFound,
127 RuleNotFound,
129 InvalidRequest,
131}
132
133impl Error {
134 pub fn parsing(
136 message: impl Into<String>,
137 source: Source,
138 suggestion: Option<impl Into<String>>,
139 ) -> Self {
140 Self::parsing_with_context(message, source, suggestion, None, None)
141 }
142
143 pub fn parsing_with_context(
145 message: impl Into<String>,
146 source: Source,
147 suggestion: Option<impl Into<String>>,
148 spec_context: Option<&LemmaSpec>,
149 related_spec: Option<&LemmaSpec>,
150 ) -> Self {
151 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
152 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
153 Self::Parsing(Box::new(ErrorDetails {
154 message: message.into(),
155 source: Some(source),
156 suggestion: suggestion.map(Into::into),
157 spec_context_name,
158 spec_context_effective_from,
159 related_spec_name,
160 related_spec_effective_from,
161 related_data: None,
162 }))
163 }
164
165 pub fn parsing_with_suggestion(
167 message: impl Into<String>,
168 source: Source,
169 suggestion: impl Into<String>,
170 ) -> Self {
171 Self::parsing_with_context(message, source, Some(suggestion), None, None)
172 }
173
174 pub fn validation(
176 message: impl Into<String>,
177 source: Option<Source>,
178 suggestion: Option<impl Into<String>>,
179 ) -> Self {
180 Self::validation_with_context(message, source, suggestion, None, None)
181 }
182
183 pub fn validation_with_context(
185 message: impl Into<String>,
186 source: Option<Source>,
187 suggestion: Option<impl Into<String>>,
188 spec_context: Option<&LemmaSpec>,
189 related_spec: Option<&LemmaSpec>,
190 ) -> Self {
191 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
192 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
193 Self::Validation(Box::new(ErrorDetails {
194 message: message.into(),
195 source,
196 suggestion: suggestion.map(Into::into),
197 spec_context_name,
198 spec_context_effective_from,
199 related_spec_name,
200 related_spec_effective_from,
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_name: None,
239 spec_context_effective_from: None,
240 related_spec_name: None,
241 related_spec_effective_from: None,
242 related_data: None,
243 }),
244 kind,
245 }
246 }
247
248 pub fn resource_limit_exceeded(
250 limit_name: impl Into<String>,
251 limit_value: impl Into<String>,
252 actual_value: impl Into<String>,
253 suggestion: impl Into<String>,
254 source: Option<Source>,
255 spec_context: Option<&LemmaSpec>,
256 related_spec: Option<&LemmaSpec>,
257 ) -> Self {
258 let limit_name = limit_name.into();
259 let limit_value = limit_value.into();
260 let actual_value = actual_value.into();
261 let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
262 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
263 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
264 Self::ResourceLimitExceeded {
265 details: Box::new(ErrorDetails {
266 message,
267 source,
268 suggestion: Some(suggestion.into()),
269 spec_context_name,
270 spec_context_effective_from,
271 related_spec_name,
272 related_spec_effective_from,
273 related_data: None,
274 }),
275 limit_name,
276 limit_value,
277 actual_value,
278 }
279 }
280
281 pub fn registry(
283 message: impl Into<String>,
284 source: Source,
285 identifier: impl Into<String>,
286 kind: RegistryErrorKind,
287 suggestion: Option<impl Into<String>>,
288 spec_context: Option<&LemmaSpec>,
289 related_spec: Option<&LemmaSpec>,
290 ) -> Self {
291 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
292 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
293 Self::Registry {
294 details: Box::new(ErrorDetails {
295 message: message.into(),
296 source: Some(source),
297 suggestion: suggestion.map(Into::into),
298 spec_context_name,
299 spec_context_effective_from,
300 related_spec_name,
301 related_spec_effective_from,
302 related_data: None,
303 }),
304 identifier: identifier.into(),
305 kind,
306 }
307 }
308
309 pub fn missing_repository(
311 message: impl Into<String>,
312 source: Option<Source>,
313 repository: impl Into<String>,
314 suggestion: Option<impl Into<String>>,
315 spec_context: Option<&LemmaSpec>,
316 ) -> Self {
317 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
318 Self::MissingRepository {
319 details: Box::new(ErrorDetails {
320 message: message.into(),
321 source,
322 suggestion: suggestion.map(Into::into),
323 spec_context_name,
324 spec_context_effective_from,
325 related_spec_name: None,
326 related_spec_effective_from: None,
327 related_data: None,
328 }),
329 repository: repository.into(),
330 }
331 }
332
333 pub fn with_spec_context(self, spec: &LemmaSpec) -> Self {
337 self.map_details(|d| {
338 if d.spec_context_name.is_some() {
339 assert!(
340 d.spec_context_effective_from.is_some(),
341 "BUG: spec_context_name set without spec_context_effective_from"
342 );
343 return;
344 }
345 d.spec_context_name = Some(spec.name.clone());
346 d.spec_context_effective_from = Some(spec.effective_from.clone());
347 })
348 }
349
350 pub fn with_related_data(self, name: impl Into<String>) -> Self {
354 let name = name.into();
355 self.map_details(|d| d.related_data = Some(name))
356 }
357
358 fn map_details(self, f: impl FnOnce(&mut ErrorDetails)) -> Self {
360 match self {
361 Error::Parsing(details) => {
362 let mut d = *details;
363 f(&mut d);
364 Error::Parsing(Box::new(d))
365 }
366 Error::Validation(details) => {
367 let mut d = *details;
368 f(&mut d);
369 Error::Validation(Box::new(d))
370 }
371 Error::Registry {
372 details,
373 identifier,
374 kind,
375 } => {
376 let mut d = *details;
377 f(&mut d);
378 Error::Registry {
379 details: Box::new(d),
380 identifier,
381 kind,
382 }
383 }
384 Error::MissingRepository {
385 details,
386 repository,
387 } => {
388 let mut d = *details;
389 f(&mut d);
390 Error::MissingRepository {
391 details: Box::new(d),
392 repository,
393 }
394 }
395 Error::ResourceLimitExceeded {
396 details,
397 limit_name,
398 limit_value,
399 actual_value,
400 } => {
401 let mut d = *details;
402 f(&mut d);
403 Error::ResourceLimitExceeded {
404 details: Box::new(d),
405 limit_name,
406 limit_value,
407 actual_value,
408 }
409 }
410 Error::Request { details, kind } => {
411 let mut d = *details;
412 f(&mut d);
413 Error::Request {
414 details: Box::new(d),
415 kind,
416 }
417 }
418 }
419 }
420}
421
422fn format_related_spec(name: &str, effective_from: &EffectiveDate) -> String {
423 let effective_from_str = effective_from
424 .as_ref()
425 .map(|d| d.to_string())
426 .unwrap_or_else(|| "beginning".to_string());
427 format!(
428 "See spec '{}' (effective from {}).",
429 name, effective_from_str
430 )
431}
432
433fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
434 if let Some(src) = source {
435 write!(
436 f,
437 " at {}:{}:{}",
438 src.source_type, src.span.line, src.span.col
439 )
440 } else {
441 Ok(())
442 }
443}
444
445fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
446 if let Some(ref name) = details.related_spec_name {
447 let effective = details
448 .related_spec_effective_from
449 .as_ref()
450 .expect("BUG: related_spec_name set without related_spec_effective_from");
451 write!(f, " {}", format_related_spec(name, effective))?;
452 }
453 Ok(())
454}
455
456fn write_spec_context(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
457 write!(f, "In spec '{}': ", name)
458}
459
460impl fmt::Display for Error {
461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462 match self {
463 Error::Parsing(details) => {
464 if let Some(ref name) = details.spec_context_name {
465 write_spec_context(f, name)?;
466 }
467 write!(f, "Parse error: {}", details.message)?;
468 if let Some(suggestion) = &details.suggestion {
469 write!(f, " (suggestion: {suggestion})")?;
470 }
471 write_related_spec(f, details)?;
472 write_source_location(f, &details.source)
473 }
474 Error::Validation(details) => {
475 if let Some(ref name) = details.spec_context_name {
476 write_spec_context(f, name)?;
477 }
478 write!(f, "Validation error: ")?;
479 if let Some(ref name) = details.related_data {
480 write!(f, "Failed to parse data '{}': ", name)?;
481 }
482 write!(f, "{}", details.message)?;
483 if let Some(suggestion) = &details.suggestion {
484 write!(f, " (suggestion: {suggestion})")?;
485 }
486 write_related_spec(f, details)?;
487 write_source_location(f, &details.source)
488 }
489 Error::Registry {
490 details,
491 identifier,
492 kind,
493 } => {
494 if let Some(ref name) = details.spec_context_name {
495 write_spec_context(f, name)?;
496 }
497 write!(
498 f,
499 "Registry error ({}): {}: {}",
500 kind, identifier, details.message
501 )?;
502 if let Some(suggestion) = &details.suggestion {
503 write!(f, " (suggestion: {suggestion})")?;
504 }
505 write_related_spec(f, details)?;
506 write_source_location(f, &details.source)
507 }
508 Error::MissingRepository {
509 details,
510 repository,
511 } => {
512 if let Some(ref name) = details.spec_context_name {
513 write_spec_context(f, name)?;
514 }
515 write!(f, "Missing repository: {}: {}", repository, details.message)?;
516 if let Some(suggestion) = &details.suggestion {
517 write!(f, " (suggestion: {suggestion})")?;
518 }
519 write_related_spec(f, details)?;
520 write_source_location(f, &details.source)
521 }
522 Error::ResourceLimitExceeded {
523 details,
524 limit_name,
525 limit_value,
526 actual_value,
527 } => {
528 if let Some(ref name) = details.spec_context_name {
529 write_spec_context(f, name)?;
530 }
531 write!(
532 f,
533 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
534 )?;
535 if let Some(suggestion) = &details.suggestion {
536 write!(f, ". {suggestion}")?;
537 }
538 write_source_location(f, &details.source)
539 }
540 Error::Request { details, .. } => {
541 if let Some(ref name) = details.spec_context_name {
542 write_spec_context(f, name)?;
543 }
544 write!(f, "Request error: {}", details.message)?;
545 if let Some(suggestion) = &details.suggestion {
546 write!(f, " (suggestion: {suggestion})")?;
547 }
548 write_related_spec(f, details)?;
549 write_source_location(f, &details.source)
550 }
551 }
552 }
553}
554
555impl std::error::Error for Error {}
556
557impl From<std::fmt::Error> for Error {
558 fn from(err: std::fmt::Error) -> Self {
559 Error::validation(format!("Format error: {err}"), None, None::<String>)
560 }
561}
562
563impl Error {
564 pub fn kind(&self) -> ErrorKind {
567 match self {
568 Error::Parsing(_) => ErrorKind::Parsing,
569 Error::Validation(_) => ErrorKind::Validation,
570 Error::Registry { .. } => ErrorKind::Registry,
571 Error::MissingRepository { .. } => ErrorKind::MissingRepository,
572 Error::Request { .. } => ErrorKind::Request,
573 Error::ResourceLimitExceeded { .. } => ErrorKind::ResourceLimit,
574 }
575 }
576
577 pub(crate) fn details(&self) -> &ErrorDetails {
579 match self {
580 Error::Parsing(d) | Error::Validation(d) => d,
581 Error::Registry { details, .. }
582 | Error::MissingRepository { details, .. }
583 | Error::ResourceLimitExceeded { details, .. }
584 | Error::Request { details, .. } => details,
585 }
586 }
587
588 #[must_use]
592 pub fn repository(&self) -> Option<&str> {
593 match self {
594 Error::MissingRepository { repository, .. } => Some(repository.as_str()),
595 Error::Registry { identifier, .. } => Some(identifier.as_str()),
596 _ => None,
597 }
598 }
599
600 pub fn message(&self) -> &str {
602 &self.details().message
603 }
604
605 pub fn location(&self) -> Option<&Source> {
607 self.details().source.as_ref()
608 }
609
610 pub fn source_location(&self) -> Option<&Source> {
612 self.location()
613 }
614
615 pub fn source_text(
617 &self,
618 sources: &std::collections::HashMap<crate::parsing::source::SourceType, String>,
619 ) -> Option<String> {
620 self.location()
621 .and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
622 }
623
624 pub fn suggestion(&self) -> Option<&str> {
626 self.details().suggestion.as_deref()
627 }
628
629 pub fn related_data(&self) -> Option<&str> {
631 self.details().related_data.as_deref()
632 }
633
634 pub fn spec_context_name(&self) -> Option<&str> {
636 self.details().spec_context_name.as_deref()
637 }
638
639 pub fn related_spec(&self) -> Option<&str> {
641 self.details().related_spec_name.as_deref()
642 }
643
644 #[must_use]
646 pub fn registry_kind(&self) -> Option<RegistryErrorKind> {
647 match self {
648 Error::Registry { kind, .. } => Some(*kind),
649 _ => None,
650 }
651 }
652
653 #[must_use]
655 pub fn request_kind(&self) -> Option<RequestErrorKind> {
656 match self {
657 Error::Request { kind, .. } => Some(*kind),
658 _ => None,
659 }
660 }
661
662 #[must_use]
664 pub fn limit_name(&self) -> Option<&str> {
665 match self {
666 Error::ResourceLimitExceeded { limit_name, .. } => Some(limit_name.as_str()),
667 _ => None,
668 }
669 }
670
671 #[must_use]
674 pub fn limit_value(&self) -> Option<&str> {
675 match self {
676 Error::ResourceLimitExceeded { limit_value, .. } => Some(limit_value.as_str()),
677 _ => None,
678 }
679 }
680
681 #[must_use]
684 pub fn actual_value(&self) -> Option<&str> {
685 match self {
686 Error::ResourceLimitExceeded { actual_value, .. } => Some(actual_value.as_str()),
687 _ => None,
688 }
689 }
690}
691
692#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
695pub struct EngineErrorSource {
696 pub attribute: String,
697 pub line: usize,
698 pub column: usize,
699 pub length: usize,
700}
701
702impl From<&Source> for EngineErrorSource {
703 fn from(source: &Source) -> Self {
704 Self {
705 attribute: source.source_type.to_string(),
706 line: source.span.line,
707 column: source.span.col,
708 length: source.span.end.saturating_sub(source.span.start),
709 }
710 }
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
716pub struct EngineError {
717 pub kind: ErrorKind,
718 pub message: String,
719 pub related_data: Option<String>,
720 pub spec: Option<String>,
721 pub related_spec: Option<String>,
722 pub source: Option<EngineErrorSource>,
723 pub suggestion: Option<String>,
724 pub repository: Option<String>,
726 pub registry_kind: Option<RegistryErrorKind>,
728 pub request_kind: Option<RequestErrorKind>,
730 pub limit_name: Option<String>,
732 pub limit_value: Option<String>,
733 pub actual_value: Option<String>,
734}
735
736impl From<&Error> for EngineError {
737 fn from(error: &Error) -> Self {
738 Self {
739 kind: error.kind(),
740 message: error.message().to_string(),
741 related_data: error.related_data().map(str::to_string),
742 spec: error.spec_context_name().map(str::to_string),
743 related_spec: error.related_spec().map(str::to_string),
744 source: error.source_location().map(EngineErrorSource::from),
745 suggestion: error.suggestion().map(str::to_string),
746 repository: error.repository().map(str::to_string),
747 registry_kind: error.registry_kind(),
748 request_kind: error.request_kind(),
749 limit_name: error.limit_name().map(str::to_string),
750 limit_value: error.limit_value().map(str::to_string),
751 actual_value: error.actual_value().map(str::to_string),
752 }
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759 use crate::parsing::ast::Span;
760
761 fn test_source() -> Source {
762 Source::new(
763 crate::parsing::source::SourceType::Path(std::sync::Arc::new(
764 std::path::PathBuf::from("test.lemma"),
765 )),
766 Span {
767 start: 14,
768 end: 21,
769 line: 1,
770 col: 15,
771 },
772 )
773 }
774
775 #[test]
776 fn test_error_creation_and_display() {
777 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
778 let parse_error_display = format!("{parse_error}");
779 assert!(parse_error_display.contains("Parse error: Invalid currency"));
780 assert!(parse_error_display.contains("test.lemma:1:15"));
781
782 let suggestion_source = Source::new(
783 crate::parsing::source::SourceType::Volatile,
784 Span {
785 start: 5,
786 end: 10,
787 line: 2,
788 col: 3,
789 },
790 );
791 let suggestion_error =
792 Error::parsing_with_suggestion("typo", suggestion_source, "did you mean X?");
793 assert!(format!("{suggestion_error}").contains("suggestion: did you mean X?"));
794 }
795
796 #[test]
797 fn test_request_error_accessors() {
798 let err = Error::request("bad id", Some("use a valid id"));
799 assert_eq!(err.kind(), ErrorKind::Request);
800 assert_eq!(err.message(), "bad id");
801 assert!(err.location().is_none());
802 assert_eq!(err.suggestion(), Some("use a valid id"));
803 assert!(err.spec_context_name().is_none());
804 assert!(err.related_spec().is_none());
805 }
806
807 #[test]
808 fn test_missing_repository_display() {
809 let err = Error::missing_repository(
810 "not loaded",
811 None,
812 "@iso/countries",
813 Some("load the dependency first"),
814 None,
815 );
816 let display = format!("{err}");
817 assert!(display.contains("Missing repository"));
818 assert!(display.contains("@iso/countries"));
819 assert!(display.contains("not loaded"));
820 }
821
822 #[test]
823 fn test_with_spec_context_copies_name() {
824 let spec = LemmaSpec::new("pricing".to_string());
825 let err = Error::validation("bad", None, None::<String>).with_spec_context(&spec);
826 assert_eq!(err.spec_context_name(), Some("pricing"));
827 let display = format!("{err}");
828 assert!(display.contains("In spec 'pricing':"));
829 }
830
831 #[test]
832 fn test_with_spec_context_keeps_existing_attribution() {
833 let origin = LemmaSpec::new("production".to_string());
834 let consumer = LemmaSpec::new("manufacturing".to_string());
835 let err = Error::validation_with_context("bad", None, None::<String>, Some(&origin), None)
836 .with_spec_context(&consumer);
837 assert_eq!(err.spec_context_name(), Some("production"));
838 }
839}