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, serde::Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ErrorKind {
26 Parsing,
27 Validation,
28 Inversion,
29 Registry,
30 Request,
31 ResourceLimit,
32}
33
34#[derive(Debug, Clone)]
36pub enum Error {
37 Parsing(Box<ErrorDetails>),
39
40 Inversion(Box<ErrorDetails>),
42
43 Validation(Box<ErrorDetails>),
45
46 Registry {
52 details: Box<ErrorDetails>,
53 identifier: String,
55 kind: RegistryErrorKind,
57 },
58
59 ResourceLimitExceeded {
61 details: Box<ErrorDetails>,
62 limit_name: String,
63 limit_value: String,
64 actual_value: String,
65 },
66
67 Request {
70 details: Box<ErrorDetails>,
71 kind: RequestErrorKind,
72 },
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum RequestErrorKind {
78 SpecNotFound,
80 RuleNotFound,
82 InvalidRequest,
84}
85
86impl Error {
87 pub fn parsing(
89 message: impl Into<String>,
90 source: Source,
91 suggestion: Option<impl Into<String>>,
92 ) -> Self {
93 Self::parsing_with_context(message, source, suggestion, None, None)
94 }
95
96 pub fn parsing_with_context(
98 message: impl Into<String>,
99 source: Source,
100 suggestion: Option<impl Into<String>>,
101 spec_context: Option<Arc<LemmaSpec>>,
102 related_spec: Option<Arc<LemmaSpec>>,
103 ) -> Self {
104 Self::Parsing(Box::new(ErrorDetails {
105 message: message.into(),
106 source: Some(source),
107 suggestion: suggestion.map(Into::into),
108 spec_context,
109 related_spec,
110 related_data: None,
111 }))
112 }
113
114 pub fn parsing_with_suggestion(
116 message: impl Into<String>,
117 source: Source,
118 suggestion: impl Into<String>,
119 ) -> Self {
120 Self::parsing_with_context(message, source, Some(suggestion), None, None)
121 }
122
123 pub fn inversion(
125 message: impl Into<String>,
126 source: Option<Source>,
127 suggestion: Option<impl Into<String>>,
128 ) -> Self {
129 Self::inversion_with_context(message, source, suggestion, None, None)
130 }
131
132 pub fn inversion_with_context(
134 message: impl Into<String>,
135 source: Option<Source>,
136 suggestion: Option<impl Into<String>>,
137 spec_context: Option<Arc<LemmaSpec>>,
138 related_spec: Option<Arc<LemmaSpec>>,
139 ) -> Self {
140 Self::Inversion(Box::new(ErrorDetails {
141 message: message.into(),
142 source,
143 suggestion: suggestion.map(Into::into),
144 spec_context,
145 related_spec,
146 related_data: None,
147 }))
148 }
149
150 pub fn inversion_with_suggestion(
152 message: impl Into<String>,
153 source: Option<Source>,
154 suggestion: impl Into<String>,
155 spec_context: Option<Arc<LemmaSpec>>,
156 related_spec: Option<Arc<LemmaSpec>>,
157 ) -> Self {
158 Self::inversion_with_context(
159 message,
160 source,
161 Some(suggestion),
162 spec_context,
163 related_spec,
164 )
165 }
166
167 pub fn validation(
169 message: impl Into<String>,
170 source: Option<Source>,
171 suggestion: Option<impl Into<String>>,
172 ) -> Self {
173 Self::validation_with_context(message, source, suggestion, None, None)
174 }
175
176 pub fn validation_with_context(
178 message: impl Into<String>,
179 source: Option<Source>,
180 suggestion: Option<impl Into<String>>,
181 spec_context: Option<Arc<LemmaSpec>>,
182 related_spec: Option<Arc<LemmaSpec>>,
183 ) -> Self {
184 Self::Validation(Box::new(ErrorDetails {
185 message: message.into(),
186 source,
187 suggestion: suggestion.map(Into::into),
188 spec_context,
189 related_spec,
190 related_data: None,
191 }))
192 }
193
194 pub fn request(message: impl Into<String>, suggestion: Option<impl Into<String>>) -> Self {
197 Self::request_with_kind(message, suggestion, RequestErrorKind::InvalidRequest)
198 }
199
200 pub fn request_not_found(
202 message: impl Into<String>,
203 suggestion: Option<impl Into<String>>,
204 ) -> Self {
205 Self::request_with_kind(message, suggestion, RequestErrorKind::SpecNotFound)
206 }
207
208 pub fn rule_not_found(rule_name: &str, suggestion: Option<impl Into<String>>) -> Self {
210 Self::request_with_kind(
211 format!("Rule '{}' not found", rule_name),
212 suggestion,
213 RequestErrorKind::RuleNotFound,
214 )
215 }
216
217 fn request_with_kind(
218 message: impl Into<String>,
219 suggestion: Option<impl Into<String>>,
220 kind: RequestErrorKind,
221 ) -> Self {
222 Self::Request {
223 details: Box::new(ErrorDetails {
224 message: message.into(),
225 source: None,
226 suggestion: suggestion.map(Into::into),
227 spec_context: None,
228 related_spec: None,
229 related_data: None,
230 }),
231 kind,
232 }
233 }
234
235 pub fn resource_limit_exceeded(
237 limit_name: impl Into<String>,
238 limit_value: impl Into<String>,
239 actual_value: impl Into<String>,
240 suggestion: impl Into<String>,
241 source: Option<Source>,
242 spec_context: Option<Arc<LemmaSpec>>,
243 related_spec: Option<Arc<LemmaSpec>>,
244 ) -> Self {
245 let limit_name = limit_name.into();
246 let limit_value = limit_value.into();
247 let actual_value = actual_value.into();
248 let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
249 Self::ResourceLimitExceeded {
250 details: Box::new(ErrorDetails {
251 message,
252 source,
253 suggestion: Some(suggestion.into()),
254 spec_context,
255 related_spec,
256 related_data: None,
257 }),
258 limit_name,
259 limit_value,
260 actual_value,
261 }
262 }
263
264 pub fn registry(
266 message: impl Into<String>,
267 source: Source,
268 identifier: impl Into<String>,
269 kind: RegistryErrorKind,
270 suggestion: Option<impl Into<String>>,
271 spec_context: Option<Arc<LemmaSpec>>,
272 related_spec: Option<Arc<LemmaSpec>>,
273 ) -> Self {
274 Self::Registry {
275 details: Box::new(ErrorDetails {
276 message: message.into(),
277 source: Some(source),
278 suggestion: suggestion.map(Into::into),
279 spec_context,
280 related_spec,
281 related_data: None,
282 }),
283 identifier: identifier.into(),
284 kind,
285 }
286 }
287
288 pub fn with_spec_context(self, spec: Arc<LemmaSpec>) -> Self {
290 self.map_details(|d| d.spec_context = Some(spec))
291 }
292
293 pub fn with_related_data(self, name: impl Into<String>) -> Self {
297 let name = name.into();
298 self.map_details(|d| d.related_data = Some(name))
299 }
300
301 fn map_details(self, f: impl FnOnce(&mut ErrorDetails)) -> Self {
303 match self {
304 Error::Parsing(details) => {
305 let mut d = *details;
306 f(&mut d);
307 Error::Parsing(Box::new(d))
308 }
309 Error::Inversion(details) => {
310 let mut d = *details;
311 f(&mut d);
312 Error::Inversion(Box::new(d))
313 }
314 Error::Validation(details) => {
315 let mut d = *details;
316 f(&mut d);
317 Error::Validation(Box::new(d))
318 }
319 Error::Registry {
320 details,
321 identifier,
322 kind,
323 } => {
324 let mut d = *details;
325 f(&mut d);
326 Error::Registry {
327 details: Box::new(d),
328 identifier,
329 kind,
330 }
331 }
332 Error::ResourceLimitExceeded {
333 details,
334 limit_name,
335 limit_value,
336 actual_value,
337 } => {
338 let mut d = *details;
339 f(&mut d);
340 Error::ResourceLimitExceeded {
341 details: Box::new(d),
342 limit_name,
343 limit_value,
344 actual_value,
345 }
346 }
347 Error::Request { details, kind } => {
348 let mut d = *details;
349 f(&mut d);
350 Error::Request {
351 details: Box::new(d),
352 kind,
353 }
354 }
355 }
356 }
357}
358
359fn format_related_spec(spec: &LemmaSpec) -> String {
360 let effective_from_str = spec
361 .effective_from()
362 .map(|d| d.to_string())
363 .unwrap_or_else(|| "beginning".to_string());
364 format!(
365 "See spec '{}' (effective from {}).",
366 spec.name, effective_from_str
367 )
368}
369
370fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
371 if let Some(src) = source {
372 write!(
373 f,
374 " at {}:{}:{}",
375 src.attribute, src.span.line, src.span.col
376 )
377 } else {
378 Ok(())
379 }
380}
381
382fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
383 if let Some(ref related) = details.related_spec {
384 write!(f, " {}", format_related_spec(related))?;
385 }
386 Ok(())
387}
388
389fn write_spec_context(f: &mut fmt::Formatter<'_>, spec: &LemmaSpec) -> fmt::Result {
390 write!(f, "In spec '{}': ", spec.name)
391}
392
393impl fmt::Display for Error {
394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395 match self {
396 Error::Parsing(details) => {
397 if let Some(ref spec) = details.spec_context {
398 write_spec_context(f, spec)?;
399 }
400 write!(f, "Parse error: {}", details.message)?;
401 if let Some(suggestion) = &details.suggestion {
402 write!(f, " (suggestion: {suggestion})")?;
403 }
404 write_related_spec(f, details)?;
405 write_source_location(f, &details.source)
406 }
407 Error::Inversion(details) => {
408 if let Some(ref spec) = details.spec_context {
409 write_spec_context(f, spec)?;
410 }
411 write!(f, "Inversion error: {}", details.message)?;
412 if let Some(suggestion) = &details.suggestion {
413 write!(f, " (suggestion: {suggestion})")?;
414 }
415 write_related_spec(f, details)?;
416 write_source_location(f, &details.source)
417 }
418 Error::Validation(details) => {
419 if let Some(ref spec) = details.spec_context {
420 write_spec_context(f, spec)?;
421 }
422 write!(f, "Validation error: ")?;
423 if let Some(ref name) = details.related_data {
424 write!(f, "Failed to parse data '{}': ", name)?;
425 }
426 write!(f, "{}", details.message)?;
427 if let Some(suggestion) = &details.suggestion {
428 write!(f, " (suggestion: {suggestion})")?;
429 }
430 write_related_spec(f, details)?;
431 write_source_location(f, &details.source)
432 }
433 Error::Registry {
434 details,
435 identifier,
436 kind,
437 } => {
438 if let Some(ref spec) = details.spec_context {
439 write_spec_context(f, spec)?;
440 }
441 write!(
442 f,
443 "Registry error ({}): {}: {}",
444 kind, identifier, details.message
445 )?;
446 if let Some(suggestion) = &details.suggestion {
447 write!(f, " (suggestion: {suggestion})")?;
448 }
449 write_related_spec(f, details)?;
450 write_source_location(f, &details.source)
451 }
452 Error::ResourceLimitExceeded {
453 details,
454 limit_name,
455 limit_value,
456 actual_value,
457 } => {
458 if let Some(ref spec) = details.spec_context {
459 write_spec_context(f, spec)?;
460 }
461 write!(
462 f,
463 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
464 )?;
465 if let Some(suggestion) = &details.suggestion {
466 write!(f, ". {suggestion}")?;
467 }
468 write_source_location(f, &details.source)
469 }
470 Error::Request { details, .. } => {
471 if let Some(ref spec) = details.spec_context {
472 write_spec_context(f, spec)?;
473 }
474 write!(f, "Request error: {}", details.message)?;
475 if let Some(suggestion) = &details.suggestion {
476 write!(f, " (suggestion: {suggestion})")?;
477 }
478 write_related_spec(f, details)?;
479 write_source_location(f, &details.source)
480 }
481 }
482 }
483}
484
485impl std::error::Error for Error {}
486
487impl From<std::fmt::Error> for Error {
488 fn from(err: std::fmt::Error) -> Self {
489 Error::validation(format!("Format error: {err}"), None, None::<String>)
490 }
491}
492
493impl Error {
494 pub fn kind(&self) -> ErrorKind {
497 match self {
498 Error::Parsing(_) => ErrorKind::Parsing,
499 Error::Validation(_) => ErrorKind::Validation,
500 Error::Inversion(_) => ErrorKind::Inversion,
501 Error::Registry { .. } => ErrorKind::Registry,
502 Error::Request { .. } => ErrorKind::Request,
503 Error::ResourceLimitExceeded { .. } => ErrorKind::ResourceLimit,
504 }
505 }
506
507 fn details(&self) -> &ErrorDetails {
509 match self {
510 Error::Parsing(d) | Error::Inversion(d) | Error::Validation(d) => d,
511 Error::Registry { details, .. }
512 | Error::ResourceLimitExceeded { details, .. }
513 | Error::Request { details, .. } => details,
514 }
515 }
516
517 pub fn message(&self) -> &str {
519 &self.details().message
520 }
521
522 pub fn location(&self) -> Option<&Source> {
524 self.details().source.as_ref()
525 }
526
527 pub fn source_location(&self) -> Option<&Source> {
529 self.location()
530 }
531
532 pub fn source_text(
534 &self,
535 sources: &std::collections::HashMap<String, String>,
536 ) -> Option<String> {
537 self.location()
538 .and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
539 }
540
541 pub fn suggestion(&self) -> Option<&str> {
543 self.details().suggestion.as_deref()
544 }
545
546 pub fn related_data(&self) -> Option<&str> {
548 self.details().related_data.as_deref()
549 }
550
551 pub fn spec(&self) -> Option<&str> {
553 self.details()
554 .spec_context
555 .as_ref()
556 .map(|s| s.name.as_str())
557 }
558
559 pub fn related_spec(&self) -> Option<&str> {
561 self.details()
562 .related_spec
563 .as_ref()
564 .map(|s| s.name.as_str())
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use crate::parsing::ast::Span;
572
573 fn test_source() -> Source {
574 Source::new(
575 "test.lemma",
576 Span {
577 start: 14,
578 end: 21,
579 line: 1,
580 col: 15,
581 },
582 )
583 }
584
585 #[test]
586 fn test_error_creation_and_display() {
587 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
588 let parse_error_display = format!("{parse_error}");
589 assert!(parse_error_display.contains("Parse error: Invalid currency"));
590 assert!(parse_error_display.contains("test.lemma:1:15"));
591
592 let suggestion_source = Source::new(
593 "suggestion.lemma",
594 Span {
595 start: 5,
596 end: 10,
597 line: 1,
598 col: 6,
599 },
600 );
601
602 let parse_error_with_suggestion = Error::parsing_with_suggestion(
603 "Typo in data name",
604 suggestion_source,
605 "Did you mean 'amount'?",
606 );
607 let parse_error_with_suggestion_display = format!("{parse_error_with_suggestion}");
608 assert!(parse_error_with_suggestion_display.contains("Typo in data name"));
609 assert!(parse_error_with_suggestion_display.contains("Did you mean 'amount'?"));
610
611 let engine_error = Error::validation("Something went wrong", None, None::<String>);
612 assert!(format!("{engine_error}").contains("Validation error: Something went wrong"));
613 assert!(!format!("{engine_error}").contains(" at "));
614
615 let validation_error =
616 Error::validation("Circular dependency: a -> b -> a", None, None::<String>);
617 assert!(format!("{validation_error}")
618 .contains("Validation error: Circular dependency: a -> b -> a"));
619 }
620
621 #[test]
622 fn test_error_kind_accessor() {
623 assert_eq!(
624 Error::parsing("x", test_source(), None::<String>).kind(),
625 ErrorKind::Parsing
626 );
627 assert_eq!(
628 Error::validation("x", None, None::<String>).kind(),
629 ErrorKind::Validation
630 );
631 assert_eq!(
632 Error::inversion("x", None, None::<String>).kind(),
633 ErrorKind::Inversion
634 );
635 assert_eq!(
636 Error::request("x", None::<String>).kind(),
637 ErrorKind::Request
638 );
639 assert_eq!(
640 Error::resource_limit_exceeded("cap", "1", "2", "try less", None, None, None).kind(),
641 ErrorKind::ResourceLimit
642 );
643 }
644
645 #[test]
646 fn test_related_data_attribution_and_display() {
647 let err = Error::validation(
648 "Unknown unit 'mete' for this scale type",
649 Some(test_source()),
650 None::<String>,
651 )
652 .with_related_data("bridge_height");
653
654 assert_eq!(err.related_data(), Some("bridge_height"));
655 assert_eq!(err.kind(), ErrorKind::Validation);
656 assert_eq!(err.message(), "Unknown unit 'mete' for this scale type");
657
658 let display = format!("{err}");
659 assert!(
660 display.contains(
661 "Validation error: Failed to parse data 'bridge_height': Unknown unit 'mete'"
662 ),
663 "unexpected display: {display}"
664 );
665
666 let at_occurrences = display.matches(" at ").count();
667 assert_eq!(
668 at_occurrences, 1,
669 "expected exactly one ` at ` in display, got {at_occurrences}: {display}"
670 );
671 }
672
673 #[test]
674 fn test_related_data_none_by_default() {
675 let err = Error::validation("x", None, None::<String>);
676 assert!(err.related_data().is_none());
677 assert!(err.spec().is_none());
678 assert!(err.related_spec().is_none());
679 }
680
681 #[test]
682 fn test_related_data_builder_preserves_other_variants() {
683 let err = Error::resource_limit_exceeded(
684 "max_data_value_bytes",
685 "100",
686 "200",
687 "reduce size",
688 Some(test_source()),
689 None,
690 None,
691 )
692 .with_related_data("big_blob");
693
694 assert_eq!(err.kind(), ErrorKind::ResourceLimit);
695 assert_eq!(err.related_data(), Some("big_blob"));
696 }
697}