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}
18
19#[derive(Debug, Clone)]
21pub enum Error {
22 Parsing(Box<ErrorDetails>),
24
25 Inversion(Box<ErrorDetails>),
27
28 Validation(Box<ErrorDetails>),
30
31 Registry {
37 details: Box<ErrorDetails>,
38 identifier: String,
40 kind: RegistryErrorKind,
42 },
43
44 ResourceLimitExceeded {
46 details: Box<ErrorDetails>,
47 limit_name: String,
48 limit_value: String,
49 actual_value: String,
50 },
51
52 Request {
55 details: Box<ErrorDetails>,
56 kind: RequestErrorKind,
57 },
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum RequestErrorKind {
63 SpecNotFound,
65 InvalidRequest,
67}
68
69impl Error {
70 pub fn parsing(
72 message: impl Into<String>,
73 source: Source,
74 suggestion: Option<impl Into<String>>,
75 ) -> Self {
76 Self::parsing_with_context(message, source, suggestion, None, None)
77 }
78
79 pub fn parsing_with_context(
81 message: impl Into<String>,
82 source: Source,
83 suggestion: Option<impl Into<String>>,
84 spec_context: Option<Arc<LemmaSpec>>,
85 related_spec: Option<Arc<LemmaSpec>>,
86 ) -> Self {
87 Self::Parsing(Box::new(ErrorDetails {
88 message: message.into(),
89 source: Some(source),
90 suggestion: suggestion.map(Into::into),
91 spec_context,
92 related_spec,
93 }))
94 }
95
96 pub fn parsing_with_suggestion(
98 message: impl Into<String>,
99 source: Source,
100 suggestion: impl Into<String>,
101 ) -> Self {
102 Self::parsing_with_context(message, source, Some(suggestion), None, None)
103 }
104
105 pub fn inversion(
107 message: impl Into<String>,
108 source: Option<Source>,
109 suggestion: Option<impl Into<String>>,
110 ) -> Self {
111 Self::inversion_with_context(message, source, suggestion, None, None)
112 }
113
114 pub fn inversion_with_context(
116 message: impl Into<String>,
117 source: Option<Source>,
118 suggestion: Option<impl Into<String>>,
119 spec_context: Option<Arc<LemmaSpec>>,
120 related_spec: Option<Arc<LemmaSpec>>,
121 ) -> Self {
122 Self::Inversion(Box::new(ErrorDetails {
123 message: message.into(),
124 source,
125 suggestion: suggestion.map(Into::into),
126 spec_context,
127 related_spec,
128 }))
129 }
130
131 pub fn inversion_with_suggestion(
133 message: impl Into<String>,
134 source: Option<Source>,
135 suggestion: impl Into<String>,
136 spec_context: Option<Arc<LemmaSpec>>,
137 related_spec: Option<Arc<LemmaSpec>>,
138 ) -> Self {
139 Self::inversion_with_context(
140 message,
141 source,
142 Some(suggestion),
143 spec_context,
144 related_spec,
145 )
146 }
147
148 pub fn validation(
150 message: impl Into<String>,
151 source: Option<Source>,
152 suggestion: Option<impl Into<String>>,
153 ) -> Self {
154 Self::validation_with_context(message, source, suggestion, None, None)
155 }
156
157 pub fn validation_with_context(
159 message: impl Into<String>,
160 source: Option<Source>,
161 suggestion: Option<impl Into<String>>,
162 spec_context: Option<Arc<LemmaSpec>>,
163 related_spec: Option<Arc<LemmaSpec>>,
164 ) -> Self {
165 Self::Validation(Box::new(ErrorDetails {
166 message: message.into(),
167 source,
168 suggestion: suggestion.map(Into::into),
169 spec_context,
170 related_spec,
171 }))
172 }
173
174 pub fn request(message: impl Into<String>, suggestion: Option<impl Into<String>>) -> Self {
177 Self::request_with_kind(message, suggestion, RequestErrorKind::InvalidRequest)
178 }
179
180 pub fn request_not_found(
182 message: impl Into<String>,
183 suggestion: Option<impl Into<String>>,
184 ) -> Self {
185 Self::request_with_kind(message, suggestion, RequestErrorKind::SpecNotFound)
186 }
187
188 fn request_with_kind(
189 message: impl Into<String>,
190 suggestion: Option<impl Into<String>>,
191 kind: RequestErrorKind,
192 ) -> Self {
193 Self::Request {
194 details: Box::new(ErrorDetails {
195 message: message.into(),
196 source: None,
197 suggestion: suggestion.map(Into::into),
198 spec_context: None,
199 related_spec: None,
200 }),
201 kind,
202 }
203 }
204
205 pub fn resource_limit_exceeded(
207 limit_name: impl Into<String>,
208 limit_value: impl Into<String>,
209 actual_value: impl Into<String>,
210 suggestion: impl Into<String>,
211 source: Option<Source>,
212 spec_context: Option<Arc<LemmaSpec>>,
213 related_spec: Option<Arc<LemmaSpec>>,
214 ) -> Self {
215 let limit_name = limit_name.into();
216 let limit_value = limit_value.into();
217 let actual_value = actual_value.into();
218 let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
219 Self::ResourceLimitExceeded {
220 details: Box::new(ErrorDetails {
221 message,
222 source,
223 suggestion: Some(suggestion.into()),
224 spec_context,
225 related_spec,
226 }),
227 limit_name,
228 limit_value,
229 actual_value,
230 }
231 }
232
233 pub fn registry(
235 message: impl Into<String>,
236 source: Source,
237 identifier: impl Into<String>,
238 kind: RegistryErrorKind,
239 suggestion: Option<impl Into<String>>,
240 spec_context: Option<Arc<LemmaSpec>>,
241 related_spec: Option<Arc<LemmaSpec>>,
242 ) -> Self {
243 Self::Registry {
244 details: Box::new(ErrorDetails {
245 message: message.into(),
246 source: Some(source),
247 suggestion: suggestion.map(Into::into),
248 spec_context,
249 related_spec,
250 }),
251 identifier: identifier.into(),
252 kind,
253 }
254 }
255
256 pub fn with_spec_context(self, spec: Arc<LemmaSpec>) -> Self {
258 match self {
259 Error::Parsing(details) => {
260 let mut d = *details;
261 d.spec_context = Some(spec.clone());
262 Error::Parsing(Box::new(d))
263 }
264 Error::Inversion(details) => {
265 let mut d = *details;
266 d.spec_context = Some(spec.clone());
267 Error::Inversion(Box::new(d))
268 }
269 Error::Validation(details) => {
270 let mut d = *details;
271 d.spec_context = Some(spec.clone());
272 Error::Validation(Box::new(d))
273 }
274 Error::Registry {
275 details,
276 identifier,
277 kind,
278 } => {
279 let mut d = *details;
280 d.spec_context = Some(spec.clone());
281 Error::Registry {
282 details: Box::new(d),
283 identifier,
284 kind,
285 }
286 }
287 Error::ResourceLimitExceeded {
288 details,
289 limit_name,
290 limit_value,
291 actual_value,
292 } => {
293 let mut d = *details;
294 d.spec_context = Some(spec.clone());
295 Error::ResourceLimitExceeded {
296 details: Box::new(d),
297 limit_name,
298 limit_value,
299 actual_value,
300 }
301 }
302 Error::Request { details, kind } => {
303 let mut d = *details;
304 d.spec_context = Some(spec);
305 Error::Request {
306 details: Box::new(d),
307 kind,
308 }
309 }
310 }
311 }
312}
313
314fn format_related_spec(spec: &LemmaSpec) -> String {
315 let effective_from_str = spec
316 .effective_from()
317 .map(|d| d.to_string())
318 .unwrap_or_else(|| "beginning".to_string());
319 format!(
320 "See spec '{}' (effective from {}).",
321 spec.name, effective_from_str
322 )
323}
324
325fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
326 if let Some(src) = source {
327 write!(
328 f,
329 " at {}:{}:{}",
330 src.attribute, src.span.line, src.span.col
331 )
332 } else {
333 Ok(())
334 }
335}
336
337fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
338 if let Some(ref related) = details.related_spec {
339 write!(f, " {}", format_related_spec(related))?;
340 }
341 Ok(())
342}
343
344fn write_spec_context(f: &mut fmt::Formatter<'_>, spec: &LemmaSpec) -> fmt::Result {
345 write!(f, "In spec '{}': ", spec.name)
346}
347
348impl fmt::Display for Error {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 match self {
351 Error::Parsing(details) => {
352 if let Some(ref spec) = details.spec_context {
353 write_spec_context(f, spec)?;
354 }
355 write!(f, "Parse error: {}", details.message)?;
356 if let Some(suggestion) = &details.suggestion {
357 write!(f, " (suggestion: {suggestion})")?;
358 }
359 write_related_spec(f, details)?;
360 write_source_location(f, &details.source)
361 }
362 Error::Inversion(details) => {
363 if let Some(ref spec) = details.spec_context {
364 write_spec_context(f, spec)?;
365 }
366 write!(f, "Inversion error: {}", details.message)?;
367 if let Some(suggestion) = &details.suggestion {
368 write!(f, " (suggestion: {suggestion})")?;
369 }
370 write_related_spec(f, details)?;
371 write_source_location(f, &details.source)
372 }
373 Error::Validation(details) => {
374 if let Some(ref spec) = details.spec_context {
375 write_spec_context(f, spec)?;
376 }
377 write!(f, "Validation error: {}", details.message)?;
378 if let Some(suggestion) = &details.suggestion {
379 write!(f, " (suggestion: {suggestion})")?;
380 }
381 write_related_spec(f, details)?;
382 write_source_location(f, &details.source)
383 }
384 Error::Registry {
385 details,
386 identifier,
387 kind,
388 } => {
389 if let Some(ref spec) = details.spec_context {
390 write_spec_context(f, spec)?;
391 }
392 write!(
393 f,
394 "Registry error ({}): {}: {}",
395 kind, identifier, details.message
396 )?;
397 if let Some(suggestion) = &details.suggestion {
398 write!(f, " (suggestion: {suggestion})")?;
399 }
400 write_related_spec(f, details)?;
401 write_source_location(f, &details.source)
402 }
403 Error::ResourceLimitExceeded {
404 details,
405 limit_name,
406 limit_value,
407 actual_value,
408 } => {
409 if let Some(ref spec) = details.spec_context {
410 write_spec_context(f, spec)?;
411 }
412 write!(
413 f,
414 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
415 )?;
416 if let Some(suggestion) = &details.suggestion {
417 write!(f, ". {suggestion}")?;
418 }
419 write_source_location(f, &details.source)
420 }
421 Error::Request { details, .. } => {
422 if let Some(ref spec) = details.spec_context {
423 write_spec_context(f, spec)?;
424 }
425 write!(f, "Request error: {}", details.message)?;
426 if let Some(suggestion) = &details.suggestion {
427 write!(f, " (suggestion: {suggestion})")?;
428 }
429 write_related_spec(f, details)?;
430 write_source_location(f, &details.source)
431 }
432 }
433 }
434}
435
436impl std::error::Error for Error {}
437
438impl From<std::fmt::Error> for Error {
439 fn from(err: std::fmt::Error) -> Self {
440 Error::validation(format!("Format error: {err}"), None, None::<String>)
441 }
442}
443
444impl Error {
445 pub fn message(&self) -> &str {
447 match self {
448 Error::Parsing(details)
449 | Error::Inversion(details)
450 | Error::Validation(details)
451 | Error::Request { details, .. } => &details.message,
452 Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
453 &details.message
454 }
455 }
456 }
457
458 pub fn location(&self) -> Option<&Source> {
460 match self {
461 Error::Parsing(details)
462 | Error::Inversion(details)
463 | Error::Validation(details)
464 | Error::Request { details, .. } => details.source.as_ref(),
465 Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
466 details.source.as_ref()
467 }
468 }
469 }
470
471 pub fn source_text(
473 &self,
474 sources: &std::collections::HashMap<String, String>,
475 ) -> Option<String> {
476 self.location()
477 .and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
478 }
479
480 pub fn suggestion(&self) -> Option<&str> {
482 match self {
483 Error::Parsing(details)
484 | Error::Inversion(details)
485 | Error::Validation(details)
486 | Error::Request { details, .. } => details.suggestion.as_deref(),
487 Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
488 details.suggestion.as_deref()
489 }
490 }
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::parsing::ast::Span;
498
499 fn test_source() -> Source {
500 Source::new(
501 "test.lemma",
502 Span {
503 start: 14,
504 end: 21,
505 line: 1,
506 col: 15,
507 },
508 )
509 }
510
511 #[test]
512 fn test_error_creation_and_display() {
513 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
514 let parse_error_display = format!("{parse_error}");
515 assert!(parse_error_display.contains("Parse error: Invalid currency"));
516 assert!(parse_error_display.contains("test.lemma:1:15"));
517
518 let suggestion_source = Source::new(
519 "suggestion.lemma",
520 Span {
521 start: 5,
522 end: 10,
523 line: 1,
524 col: 6,
525 },
526 );
527
528 let parse_error_with_suggestion = Error::parsing_with_suggestion(
529 "Typo in fact name",
530 suggestion_source,
531 "Did you mean 'amount'?",
532 );
533 let parse_error_with_suggestion_display = format!("{parse_error_with_suggestion}");
534 assert!(parse_error_with_suggestion_display.contains("Typo in fact name"));
535 assert!(parse_error_with_suggestion_display.contains("Did you mean 'amount'?"));
536
537 let engine_error = Error::validation("Something went wrong", None, None::<String>);
538 assert!(format!("{engine_error}").contains("Validation error: Something went wrong"));
539 assert!(!format!("{engine_error}").contains(" at "));
540
541 let validation_error =
542 Error::validation("Circular dependency: a -> b -> a", None, None::<String>);
543 assert!(format!("{validation_error}")
544 .contains("Validation error: Circular dependency: a -> b -> a"));
545 }
546}