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 Inversion,
68 Registry,
69 MissingRepository,
70 Request,
71 ResourceLimit,
72}
73
74#[derive(Debug, Clone)]
76pub enum Error {
77 Parsing(Box<ErrorDetails>),
79
80 Inversion(Box<ErrorDetails>),
82
83 Validation(Box<ErrorDetails>),
85
86 Registry {
92 details: Box<ErrorDetails>,
93 identifier: String,
95 kind: RegistryErrorKind,
97 },
98
99 MissingRepository {
104 details: Box<ErrorDetails>,
105 repository: String,
107 },
108
109 ResourceLimitExceeded {
111 details: Box<ErrorDetails>,
112 limit_name: String,
113 limit_value: String,
114 actual_value: String,
115 },
116
117 Request {
120 details: Box<ErrorDetails>,
121 kind: RequestErrorKind,
122 },
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
127#[serde(rename_all = "snake_case")]
128pub enum RequestErrorKind {
129 SpecNotFound,
131 RuleNotFound,
133 InvalidRequest,
135}
136
137impl Error {
138 pub fn parsing(
140 message: impl Into<String>,
141 source: Source,
142 suggestion: Option<impl Into<String>>,
143 ) -> Self {
144 Self::parsing_with_context(message, source, suggestion, None, None)
145 }
146
147 pub fn parsing_with_context(
149 message: impl Into<String>,
150 source: Source,
151 suggestion: Option<impl Into<String>>,
152 spec_context: Option<&LemmaSpec>,
153 related_spec: Option<&LemmaSpec>,
154 ) -> Self {
155 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
156 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
157 Self::Parsing(Box::new(ErrorDetails {
158 message: message.into(),
159 source: Some(source),
160 suggestion: suggestion.map(Into::into),
161 spec_context_name,
162 spec_context_effective_from,
163 related_spec_name,
164 related_spec_effective_from,
165 related_data: None,
166 }))
167 }
168
169 pub fn parsing_with_suggestion(
171 message: impl Into<String>,
172 source: Source,
173 suggestion: impl Into<String>,
174 ) -> Self {
175 Self::parsing_with_context(message, source, Some(suggestion), None, None)
176 }
177
178 pub fn inversion(
180 message: impl Into<String>,
181 source: Option<Source>,
182 suggestion: Option<impl Into<String>>,
183 ) -> Self {
184 Self::inversion_with_context(message, source, suggestion, None, None)
185 }
186
187 pub fn inversion_with_context(
189 message: impl Into<String>,
190 source: Option<Source>,
191 suggestion: Option<impl Into<String>>,
192 spec_context: Option<&LemmaSpec>,
193 related_spec: Option<&LemmaSpec>,
194 ) -> Self {
195 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
196 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
197 Self::Inversion(Box::new(ErrorDetails {
198 message: message.into(),
199 source,
200 suggestion: suggestion.map(Into::into),
201 spec_context_name,
202 spec_context_effective_from,
203 related_spec_name,
204 related_spec_effective_from,
205 related_data: None,
206 }))
207 }
208
209 pub fn inversion_with_suggestion(
211 message: impl Into<String>,
212 source: Option<Source>,
213 suggestion: impl Into<String>,
214 spec_context: Option<&LemmaSpec>,
215 related_spec: Option<&LemmaSpec>,
216 ) -> Self {
217 Self::inversion_with_context(
218 message,
219 source,
220 Some(suggestion),
221 spec_context,
222 related_spec,
223 )
224 }
225
226 pub fn validation(
228 message: impl Into<String>,
229 source: Option<Source>,
230 suggestion: Option<impl Into<String>>,
231 ) -> Self {
232 Self::validation_with_context(message, source, suggestion, None, None)
233 }
234
235 pub fn validation_with_context(
237 message: impl Into<String>,
238 source: Option<Source>,
239 suggestion: Option<impl Into<String>>,
240 spec_context: Option<&LemmaSpec>,
241 related_spec: Option<&LemmaSpec>,
242 ) -> Self {
243 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
244 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
245 Self::Validation(Box::new(ErrorDetails {
246 message: message.into(),
247 source,
248 suggestion: suggestion.map(Into::into),
249 spec_context_name,
250 spec_context_effective_from,
251 related_spec_name,
252 related_spec_effective_from,
253 related_data: None,
254 }))
255 }
256
257 pub fn request(message: impl Into<String>, suggestion: Option<impl Into<String>>) -> Self {
260 Self::request_with_kind(message, suggestion, RequestErrorKind::InvalidRequest)
261 }
262
263 pub fn request_not_found(
265 message: impl Into<String>,
266 suggestion: Option<impl Into<String>>,
267 ) -> Self {
268 Self::request_with_kind(message, suggestion, RequestErrorKind::SpecNotFound)
269 }
270
271 pub fn rule_not_found(rule_name: &str, suggestion: Option<impl Into<String>>) -> Self {
273 Self::request_with_kind(
274 format!("Rule '{}' not found", rule_name),
275 suggestion,
276 RequestErrorKind::RuleNotFound,
277 )
278 }
279
280 fn request_with_kind(
281 message: impl Into<String>,
282 suggestion: Option<impl Into<String>>,
283 kind: RequestErrorKind,
284 ) -> Self {
285 Self::Request {
286 details: Box::new(ErrorDetails {
287 message: message.into(),
288 source: None,
289 suggestion: suggestion.map(Into::into),
290 spec_context_name: None,
291 spec_context_effective_from: None,
292 related_spec_name: None,
293 related_spec_effective_from: None,
294 related_data: None,
295 }),
296 kind,
297 }
298 }
299
300 pub fn resource_limit_exceeded(
302 limit_name: impl Into<String>,
303 limit_value: impl Into<String>,
304 actual_value: impl Into<String>,
305 suggestion: impl Into<String>,
306 source: Option<Source>,
307 spec_context: Option<&LemmaSpec>,
308 related_spec: Option<&LemmaSpec>,
309 ) -> Self {
310 let limit_name = limit_name.into();
311 let limit_value = limit_value.into();
312 let actual_value = actual_value.into();
313 let message = format!("{limit_name} (limit: {limit_value}, actual: {actual_value})");
314 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
315 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
316 Self::ResourceLimitExceeded {
317 details: Box::new(ErrorDetails {
318 message,
319 source,
320 suggestion: Some(suggestion.into()),
321 spec_context_name,
322 spec_context_effective_from,
323 related_spec_name,
324 related_spec_effective_from,
325 related_data: None,
326 }),
327 limit_name,
328 limit_value,
329 actual_value,
330 }
331 }
332
333 pub fn registry(
335 message: impl Into<String>,
336 source: Source,
337 identifier: impl Into<String>,
338 kind: RegistryErrorKind,
339 suggestion: Option<impl Into<String>>,
340 spec_context: Option<&LemmaSpec>,
341 related_spec: Option<&LemmaSpec>,
342 ) -> Self {
343 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
344 let (related_spec_name, related_spec_effective_from) = attribution_fields(related_spec);
345 Self::Registry {
346 details: Box::new(ErrorDetails {
347 message: message.into(),
348 source: Some(source),
349 suggestion: suggestion.map(Into::into),
350 spec_context_name,
351 spec_context_effective_from,
352 related_spec_name,
353 related_spec_effective_from,
354 related_data: None,
355 }),
356 identifier: identifier.into(),
357 kind,
358 }
359 }
360
361 pub fn missing_repository(
363 message: impl Into<String>,
364 source: Option<Source>,
365 repository: impl Into<String>,
366 suggestion: Option<impl Into<String>>,
367 spec_context: Option<&LemmaSpec>,
368 ) -> Self {
369 let (spec_context_name, spec_context_effective_from) = attribution_fields(spec_context);
370 Self::MissingRepository {
371 details: Box::new(ErrorDetails {
372 message: message.into(),
373 source,
374 suggestion: suggestion.map(Into::into),
375 spec_context_name,
376 spec_context_effective_from,
377 related_spec_name: None,
378 related_spec_effective_from: None,
379 related_data: None,
380 }),
381 repository: repository.into(),
382 }
383 }
384
385 pub fn with_spec_context(self, spec: &LemmaSpec) -> Self {
389 self.map_details(|d| {
390 if d.spec_context_name.is_some() {
391 assert!(
392 d.spec_context_effective_from.is_some(),
393 "BUG: spec_context_name set without spec_context_effective_from"
394 );
395 return;
396 }
397 d.spec_context_name = Some(spec.name.clone());
398 d.spec_context_effective_from = Some(spec.effective_from.clone());
399 })
400 }
401
402 pub fn with_related_data(self, name: impl Into<String>) -> Self {
406 let name = name.into();
407 self.map_details(|d| d.related_data = Some(name))
408 }
409
410 fn map_details(self, f: impl FnOnce(&mut ErrorDetails)) -> Self {
412 match self {
413 Error::Parsing(details) => {
414 let mut d = *details;
415 f(&mut d);
416 Error::Parsing(Box::new(d))
417 }
418 Error::Inversion(details) => {
419 let mut d = *details;
420 f(&mut d);
421 Error::Inversion(Box::new(d))
422 }
423 Error::Validation(details) => {
424 let mut d = *details;
425 f(&mut d);
426 Error::Validation(Box::new(d))
427 }
428 Error::Registry {
429 details,
430 identifier,
431 kind,
432 } => {
433 let mut d = *details;
434 f(&mut d);
435 Error::Registry {
436 details: Box::new(d),
437 identifier,
438 kind,
439 }
440 }
441 Error::MissingRepository {
442 details,
443 repository,
444 } => {
445 let mut d = *details;
446 f(&mut d);
447 Error::MissingRepository {
448 details: Box::new(d),
449 repository,
450 }
451 }
452 Error::ResourceLimitExceeded {
453 details,
454 limit_name,
455 limit_value,
456 actual_value,
457 } => {
458 let mut d = *details;
459 f(&mut d);
460 Error::ResourceLimitExceeded {
461 details: Box::new(d),
462 limit_name,
463 limit_value,
464 actual_value,
465 }
466 }
467 Error::Request { details, kind } => {
468 let mut d = *details;
469 f(&mut d);
470 Error::Request {
471 details: Box::new(d),
472 kind,
473 }
474 }
475 }
476 }
477}
478
479fn format_related_spec(name: &str, effective_from: &EffectiveDate) -> String {
480 let effective_from_str = effective_from
481 .as_ref()
482 .map(|d| d.to_string())
483 .unwrap_or_else(|| "beginning".to_string());
484 format!(
485 "See spec '{}' (effective from {}).",
486 name, effective_from_str
487 )
488}
489
490fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
491 if let Some(src) = source {
492 write!(
493 f,
494 " at {}:{}:{}",
495 src.source_type, src.span.line, src.span.col
496 )
497 } else {
498 Ok(())
499 }
500}
501
502fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
503 if let Some(ref name) = details.related_spec_name {
504 let effective = details
505 .related_spec_effective_from
506 .as_ref()
507 .expect("BUG: related_spec_name set without related_spec_effective_from");
508 write!(f, " {}", format_related_spec(name, effective))?;
509 }
510 Ok(())
511}
512
513fn write_spec_context(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
514 write!(f, "In spec '{}': ", name)
515}
516
517impl fmt::Display for Error {
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 match self {
520 Error::Parsing(details) => {
521 if let Some(ref name) = details.spec_context_name {
522 write_spec_context(f, name)?;
523 }
524 write!(f, "Parse error: {}", details.message)?;
525 if let Some(suggestion) = &details.suggestion {
526 write!(f, " (suggestion: {suggestion})")?;
527 }
528 write_related_spec(f, details)?;
529 write_source_location(f, &details.source)
530 }
531 Error::Inversion(details) => {
532 if let Some(ref name) = details.spec_context_name {
533 write_spec_context(f, name)?;
534 }
535 write!(f, "Inversion error: {}", details.message)?;
536 if let Some(suggestion) = &details.suggestion {
537 write!(f, " (suggestion: {suggestion})")?;
538 }
539 write_related_spec(f, details)?;
540 write_source_location(f, &details.source)
541 }
542 Error::Validation(details) => {
543 if let Some(ref name) = details.spec_context_name {
544 write_spec_context(f, name)?;
545 }
546 write!(f, "Validation error: ")?;
547 if let Some(ref name) = details.related_data {
548 write!(f, "Failed to parse data '{}': ", name)?;
549 }
550 write!(f, "{}", details.message)?;
551 if let Some(suggestion) = &details.suggestion {
552 write!(f, " (suggestion: {suggestion})")?;
553 }
554 write_related_spec(f, details)?;
555 write_source_location(f, &details.source)
556 }
557 Error::Registry {
558 details,
559 identifier,
560 kind,
561 } => {
562 if let Some(ref name) = details.spec_context_name {
563 write_spec_context(f, name)?;
564 }
565 write!(
566 f,
567 "Registry error ({}): {}: {}",
568 kind, identifier, details.message
569 )?;
570 if let Some(suggestion) = &details.suggestion {
571 write!(f, " (suggestion: {suggestion})")?;
572 }
573 write_related_spec(f, details)?;
574 write_source_location(f, &details.source)
575 }
576 Error::MissingRepository {
577 details,
578 repository,
579 } => {
580 if let Some(ref name) = details.spec_context_name {
581 write_spec_context(f, name)?;
582 }
583 write!(f, "Missing repository: {}: {}", repository, details.message)?;
584 if let Some(suggestion) = &details.suggestion {
585 write!(f, " (suggestion: {suggestion})")?;
586 }
587 write_related_spec(f, details)?;
588 write_source_location(f, &details.source)
589 }
590 Error::ResourceLimitExceeded {
591 details,
592 limit_name,
593 limit_value,
594 actual_value,
595 } => {
596 if let Some(ref name) = details.spec_context_name {
597 write_spec_context(f, name)?;
598 }
599 write!(
600 f,
601 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
602 )?;
603 if let Some(suggestion) = &details.suggestion {
604 write!(f, ". {suggestion}")?;
605 }
606 write_source_location(f, &details.source)
607 }
608 Error::Request { details, .. } => {
609 if let Some(ref name) = details.spec_context_name {
610 write_spec_context(f, name)?;
611 }
612 write!(f, "Request error: {}", details.message)?;
613 if let Some(suggestion) = &details.suggestion {
614 write!(f, " (suggestion: {suggestion})")?;
615 }
616 write_related_spec(f, details)?;
617 write_source_location(f, &details.source)
618 }
619 }
620 }
621}
622
623impl std::error::Error for Error {}
624
625impl From<std::fmt::Error> for Error {
626 fn from(err: std::fmt::Error) -> Self {
627 Error::validation(format!("Format error: {err}"), None, None::<String>)
628 }
629}
630
631impl Error {
632 pub fn kind(&self) -> ErrorKind {
635 match self {
636 Error::Parsing(_) => ErrorKind::Parsing,
637 Error::Validation(_) => ErrorKind::Validation,
638 Error::Inversion(_) => ErrorKind::Inversion,
639 Error::Registry { .. } => ErrorKind::Registry,
640 Error::MissingRepository { .. } => ErrorKind::MissingRepository,
641 Error::Request { .. } => ErrorKind::Request,
642 Error::ResourceLimitExceeded { .. } => ErrorKind::ResourceLimit,
643 }
644 }
645
646 pub(crate) fn details(&self) -> &ErrorDetails {
648 match self {
649 Error::Parsing(d) | Error::Inversion(d) | Error::Validation(d) => d,
650 Error::Registry { details, .. }
651 | Error::MissingRepository { details, .. }
652 | Error::ResourceLimitExceeded { details, .. }
653 | Error::Request { details, .. } => details,
654 }
655 }
656
657 #[must_use]
661 pub fn repository(&self) -> Option<&str> {
662 match self {
663 Error::MissingRepository { repository, .. } => Some(repository.as_str()),
664 Error::Registry { identifier, .. } => Some(identifier.as_str()),
665 _ => None,
666 }
667 }
668
669 pub fn message(&self) -> &str {
671 &self.details().message
672 }
673
674 pub fn location(&self) -> Option<&Source> {
676 self.details().source.as_ref()
677 }
678
679 pub fn source_location(&self) -> Option<&Source> {
681 self.location()
682 }
683
684 pub fn source_text(
686 &self,
687 sources: &std::collections::HashMap<crate::parsing::source::SourceType, String>,
688 ) -> Option<String> {
689 self.location()
690 .and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
691 }
692
693 pub fn suggestion(&self) -> Option<&str> {
695 self.details().suggestion.as_deref()
696 }
697
698 pub fn related_data(&self) -> Option<&str> {
700 self.details().related_data.as_deref()
701 }
702
703 pub fn spec_context_name(&self) -> Option<&str> {
705 self.details().spec_context_name.as_deref()
706 }
707
708 pub fn related_spec(&self) -> Option<&str> {
710 self.details().related_spec_name.as_deref()
711 }
712
713 #[must_use]
715 pub fn registry_kind(&self) -> Option<RegistryErrorKind> {
716 match self {
717 Error::Registry { kind, .. } => Some(*kind),
718 _ => None,
719 }
720 }
721
722 #[must_use]
724 pub fn request_kind(&self) -> Option<RequestErrorKind> {
725 match self {
726 Error::Request { kind, .. } => Some(*kind),
727 _ => None,
728 }
729 }
730
731 #[must_use]
733 pub fn limit_name(&self) -> Option<&str> {
734 match self {
735 Error::ResourceLimitExceeded { limit_name, .. } => Some(limit_name.as_str()),
736 _ => None,
737 }
738 }
739
740 #[must_use]
743 pub fn limit_value(&self) -> Option<&str> {
744 match self {
745 Error::ResourceLimitExceeded { limit_value, .. } => Some(limit_value.as_str()),
746 _ => None,
747 }
748 }
749
750 #[must_use]
753 pub fn actual_value(&self) -> Option<&str> {
754 match self {
755 Error::ResourceLimitExceeded { actual_value, .. } => Some(actual_value.as_str()),
756 _ => None,
757 }
758 }
759}
760
761#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
764pub struct EngineErrorSource {
765 pub attribute: String,
766 pub line: usize,
767 pub column: usize,
768 pub length: usize,
769}
770
771impl From<&Source> for EngineErrorSource {
772 fn from(source: &Source) -> Self {
773 Self {
774 attribute: source.source_type.to_string(),
775 line: source.span.line,
776 column: source.span.col,
777 length: source.span.end.saturating_sub(source.span.start),
778 }
779 }
780}
781
782#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
785pub struct EngineError {
786 pub kind: ErrorKind,
787 pub message: String,
788 pub related_data: Option<String>,
789 pub spec: Option<String>,
790 pub related_spec: Option<String>,
791 pub source: Option<EngineErrorSource>,
792 pub suggestion: Option<String>,
793 pub repository: Option<String>,
795 pub registry_kind: Option<RegistryErrorKind>,
797 pub request_kind: Option<RequestErrorKind>,
799 pub limit_name: Option<String>,
801 pub limit_value: Option<String>,
802 pub actual_value: Option<String>,
803}
804
805impl From<&Error> for EngineError {
806 fn from(error: &Error) -> Self {
807 Self {
808 kind: error.kind(),
809 message: error.message().to_string(),
810 related_data: error.related_data().map(str::to_string),
811 spec: error.spec_context_name().map(str::to_string),
812 related_spec: error.related_spec().map(str::to_string),
813 source: error.source_location().map(EngineErrorSource::from),
814 suggestion: error.suggestion().map(str::to_string),
815 repository: error.repository().map(str::to_string),
816 registry_kind: error.registry_kind(),
817 request_kind: error.request_kind(),
818 limit_name: error.limit_name().map(str::to_string),
819 limit_value: error.limit_value().map(str::to_string),
820 actual_value: error.actual_value().map(str::to_string),
821 }
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use crate::parsing::ast::Span;
829
830 fn test_source() -> Source {
831 Source::new(
832 crate::parsing::source::SourceType::Path(std::sync::Arc::new(
833 std::path::PathBuf::from("test.lemma"),
834 )),
835 Span {
836 start: 14,
837 end: 21,
838 line: 1,
839 col: 15,
840 },
841 )
842 }
843
844 #[test]
845 fn test_error_creation_and_display() {
846 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
847 let parse_error_display = format!("{parse_error}");
848 assert!(parse_error_display.contains("Parse error: Invalid currency"));
849 assert!(parse_error_display.contains("test.lemma:1:15"));
850
851 let suggestion_source = Source::new(
852 crate::parsing::source::SourceType::Volatile,
853 Span {
854 start: 5,
855 end: 10,
856 line: 2,
857 col: 3,
858 },
859 );
860 let suggestion_error =
861 Error::parsing_with_suggestion("typo", suggestion_source, "did you mean X?");
862 assert!(format!("{suggestion_error}").contains("suggestion: did you mean X?"));
863 }
864
865 #[test]
866 fn test_request_error_accessors() {
867 let err = Error::request("bad id", Some("use a valid id"));
868 assert_eq!(err.kind(), ErrorKind::Request);
869 assert_eq!(err.message(), "bad id");
870 assert!(err.location().is_none());
871 assert_eq!(err.suggestion(), Some("use a valid id"));
872 assert!(err.spec_context_name().is_none());
873 assert!(err.related_spec().is_none());
874 }
875
876 #[test]
877 fn test_missing_repository_display() {
878 let err = Error::missing_repository(
879 "not loaded",
880 None,
881 "@iso/countries",
882 Some("load the dependency first"),
883 None,
884 );
885 let display = format!("{err}");
886 assert!(display.contains("Missing repository"));
887 assert!(display.contains("@iso/countries"));
888 assert!(display.contains("not loaded"));
889 }
890
891 #[test]
892 fn test_with_spec_context_copies_name() {
893 let spec = LemmaSpec::new("pricing".to_string());
894 let err = Error::validation("bad", None, None::<String>).with_spec_context(&spec);
895 assert_eq!(err.spec_context_name(), Some("pricing"));
896 let display = format!("{err}");
897 assert!(display.contains("In spec 'pricing':"));
898 }
899
900 #[test]
901 fn test_with_spec_context_keeps_existing_attribution() {
902 let origin = LemmaSpec::new("production".to_string());
903 let consumer = LemmaSpec::new("manufacturing".to_string());
904 let err = Error::validation_with_context("bad", None, None::<String>, Some(&origin), None)
905 .with_spec_context(&consumer);
906 assert_eq!(err.spec_context_name(), Some("production"));
907 }
908}