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 related_spec: Option<Arc<LemmaSpec>>,
15 pub spec_context: 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(Box<ErrorDetails>),
55}
56
57impl Error {
58 pub fn parsing(
60 message: impl Into<String>,
61 source: Source,
62 suggestion: Option<impl Into<String>>,
63 ) -> Self {
64 Self::Parsing(Box::new(ErrorDetails {
65 message: message.into(),
66 source: Some(source),
67 suggestion: suggestion.map(Into::into),
68 related_spec: None,
69 spec_context: None,
70 }))
71 }
72
73 pub fn parsing_with_suggestion(
75 message: impl Into<String>,
76 source: Source,
77 suggestion: impl Into<String>,
78 ) -> Self {
79 Self::parsing(message, source, Some(suggestion))
80 }
81
82 pub fn inversion(
84 message: impl Into<String>,
85 source: Option<Source>,
86 suggestion: Option<impl Into<String>>,
87 ) -> Self {
88 Self::Inversion(Box::new(ErrorDetails {
89 message: message.into(),
90 source,
91 suggestion: suggestion.map(Into::into),
92 related_spec: None,
93 spec_context: None,
94 }))
95 }
96
97 pub fn inversion_with_suggestion(
99 message: impl Into<String>,
100 source: Option<Source>,
101 suggestion: impl Into<String>,
102 ) -> Self {
103 Self::inversion(message, source, Some(suggestion))
104 }
105
106 pub fn validation(
108 message: impl Into<String>,
109 source: Option<Source>,
110 suggestion: Option<impl Into<String>>,
111 ) -> Self {
112 Self::Validation(Box::new(ErrorDetails {
113 message: message.into(),
114 source,
115 suggestion: suggestion.map(Into::into),
116 related_spec: None,
117 spec_context: None,
118 }))
119 }
120
121 pub fn request(message: impl Into<String>, suggestion: Option<impl Into<String>>) -> Self {
124 Self::Request(Box::new(ErrorDetails {
125 message: message.into(),
126 source: None,
127 suggestion: suggestion.map(Into::into),
128 related_spec: None,
129 spec_context: None,
130 }))
131 }
132
133 pub fn resource_limit_exceeded(
135 limit_name: impl Into<String>,
136 limit_value: impl Into<String>,
137 actual_value: impl Into<String>,
138 suggestion: impl Into<String>,
139 source: Option<Source>,
140 ) -> Self {
141 let limit_name = limit_name.into();
142 let limit_value = limit_value.into();
143 let actual_value = actual_value.into();
144 let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
145 Self::ResourceLimitExceeded {
146 details: Box::new(ErrorDetails {
147 message,
148 source,
149 suggestion: Some(suggestion.into()),
150 related_spec: None,
151 spec_context: None,
152 }),
153 limit_name,
154 limit_value,
155 actual_value,
156 }
157 }
158
159 pub fn validation_with_context(
162 message: impl Into<String>,
163 source: Option<Source>,
164 suggestion: Option<impl Into<String>>,
165 related_spec: Option<Arc<LemmaSpec>>,
166 ) -> Self {
167 Self::Validation(Box::new(ErrorDetails {
168 message: message.into(),
169 source,
170 suggestion: suggestion.map(Into::into),
171 related_spec,
172 spec_context: None,
173 }))
174 }
175
176 pub fn registry(
178 message: impl Into<String>,
179 source: Source,
180 identifier: impl Into<String>,
181 kind: RegistryErrorKind,
182 suggestion: Option<impl Into<String>>,
183 ) -> Self {
184 Self::Registry {
185 details: Box::new(ErrorDetails {
186 message: message.into(),
187 source: Some(source),
188 suggestion: suggestion.map(Into::into),
189 related_spec: None,
190 spec_context: None,
191 }),
192 identifier: identifier.into(),
193 kind,
194 }
195 }
196
197 pub fn with_spec_context(self, spec: Arc<LemmaSpec>) -> Self {
199 match self {
200 Error::Parsing(details) => {
201 let mut d = *details;
202 d.spec_context = Some(spec.clone());
203 Error::Parsing(Box::new(d))
204 }
205 Error::Inversion(details) => {
206 let mut d = *details;
207 d.spec_context = Some(spec.clone());
208 Error::Inversion(Box::new(d))
209 }
210 Error::Validation(details) => {
211 let mut d = *details;
212 d.spec_context = Some(spec.clone());
213 Error::Validation(Box::new(d))
214 }
215 Error::Registry {
216 details,
217 identifier,
218 kind,
219 } => {
220 let mut d = *details;
221 d.spec_context = Some(spec.clone());
222 Error::Registry {
223 details: Box::new(d),
224 identifier,
225 kind,
226 }
227 }
228 Error::ResourceLimitExceeded {
229 details,
230 limit_name,
231 limit_value,
232 actual_value,
233 } => {
234 let mut d = *details;
235 d.spec_context = Some(spec.clone());
236 Error::ResourceLimitExceeded {
237 details: Box::new(d),
238 limit_name,
239 limit_value,
240 actual_value,
241 }
242 }
243 Error::Request(details) => {
244 let mut d = *details;
245 d.spec_context = Some(spec);
246 Error::Request(Box::new(d))
247 }
248 }
249 }
250}
251
252fn format_related_spec(spec: &LemmaSpec) -> String {
253 let effective_from_str = spec
254 .effective_from()
255 .map(|d| d.to_string())
256 .unwrap_or_else(|| "beginning".to_string());
257 format!(
258 "See spec '{}' (effective from {}).",
259 spec.name, effective_from_str
260 )
261}
262
263fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
264 if let Some(src) = source {
265 write!(
266 f,
267 " at {}:{}:{}",
268 src.attribute, src.span.line, src.span.col
269 )
270 } else {
271 Ok(())
272 }
273}
274
275fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
276 if let Some(ref related) = details.related_spec {
277 write!(f, " {}", format_related_spec(related))?;
278 }
279 Ok(())
280}
281
282fn write_spec_context(f: &mut fmt::Formatter<'_>, spec: &LemmaSpec) -> fmt::Result {
283 write!(f, "In spec '{}': ", spec.name)
284}
285
286impl fmt::Display for Error {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 match self {
289 Error::Parsing(details) => {
290 if let Some(ref spec) = details.spec_context {
291 write_spec_context(f, spec)?;
292 }
293 write!(f, "Parse error: {}", details.message)?;
294 if let Some(suggestion) = &details.suggestion {
295 write!(f, " (suggestion: {suggestion})")?;
296 }
297 write_related_spec(f, details)?;
298 write_source_location(f, &details.source)
299 }
300 Error::Inversion(details) => {
301 if let Some(ref spec) = details.spec_context {
302 write_spec_context(f, spec)?;
303 }
304 write!(f, "Inversion error: {}", details.message)?;
305 if let Some(suggestion) = &details.suggestion {
306 write!(f, " (suggestion: {suggestion})")?;
307 }
308 write_related_spec(f, details)?;
309 write_source_location(f, &details.source)
310 }
311 Error::Validation(details) => {
312 if let Some(ref spec) = details.spec_context {
313 write_spec_context(f, spec)?;
314 }
315 write!(f, "Validation error: {}", details.message)?;
316 if let Some(suggestion) = &details.suggestion {
317 write!(f, " (suggestion: {suggestion})")?;
318 }
319 write_related_spec(f, details)?;
320 write_source_location(f, &details.source)
321 }
322 Error::Registry {
323 details,
324 identifier,
325 kind,
326 } => {
327 if let Some(ref spec) = details.spec_context {
328 write_spec_context(f, spec)?;
329 }
330 write!(
331 f,
332 "Registry error ({}): {}: {}",
333 kind, identifier, details.message
334 )?;
335 if let Some(suggestion) = &details.suggestion {
336 write!(f, " (suggestion: {suggestion})")?;
337 }
338 write_related_spec(f, details)?;
339 write_source_location(f, &details.source)
340 }
341 Error::ResourceLimitExceeded {
342 details,
343 limit_name,
344 limit_value,
345 actual_value,
346 } => {
347 if let Some(ref spec) = details.spec_context {
348 write_spec_context(f, spec)?;
349 }
350 write!(
351 f,
352 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
353 )?;
354 if let Some(suggestion) = &details.suggestion {
355 write!(f, ". {suggestion}")?;
356 }
357 write_source_location(f, &details.source)
358 }
359 Error::Request(details) => {
360 if let Some(ref spec) = details.spec_context {
361 write_spec_context(f, spec)?;
362 }
363 write!(f, "Request error: {}", details.message)?;
364 if let Some(suggestion) = &details.suggestion {
365 write!(f, " (suggestion: {suggestion})")?;
366 }
367 write_related_spec(f, details)?;
368 write_source_location(f, &details.source)
369 }
370 }
371 }
372}
373
374impl std::error::Error for Error {}
375
376impl From<std::fmt::Error> for Error {
377 fn from(err: std::fmt::Error) -> Self {
378 Error::validation(format!("Format error: {err}"), None, None::<String>)
379 }
380}
381
382impl Error {
383 pub fn message(&self) -> &str {
385 match self {
386 Error::Parsing(details)
387 | Error::Inversion(details)
388 | Error::Validation(details)
389 | Error::Request(details) => &details.message,
390 Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
391 &details.message
392 }
393 }
394 }
395
396 pub fn location(&self) -> Option<&Source> {
398 match self {
399 Error::Parsing(details)
400 | Error::Inversion(details)
401 | Error::Validation(details)
402 | Error::Request(details) => details.source.as_ref(),
403 Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
404 details.source.as_ref()
405 }
406 }
407 }
408
409 pub fn source_text(&self) -> Option<&str> {
411 self.location().map(|s| &*s.source_text)
412 }
413
414 pub fn suggestion(&self) -> Option<&str> {
416 match self {
417 Error::Parsing(details)
418 | Error::Inversion(details)
419 | Error::Validation(details)
420 | Error::Request(details) => details.suggestion.as_deref(),
421 Error::Registry { details, .. } | Error::ResourceLimitExceeded { details, .. } => {
422 details.suggestion.as_deref()
423 }
424 }
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::parsing::ast::Span;
432 use std::sync::Arc;
433
434 fn test_source() -> Source {
435 Source::new(
436 "test.lemma",
437 Span {
438 start: 14,
439 end: 21,
440 line: 1,
441 col: 15,
442 },
443 Arc::from("fact amount: 100"),
444 )
445 }
446
447 #[test]
448 fn test_error_creation_and_display() {
449 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
450 let parse_error_display = format!("{parse_error}");
451 assert!(parse_error_display.contains("Parse error: Invalid currency"));
452 assert!(parse_error_display.contains("test.lemma:1:15"));
453
454 let suggestion_source = Source::new(
455 "suggestion.lemma",
456 Span {
457 start: 5,
458 end: 10,
459 line: 1,
460 col: 6,
461 },
462 Arc::from("fact amont: 100"),
463 );
464
465 let parse_error_with_suggestion = Error::parsing_with_suggestion(
466 "Typo in fact name",
467 suggestion_source,
468 "Did you mean 'amount'?",
469 );
470 let parse_error_with_suggestion_display = format!("{parse_error_with_suggestion}");
471 assert!(parse_error_with_suggestion_display.contains("Typo in fact name"));
472 assert!(parse_error_with_suggestion_display.contains("Did you mean 'amount'?"));
473
474 let engine_error = Error::validation("Something went wrong", None, None::<String>);
475 assert!(format!("{engine_error}").contains("Validation error: Something went wrong"));
476 assert!(!format!("{engine_error}").contains(" at "));
477
478 let validation_error =
479 Error::validation("Circular dependency: a -> b -> a", None, None::<String>);
480 assert!(format!("{validation_error}")
481 .contains("Validation error: Circular dependency: a -> b -> a"));
482 }
483}