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 {
387 self.map_details(|d| {
388 d.spec_context_name = Some(spec.name.clone());
389 d.spec_context_effective_from = Some(spec.effective_from.clone());
390 })
391 }
392
393 pub fn with_related_data(self, name: impl Into<String>) -> Self {
397 let name = name.into();
398 self.map_details(|d| d.related_data = Some(name))
399 }
400
401 fn map_details(self, f: impl FnOnce(&mut ErrorDetails)) -> Self {
403 match self {
404 Error::Parsing(details) => {
405 let mut d = *details;
406 f(&mut d);
407 Error::Parsing(Box::new(d))
408 }
409 Error::Inversion(details) => {
410 let mut d = *details;
411 f(&mut d);
412 Error::Inversion(Box::new(d))
413 }
414 Error::Validation(details) => {
415 let mut d = *details;
416 f(&mut d);
417 Error::Validation(Box::new(d))
418 }
419 Error::Registry {
420 details,
421 identifier,
422 kind,
423 } => {
424 let mut d = *details;
425 f(&mut d);
426 Error::Registry {
427 details: Box::new(d),
428 identifier,
429 kind,
430 }
431 }
432 Error::MissingRepository {
433 details,
434 repository,
435 } => {
436 let mut d = *details;
437 f(&mut d);
438 Error::MissingRepository {
439 details: Box::new(d),
440 repository,
441 }
442 }
443 Error::ResourceLimitExceeded {
444 details,
445 limit_name,
446 limit_value,
447 actual_value,
448 } => {
449 let mut d = *details;
450 f(&mut d);
451 Error::ResourceLimitExceeded {
452 details: Box::new(d),
453 limit_name,
454 limit_value,
455 actual_value,
456 }
457 }
458 Error::Request { details, kind } => {
459 let mut d = *details;
460 f(&mut d);
461 Error::Request {
462 details: Box::new(d),
463 kind,
464 }
465 }
466 }
467 }
468}
469
470fn format_related_spec(name: &str, effective_from: &EffectiveDate) -> String {
471 let effective_from_str = effective_from
472 .as_ref()
473 .map(|d| d.to_string())
474 .unwrap_or_else(|| "beginning".to_string());
475 format!(
476 "See spec '{}' (effective from {}).",
477 name, effective_from_str
478 )
479}
480
481fn write_source_location(f: &mut fmt::Formatter<'_>, source: &Option<Source>) -> fmt::Result {
482 if let Some(src) = source {
483 write!(
484 f,
485 " at {}:{}:{}",
486 src.source_type, src.span.line, src.span.col
487 )
488 } else {
489 Ok(())
490 }
491}
492
493fn write_related_spec(f: &mut fmt::Formatter<'_>, details: &ErrorDetails) -> fmt::Result {
494 if let Some(ref name) = details.related_spec_name {
495 let effective = details
496 .related_spec_effective_from
497 .as_ref()
498 .expect("BUG: related_spec_name set without related_spec_effective_from");
499 write!(f, " {}", format_related_spec(name, effective))?;
500 }
501 Ok(())
502}
503
504fn write_spec_context(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
505 write!(f, "In spec '{}': ", name)
506}
507
508impl fmt::Display for Error {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 match self {
511 Error::Parsing(details) => {
512 if let Some(ref name) = details.spec_context_name {
513 write_spec_context(f, name)?;
514 }
515 write!(f, "Parse error: {}", details.message)?;
516 if let Some(suggestion) = &details.suggestion {
517 write!(f, " (suggestion: {suggestion})")?;
518 }
519 write_related_spec(f, details)?;
520 write_source_location(f, &details.source)
521 }
522 Error::Inversion(details) => {
523 if let Some(ref name) = details.spec_context_name {
524 write_spec_context(f, name)?;
525 }
526 write!(f, "Inversion error: {}", details.message)?;
527 if let Some(suggestion) = &details.suggestion {
528 write!(f, " (suggestion: {suggestion})")?;
529 }
530 write_related_spec(f, details)?;
531 write_source_location(f, &details.source)
532 }
533 Error::Validation(details) => {
534 if let Some(ref name) = details.spec_context_name {
535 write_spec_context(f, name)?;
536 }
537 write!(f, "Validation error: ")?;
538 if let Some(ref name) = details.related_data {
539 write!(f, "Failed to parse data '{}': ", name)?;
540 }
541 write!(f, "{}", details.message)?;
542 if let Some(suggestion) = &details.suggestion {
543 write!(f, " (suggestion: {suggestion})")?;
544 }
545 write_related_spec(f, details)?;
546 write_source_location(f, &details.source)
547 }
548 Error::Registry {
549 details,
550 identifier,
551 kind,
552 } => {
553 if let Some(ref name) = details.spec_context_name {
554 write_spec_context(f, name)?;
555 }
556 write!(
557 f,
558 "Registry error ({}): {}: {}",
559 kind, identifier, details.message
560 )?;
561 if let Some(suggestion) = &details.suggestion {
562 write!(f, " (suggestion: {suggestion})")?;
563 }
564 write_related_spec(f, details)?;
565 write_source_location(f, &details.source)
566 }
567 Error::MissingRepository {
568 details,
569 repository,
570 } => {
571 if let Some(ref name) = details.spec_context_name {
572 write_spec_context(f, name)?;
573 }
574 write!(f, "Missing repository: {}: {}", repository, details.message)?;
575 if let Some(suggestion) = &details.suggestion {
576 write!(f, " (suggestion: {suggestion})")?;
577 }
578 write_related_spec(f, details)?;
579 write_source_location(f, &details.source)
580 }
581 Error::ResourceLimitExceeded {
582 details,
583 limit_name,
584 limit_value,
585 actual_value,
586 } => {
587 if let Some(ref name) = details.spec_context_name {
588 write_spec_context(f, name)?;
589 }
590 write!(
591 f,
592 "Resource limit exceeded: {limit_name} (limit: {limit_value}, actual: {actual_value})"
593 )?;
594 if let Some(suggestion) = &details.suggestion {
595 write!(f, ". {suggestion}")?;
596 }
597 write_source_location(f, &details.source)
598 }
599 Error::Request { details, .. } => {
600 if let Some(ref name) = details.spec_context_name {
601 write_spec_context(f, name)?;
602 }
603 write!(f, "Request error: {}", details.message)?;
604 if let Some(suggestion) = &details.suggestion {
605 write!(f, " (suggestion: {suggestion})")?;
606 }
607 write_related_spec(f, details)?;
608 write_source_location(f, &details.source)
609 }
610 }
611 }
612}
613
614impl std::error::Error for Error {}
615
616impl From<std::fmt::Error> for Error {
617 fn from(err: std::fmt::Error) -> Self {
618 Error::validation(format!("Format error: {err}"), None, None::<String>)
619 }
620}
621
622impl Error {
623 pub fn kind(&self) -> ErrorKind {
626 match self {
627 Error::Parsing(_) => ErrorKind::Parsing,
628 Error::Validation(_) => ErrorKind::Validation,
629 Error::Inversion(_) => ErrorKind::Inversion,
630 Error::Registry { .. } => ErrorKind::Registry,
631 Error::MissingRepository { .. } => ErrorKind::MissingRepository,
632 Error::Request { .. } => ErrorKind::Request,
633 Error::ResourceLimitExceeded { .. } => ErrorKind::ResourceLimit,
634 }
635 }
636
637 pub(crate) fn details(&self) -> &ErrorDetails {
639 match self {
640 Error::Parsing(d) | Error::Inversion(d) | Error::Validation(d) => d,
641 Error::Registry { details, .. }
642 | Error::MissingRepository { details, .. }
643 | Error::ResourceLimitExceeded { details, .. }
644 | Error::Request { details, .. } => details,
645 }
646 }
647
648 #[must_use]
652 pub fn repository(&self) -> Option<&str> {
653 match self {
654 Error::MissingRepository { repository, .. } => Some(repository.as_str()),
655 Error::Registry { identifier, .. } => Some(identifier.as_str()),
656 _ => None,
657 }
658 }
659
660 pub fn message(&self) -> &str {
662 &self.details().message
663 }
664
665 pub fn location(&self) -> Option<&Source> {
667 self.details().source.as_ref()
668 }
669
670 pub fn source_location(&self) -> Option<&Source> {
672 self.location()
673 }
674
675 pub fn source_text(
677 &self,
678 sources: &std::collections::HashMap<crate::parsing::source::SourceType, String>,
679 ) -> Option<String> {
680 self.location()
681 .and_then(|s| s.text_from(sources).map(|c| c.into_owned()))
682 }
683
684 pub fn suggestion(&self) -> Option<&str> {
686 self.details().suggestion.as_deref()
687 }
688
689 pub fn related_data(&self) -> Option<&str> {
691 self.details().related_data.as_deref()
692 }
693
694 pub fn spec_context_name(&self) -> Option<&str> {
696 self.details().spec_context_name.as_deref()
697 }
698
699 pub fn related_spec(&self) -> Option<&str> {
701 self.details().related_spec_name.as_deref()
702 }
703
704 #[must_use]
706 pub fn registry_kind(&self) -> Option<RegistryErrorKind> {
707 match self {
708 Error::Registry { kind, .. } => Some(*kind),
709 _ => None,
710 }
711 }
712
713 #[must_use]
715 pub fn request_kind(&self) -> Option<RequestErrorKind> {
716 match self {
717 Error::Request { kind, .. } => Some(*kind),
718 _ => None,
719 }
720 }
721
722 #[must_use]
724 pub fn limit_name(&self) -> Option<&str> {
725 match self {
726 Error::ResourceLimitExceeded { limit_name, .. } => Some(limit_name.as_str()),
727 _ => None,
728 }
729 }
730
731 #[must_use]
734 pub fn limit_value(&self) -> Option<&str> {
735 match self {
736 Error::ResourceLimitExceeded { limit_value, .. } => Some(limit_value.as_str()),
737 _ => None,
738 }
739 }
740
741 #[must_use]
744 pub fn actual_value(&self) -> Option<&str> {
745 match self {
746 Error::ResourceLimitExceeded { actual_value, .. } => Some(actual_value.as_str()),
747 _ => None,
748 }
749 }
750}
751
752#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
755pub struct EngineErrorSource {
756 pub attribute: String,
757 pub line: usize,
758 pub column: usize,
759 pub length: usize,
760}
761
762impl From<&Source> for EngineErrorSource {
763 fn from(source: &Source) -> Self {
764 Self {
765 attribute: source.source_type.to_string(),
766 line: source.span.line,
767 column: source.span.col,
768 length: source.span.end.saturating_sub(source.span.start),
769 }
770 }
771}
772
773#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
776pub struct EngineError {
777 pub kind: ErrorKind,
778 pub message: String,
779 pub related_data: Option<String>,
780 pub spec: Option<String>,
781 pub related_spec: Option<String>,
782 pub source: Option<EngineErrorSource>,
783 pub suggestion: Option<String>,
784 pub repository: Option<String>,
786 pub registry_kind: Option<RegistryErrorKind>,
788 pub request_kind: Option<RequestErrorKind>,
790 pub limit_name: Option<String>,
792 pub limit_value: Option<String>,
793 pub actual_value: Option<String>,
794}
795
796impl From<&Error> for EngineError {
797 fn from(error: &Error) -> Self {
798 Self {
799 kind: error.kind(),
800 message: error.message().to_string(),
801 related_data: error.related_data().map(str::to_string),
802 spec: error.spec_context_name().map(str::to_string),
803 related_spec: error.related_spec().map(str::to_string),
804 source: error.source_location().map(EngineErrorSource::from),
805 suggestion: error.suggestion().map(str::to_string),
806 repository: error.repository().map(str::to_string),
807 registry_kind: error.registry_kind(),
808 request_kind: error.request_kind(),
809 limit_name: error.limit_name().map(str::to_string),
810 limit_value: error.limit_value().map(str::to_string),
811 actual_value: error.actual_value().map(str::to_string),
812 }
813 }
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819 use crate::parsing::ast::Span;
820
821 fn test_source() -> Source {
822 Source::new(
823 crate::parsing::source::SourceType::Path(std::sync::Arc::new(
824 std::path::PathBuf::from("test.lemma"),
825 )),
826 Span {
827 start: 14,
828 end: 21,
829 line: 1,
830 col: 15,
831 },
832 )
833 }
834
835 #[test]
836 fn test_error_creation_and_display() {
837 let parse_error = Error::parsing("Invalid currency", test_source(), None::<String>);
838 let parse_error_display = format!("{parse_error}");
839 assert!(parse_error_display.contains("Parse error: Invalid currency"));
840 assert!(parse_error_display.contains("test.lemma:1:15"));
841
842 let suggestion_source = Source::new(
843 crate::parsing::source::SourceType::Volatile,
844 Span {
845 start: 5,
846 end: 10,
847 line: 2,
848 col: 3,
849 },
850 );
851 let suggestion_error =
852 Error::parsing_with_suggestion("typo", suggestion_source, "did you mean X?");
853 assert!(format!("{suggestion_error}").contains("suggestion: did you mean X?"));
854 }
855
856 #[test]
857 fn test_request_error_accessors() {
858 let err = Error::request("bad id", Some("use a valid id"));
859 assert_eq!(err.kind(), ErrorKind::Request);
860 assert_eq!(err.message(), "bad id");
861 assert!(err.location().is_none());
862 assert_eq!(err.suggestion(), Some("use a valid id"));
863 assert!(err.spec_context_name().is_none());
864 assert!(err.related_spec().is_none());
865 }
866
867 #[test]
868 fn test_missing_repository_display() {
869 let err = Error::missing_repository(
870 "not loaded",
871 None,
872 "@iso/countries",
873 Some("load the dependency first"),
874 None,
875 );
876 let display = format!("{err}");
877 assert!(display.contains("Missing repository"));
878 assert!(display.contains("@iso/countries"));
879 assert!(display.contains("not loaded"));
880 }
881
882 #[test]
883 fn test_with_spec_context_copies_name() {
884 let spec = LemmaSpec::new("pricing".to_string());
885 let err = Error::validation("bad", None, None::<String>).with_spec_context(&spec);
886 assert_eq!(err.spec_context_name(), Some("pricing"));
887 let display = format!("{err}");
888 assert!(display.contains("In spec 'pricing':"));
889 }
890}