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