1use crate::Location;
2use crate::de_error::{Error, MessageFormatter, UserMessageFormatter};
3use crate::localizer::{ExternalMessage, Localizer};
4
5use std::borrow::Cow;
6
7#[cfg(any(feature = "garde", feature = "validator"))]
8use crate::{
9 Locations,
10 de_error::ValidationIssue,
11 localizer::ExternalMessageSource,
12 path_map::{PathMap, format_path_with_resolved_leaf},
13};
14
15#[derive(Debug, Default, Clone, Copy)]
21pub struct DefaultMessageFormatter;
22
23pub type DeveloperMessageFormatter = DefaultMessageFormatter;
25
26#[cfg(any(feature = "garde", feature = "validator"))]
27fn format_validation_issues(
28 l10n: &dyn Localizer,
29 source: ExternalMessageSource,
30 issues: &[ValidationIssue],
31 locations: &PathMap,
32) -> String {
33 let mut lines = Vec::with_capacity(issues.len());
34 for issue in issues {
35 let entry = issue.display_entry_overridden(l10n, source);
36 let path_key = &issue.path;
37 let original_leaf = path_key
38 .leaf_string()
39 .unwrap_or_else(|| l10n.root_path_label().into_owned());
40
41 let (locs, resolved_leaf) = locations
42 .search_with_ancestor_fallback(path_key)
43 .unwrap_or((Locations::UNKNOWN, original_leaf));
44
45 let loc = if locs.reference_location != Location::UNKNOWN {
46 locs.reference_location
47 } else {
48 locs.defined_location
49 };
50
51 let resolved_path = format_path_with_resolved_leaf(path_key, &resolved_leaf);
52
53 lines.push(l10n.validation_issue_line(
54 &resolved_path,
55 &entry,
56 (loc != Location::UNKNOWN).then_some(loc),
57 ));
58 }
59 l10n.join_validation_issues(&lines)
60}
61
62fn default_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
63 match err {
64 Error::WithSnippet { error, .. } => default_format_message(formatter, error),
65 Error::ExternalMessage {
66 source,
67 msg,
68 code,
69 params,
70 ..
71 } => {
72 let l10n = formatter.localizer();
73 l10n.override_external_message(ExternalMessage {
74 source: *source,
75 original: msg.as_str(),
76 code: code.as_deref(),
77 params,
78 })
79 .unwrap_or(Cow::Borrowed(msg.as_str()))
80 }
81 Error::Message { msg, .. }
82 | Error::HookError { msg, .. }
83 | Error::SerdeVariantId { msg, .. } => Cow::Borrowed(msg.as_str()),
84 Error::UnresolvedProperty { name, .. } => Cow::Owned(format!("missing property `{name}`")),
85 Error::InvalidPropertyName { name, .. } => Cow::Owned(format!("Invalid name: '{name}'")),
86 Error::PropertyRequiredButUnset { name, message, .. } if message.is_empty() => {
87 Cow::Owned(format!("missing property `{name}`"))
88 }
89 Error::PropertyRequiredButUnset { name, message, .. } => {
90 Cow::Owned(format!("missing property `{name}`: {message}"))
91 }
92 Error::PropertyRequiredButEmpty { name, message, .. } if message.is_empty() => {
93 Cow::Owned(format!("empty property `{name}`"))
94 }
95 Error::PropertyRequiredButEmpty { name, message, .. } => {
96 Cow::Owned(format!("empty property `{name}`: {message}"))
97 }
98 Error::Eof { .. } => Cow::Borrowed("unexpected end of input"),
99 Error::MultipleDocuments { hint, .. } => {
100 Cow::Owned(format!("multiple YAML documents detected; {hint}"))
101 }
102 Error::Unexpected { expected, .. } => {
103 Cow::Owned(format!("unexpected event: expected {expected}"))
104 }
105 Error::MergeValueNotMapOrSeqOfMaps { .. } => {
106 Cow::Borrowed("YAML merge value must be mapping or sequence of mappings")
107 }
108 Error::MergeKeyNotAllowed { .. } => {
109 Cow::Borrowed("YAML merge keys are not allowed by configured policy")
110 }
111 Error::InvalidBinaryBase64 { .. } => Cow::Borrowed("invalid !!binary base64"),
112 Error::InvalidUtf8Input => Cow::Borrowed("input is not valid UTF-8"),
113 Error::BinaryNotUtf8 { .. } => Cow::Borrowed(
114 "!!binary scalar is not valid UTF-8 so cannot be stored into string. \
115 If you just use !!binary for documentation/annotation, set ignore_binary_tag_for_string in Options",
116 ),
117 Error::TaggedScalarCannotDeserializeIntoString { .. } => {
118 Cow::Borrowed("cannot deserialize tagged scalar into string")
119 }
120 Error::UnexpectedSequenceEnd { .. } => Cow::Borrowed("unexpected sequence end"),
121 Error::UnexpectedMappingEnd { .. } => Cow::Borrowed("unexpected mapping end"),
122 Error::InvalidBooleanStrict { .. } => {
123 Cow::Borrowed("invalid boolean (strict mode expects true/false)")
124 }
125 Error::InvalidCharNull { .. } => {
126 Cow::Borrowed("invalid char: cannot deserialize null; use Option<char>")
127 }
128 Error::InvalidCharNotSingleScalar { .. } => {
129 Cow::Borrowed("invalid char: expected a single Unicode scalar value")
130 }
131 Error::NullIntoString { .. } => {
132 Cow::Borrowed("cannot deserialize null into string; use Option<String>")
133 }
134 Error::BytesNotSupportedMissingBinaryTag { .. } => {
135 Cow::Borrowed("bytes not supported (missing !!binary tag)")
136 }
137 Error::UnexpectedValueForUnit { .. } => Cow::Borrowed("unexpected value for unit"),
138 Error::ExpectedEmptyMappingForUnitStruct { .. } => {
139 Cow::Borrowed("expected empty mapping for unit struct")
140 }
141 Error::UnexpectedContainerEndWhileSkippingNode { .. } => {
142 Cow::Borrowed("unexpected container end while skipping node")
143 }
144 Error::InternalSeedReusedForMapKey { .. } => {
145 Cow::Borrowed("internal error: seed reused for map key")
146 }
147 Error::ValueRequestedBeforeKey { .. } => Cow::Borrowed("value requested before key"),
148 Error::ExpectedStringKeyForExternallyTaggedEnum { .. } => {
149 Cow::Borrowed("expected string key for externally tagged enum")
150 }
151 Error::ExternallyTaggedEnumExpectedScalarOrMapping { .. } => {
152 Cow::Borrowed("externally tagged enum expected scalar or mapping")
153 }
154 Error::UnexpectedValueForUnitEnumVariant { .. } => {
155 Cow::Borrowed("unexpected value for unit enum variant")
156 }
157 Error::AliasReplayCounterOverflow { .. } => Cow::Borrowed("alias replay counter overflow"),
158 Error::AliasReplayLimitExceeded {
159 total_replayed_events,
160 max_total_replayed_events,
161 ..
162 } => Cow::Owned(format!(
163 "alias replay limit exceeded: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
164 )),
165 Error::AliasExpansionLimitExceeded {
166 anchor_id,
167 expansions,
168 max_expansions_per_anchor,
169 ..
170 } => Cow::Owned(format!(
171 "alias expansion limit exceeded for anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
172 )),
173 Error::AliasReplayStackDepthExceeded {
174 depth, max_depth, ..
175 } => Cow::Owned(format!(
176 "alias replay stack depth exceeded: depth={depth} > {max_depth}"
177 )),
178 Error::FoldedBlockScalarMustIndentContent { .. } => {
179 Cow::Borrowed("folded block scalars must indent their content")
180 }
181 Error::InternalDepthUnderflow { .. } => Cow::Borrowed("internal depth underflow"),
182 Error::InternalRecursionStackEmpty { .. } => {
183 Cow::Borrowed("internal recursion stack empty")
184 }
185 Error::RecursiveReferencesRequireWeakTypes { .. } => {
186 Cow::Borrowed("recursive references require weak recursion types")
187 }
188 Error::InvalidScalar { ty, .. } => Cow::Owned(format!("invalid {ty}")),
189 Error::SerdeInvalidType {
190 unexpected,
191 expected,
192 ..
193 } => Cow::Owned(format!("invalid type: {unexpected}, expected {expected}")),
194 Error::SerdeInvalidValue {
195 unexpected,
196 expected,
197 ..
198 } => Cow::Owned(format!("invalid value: {unexpected}, expected {expected}")),
199 Error::SerdeUnknownVariant {
200 variant, expected, ..
201 } => Cow::Owned(format!(
202 "unknown variant `{variant}`, expected one of {}",
203 expected.join(", ")
204 )),
205 Error::SerdeUnknownField {
206 field, expected, ..
207 } => Cow::Owned(format!(
208 "unknown field `{field}`, expected one of {}",
209 expected.join(", ")
210 )),
211 Error::SerdeMissingField { field, .. } => Cow::Owned(format!("missing field `{field}`")),
212 Error::UnexpectedContainerEndWhileReadingKeyNode { .. } => {
213 Cow::Borrowed("unexpected container end while reading key")
214 }
215 Error::DuplicateMappingKey { key, .. } => match key {
216 Some(k) => Cow::Owned(format!(
217 "duplicate mapping key: {k}, set DuplicateKeyPolicy in Options if acceptable"
218 )),
219 None => Cow::Borrowed(
220 "duplicate mapping key, set DuplicateKeyPolicy in Options if acceptable",
221 ),
222 },
223 Error::TaggedEnumMismatch { tagged, target, .. } => Cow::Owned(format!(
224 "tagged enum `{tagged}` does not match target enum `{target}`",
225 )),
226 Error::ExpectedMappingEndAfterEnumVariantValue { .. } => {
227 Cow::Borrowed("expected end of mapping after enum variant value")
228 }
229 Error::ContainerEndMismatch { .. } => Cow::Borrowed("list or mapping end with no start"),
230 Error::UnknownAnchor { .. } => Cow::Borrowed("alias references unknown anchor"),
231 Error::CyclicInclude { id, stack, .. } => {
232 let mut full_msg = format!("cyclic include detected: {id}");
233 if !stack.is_empty() {
234 full_msg.push_str("\nwhile processing include from ");
235 full_msg.push_str(&stack.join(" -> "));
236 }
237 Cow::Owned(full_msg)
238 }
239 Error::UnsupportedIncludeForm { .. } => {
240 Cow::Borrowed("!include currently only supports the scalar form: !include <path>")
241 }
242 Error::ResolverError {
243 target,
244 error,
245 stack,
246 ..
247 } => {
248 let mut full_msg = format!("failed to resolve include {target:?}");
249 if !stack.is_empty() {
250 full_msg.push_str("\nwhile processing include from ");
251 full_msg.push_str(&stack.join(" -> "));
252 }
253 full_msg.push('\n');
254 let msg = match error {
255 crate::input_source::IncludeResolveError::Io(e) => e.to_string(),
256 crate::input_source::IncludeResolveError::Message(m) => m.clone(),
257 crate::input_source::IncludeResolveError::SizeLimitExceeded(size, limit) => {
258 format!("include size {size} bytes exceeds remaining size limit {limit} bytes")
259 }
260 crate::input_source::IncludeResolveError::FileInclude(problem) => {
261 match &**problem {
262 crate::input_source::ResolveProblem::ResolveFailed {
263 spec,
264 base_dir,
265 err,
266 } => {
267 format!(
268 "failed to resolve include '{}' from '{}': {}",
269 spec, base_dir, err
270 )
271 }
272 crate::input_source::ResolveProblem::TargetNotRegularFile { target } => {
273 format!("include target '{}' is not a regular file", target)
274 }
275 crate::input_source::ResolveProblem::TargetIsRootFile { spec } => {
276 format!(
277 "include target '{}' resolves to the configured root file itself",
278 spec
279 )
280 }
281 crate::input_source::ResolveProblem::ParentIdNotAbsoluteCanonical {
282 parent_id,
283 } => {
284 format!(
285 "SafeFileResolver expected parent include id to be an absolute canonical path, got '{}'",
286 parent_id
287 )
288 }
289 crate::input_source::ResolveProblem::ParentResolveFailed {
290 parent_id,
291 from_name,
292 err,
293 } => {
294 format!(
295 "failed to resolve parent include source '{}' (from '{}'): {}",
296 parent_id, from_name, err
297 )
298 }
299 crate::input_source::ResolveProblem::ParentNotRegularFile { parent } => {
300 format!("include parent '{}' is not a regular file", parent)
301 }
302 crate::input_source::ResolveProblem::ParentHasNoDirectory { parent } => {
303 format!(
304 "include parent '{}' does not have a parent directory",
305 parent
306 )
307 }
308 crate::input_source::ResolveProblem::ResolvesOutsideRoot { spec, root } => {
309 format!(
310 "include '{}' resolves outside the configured root '{}'",
311 spec, root
312 )
313 }
314 crate::input_source::ResolveProblem::TraversesSymlink { spec } => {
315 format!(
316 "include '{}' traverses a symlink, which is disabled by policy",
317 spec
318 )
319 }
320 crate::input_source::ResolveProblem::AbsolutePathNotAllowed { spec } => {
321 format!("absolute include paths are not allowed: {}", spec)
322 }
323 crate::input_source::ResolveProblem::EmptyPath => {
324 "include path must not be empty".to_string()
325 }
326 crate::input_source::ResolveProblem::InvalidExtension { spec } => {
327 format!(
328 "include target '{}' does not have a valid YAML extension (.yml or .yaml)",
329 spec
330 )
331 }
332 crate::input_source::ResolveProblem::HiddenFile { spec } => {
333 format!(
334 "include target '{}' is a hidden file, which is not allowed",
335 spec
336 )
337 }
338 crate::input_source::ResolveProblem::EmptyFragment => {
339 "include fragment must not be empty".to_string()
340 }
341 crate::input_source::ResolveProblem::FragmentContainsHash { spec } => {
342 format!("include fragment must not contain '#': {}", spec)
343 }
344 }
345 }
346 };
347 full_msg.push_str(&msg);
348 Cow::Owned(full_msg)
349 }
350 Error::Budget { breach, .. } => Cow::Owned(format!("budget breached: {breach:?}")),
351 Error::QuotingRequired { value, .. } => {
352 Cow::Owned(format!("The string value [{value}] must be quoted"))
353 }
354 Error::CannotBorrowTransformedString { reason, .. } => Cow::Owned(format!(
355 "input does not contain value verbatim so cannot deserialize into &str ({reason}); use String or Cow<str> instead",
356 )),
357 Error::IndentationError {
358 required, actual, ..
359 } => Cow::Owned(format!(
360 "indentation error: expected {required}, found {actual} spaces"
361 )),
362 Error::IOError { cause } => Cow::Owned(format!("IO error: {cause}")),
363 Error::AliasError { msg, locations } => {
364 let l10n = formatter.localizer();
365 let ref_loc = locations.reference_location;
366 let def_loc = locations.defined_location;
367 match (ref_loc, def_loc) {
368 (Location::UNKNOWN, Location::UNKNOWN) => Cow::Borrowed(msg.as_str()),
369 (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
370 Cow::Borrowed(msg.as_str())
371 }
372 (_r, d) => Cow::Owned(format!("{msg}{}", l10n.alias_defined_at(d))),
373 }
374 }
375
376 #[cfg(any(feature = "garde", feature = "validator"))]
377 Error::ValidationError {
378 source,
379 issues,
380 locations,
381 } => {
382 let l10n = formatter.localizer();
383 Cow::Owned(format_validation_issues(
384 l10n,
385 source.external_message_source(),
386 issues,
387 locations,
388 ))
389 }
390 #[cfg(any(feature = "garde", feature = "validator"))]
391 Error::ValidationErrors { errors, .. } => Cow::Owned(format!(
392 "validation failed for {} document(s)",
393 errors.len()
394 )),
395 }
396}
397
398impl MessageFormatter for DefaultMessageFormatter {
399 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
400 default_format_message(self, err)
401 }
402}
403
404pub struct DefaultMessageFormatterWithLocalizer<'a> {
405 localizer: &'a dyn Localizer,
406}
407
408impl MessageFormatter for DefaultMessageFormatterWithLocalizer<'_> {
409 fn localizer(&self) -> &dyn Localizer {
410 self.localizer
411 }
412
413 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
414 default_format_message(self, err)
415 }
416}
417
418impl DefaultMessageFormatter {
419 pub fn with_localizer<'a>(
425 &self,
426 localizer: &'a dyn Localizer,
427 ) -> DefaultMessageFormatterWithLocalizer<'a> {
428 DefaultMessageFormatterWithLocalizer { localizer }
429 }
430}
431
432fn user_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
433 if let Error::WithSnippet { error, .. } = err {
434 return user_format_message(formatter, error);
435 }
436
437 match err {
438 Error::WithSnippet { .. } => unreachable!(),
440
441 Error::Eof { .. } => Cow::Borrowed("unexpected end of file"),
442 Error::MultipleDocuments { .. } => {
443 Cow::Borrowed("only single YAML document expected but multiple found")
444 }
445 Error::InvalidUtf8Input => Cow::Borrowed("YAML parser input is not valid UTF-8"),
446 Error::BinaryNotUtf8 { .. } => {
447 Cow::Borrowed("!!binary scalar is not valid UTF-8 so cannot be stored into string.")
448 }
449 Error::InvalidBooleanStrict { .. } => {
450 Cow::Borrowed("invalid boolean (true or false expected)")
451 }
452 Error::NullIntoString { .. } | Error::InvalidCharNull { .. } => {
453 Cow::Borrowed("null is not allowed here")
454 }
455 Error::InvalidCharNotSingleScalar { .. } => {
456 Cow::Borrowed("only single character allowed here")
457 }
458 Error::BytesNotSupportedMissingBinaryTag { .. } => Cow::Borrowed("missing !!binary tag"),
459 Error::ExpectedEmptyMappingForUnitStruct { .. } => {
460 Cow::Borrowed("expected empty mapping here")
461 }
462 Error::UnexpectedContainerEndWhileSkippingNode { .. } => {
463 Cow::Borrowed("unexpected container end")
464 }
465 Error::AliasReplayCounterOverflow { .. } => {
466 Cow::Borrowed("YAML document too large or too complex")
467 }
468 Error::AliasReplayLimitExceeded {
469 total_replayed_events,
470 max_total_replayed_events,
471 ..
472 } => Cow::Owned(format!(
473 "YAML document too large or too complex: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
474 )),
475 Error::AliasExpansionLimitExceeded {
476 anchor_id,
477 expansions,
478 max_expansions_per_anchor,
479 ..
480 } => Cow::Owned(format!(
481 "YAML document too large or too complex: anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
482 )),
483 Error::AliasReplayStackDepthExceeded {
484 depth, max_depth, ..
485 } => Cow::Owned(format!(
486 "YAML document too large or too complex: depth={depth} > {max_depth}"
487 )),
488 Error::UnknownAnchor { .. } => Cow::Borrowed("reference to unknown value"),
489 Error::MergeKeyNotAllowed { .. } => Cow::Borrowed("merge key not allowed here"),
490 Error::CyclicInclude { .. } => Cow::Borrowed("cyclic include detected"),
491 Error::UnsupportedIncludeForm { .. } => {
492 Cow::Borrowed("!include currently only supports the scalar form: !include <path>")
493 }
494 Error::ResolverError { .. } => Cow::Borrowed("failed to resolve include"),
495 Error::RecursiveReferencesRequireWeakTypes { .. } => {
496 Cow::Borrowed("Recursive reference not allowed here")
497 }
498 Error::DuplicateMappingKey { key, .. } => match key {
499 Some(k) => Cow::Owned(format!("duplicate mapping key: {k} not allowed here")),
500 None => Cow::Borrowed("duplicate mapping key not allowed here"),
501 },
502 Error::QuotingRequired { .. } => Cow::Borrowed("value requires quoting"),
503 Error::Budget { breach, .. } => Cow::Owned(format!(
504 "YAML document too large or too complex: limits breached: {breach:?}"
505 )),
506 Error::CannotBorrowTransformedString { .. } => {
507 Cow::Borrowed("Only single string with no escape sequences is allowed here")
508 }
509 Error::IndentationError {
510 required, actual, ..
511 } => Cow::Owned(format!(
512 "incorrect indentation: expected {required}, found {actual} spaces"
513 )),
514
515 _ => default_format_message(formatter, err),
517 }
518}
519
520impl MessageFormatter for UserMessageFormatter {
521 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
522 user_format_message(self, err)
523 }
524}
525
526pub struct UserMessageFormatterWithLocalizer<'a> {
527 localizer: &'a dyn Localizer,
528}
529
530impl MessageFormatter for UserMessageFormatterWithLocalizer<'_> {
531 fn localizer(&self) -> &dyn Localizer {
532 self.localizer
533 }
534
535 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
536 user_format_message(self, err)
537 }
538}
539
540impl UserMessageFormatter {
541 pub fn with_localizer<'a>(
547 &self,
548 localizer: &'a dyn Localizer,
549 ) -> UserMessageFormatterWithLocalizer<'a> {
550 UserMessageFormatterWithLocalizer { localizer }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use crate::Location;
558 use crate::de_error::{Error, MessageFormatter, TransformReason};
559 use crate::location::Locations;
560
561 fn loc() -> Location {
562 Location::UNKNOWN
563 }
564
565 #[rstest::rstest]
570 #[case::with_snippet_delegates(
571 Error::WithSnippet {
572 regions: vec![],
573 crop_radius: 3,
574 error: Box::new(Error::Eof { location: loc() }),
575 },
576 "unexpected end of input"
577 )]
578 #[case::hook_error(
579 Error::HookError { msg: "hook msg".to_owned(), location: loc() },
580 "hook msg"
581 )]
582 #[case::serde_variant_id(
583 Error::SerdeVariantId { msg: "variant id msg".to_owned(), location: loc() },
584 "variant id msg"
585 )]
586 #[case::invalid_binary_base64(
587 Error::InvalidBinaryBase64 { location: loc() },
588 "invalid !!binary base64"
589 )]
590 #[case::merge_key_not_allowed(
591 Error::MergeKeyNotAllowed { location: loc() },
592 "YAML merge keys are not allowed by configured policy"
593 )]
594 #[case::unexpected_sequence_end(
595 Error::UnexpectedSequenceEnd { location: loc() },
596 "unexpected sequence end"
597 )]
598 #[case::unexpected_mapping_end(
599 Error::UnexpectedMappingEnd { location: loc() },
600 "unexpected mapping end"
601 )]
602 #[case::unexpected_container_end_while_skipping(
603 Error::UnexpectedContainerEndWhileSkippingNode { location: loc() },
604 "unexpected container end while skipping node"
605 )]
606 #[case::internal_seed_reused(
607 Error::InternalSeedReusedForMapKey { location: loc() },
608 "internal error: seed reused for map key"
609 )]
610 #[case::value_requested_before_key(
611 Error::ValueRequestedBeforeKey { location: loc() },
612 "value requested before key"
613 )]
614 #[case::alias_replay_counter_overflow(
615 Error::AliasReplayCounterOverflow { location: loc() },
616 "alias replay counter overflow"
617 )]
618 #[case::folded_block_scalar(
619 Error::FoldedBlockScalarMustIndentContent { location: loc() },
620 "folded block scalars must indent their content"
621 )]
622 #[case::internal_depth_underflow(
623 Error::InternalDepthUnderflow { location: loc() },
624 "internal depth underflow"
625 )]
626 #[case::internal_recursion_stack_empty(
627 Error::InternalRecursionStackEmpty { location: loc() },
628 "internal recursion stack empty"
629 )]
630 #[case::recursive_references_require_weak_types(
631 Error::RecursiveReferencesRequireWeakTypes { location: loc() },
632 "recursive references require weak recursion types"
633 )]
634 #[case::unexpected_container_end_while_reading_key(
635 Error::UnexpectedContainerEndWhileReadingKeyNode { location: loc() },
636 "unexpected container end while reading key"
637 )]
638 #[case::expected_mapping_end_after_enum_variant(
639 Error::ExpectedMappingEndAfterEnumVariantValue { location: loc() },
640 "expected end of mapping after enum variant value"
641 )]
642 #[case::container_end_mismatch(
643 Error::ContainerEndMismatch { location: loc() },
644 "list or mapping end with no start"
645 )]
646 #[case::unresolved_property(
647 Error::UnresolvedProperty { name: "MISSING".to_owned(), location: loc() },
648 "missing property `MISSING`"
649 )]
650 #[case::invalid_property_name(
651 Error::InvalidPropertyName { name: "${ab-cd}".to_owned(), location: loc() },
652 "Invalid name: '${ab-cd}'"
653 )]
654 fn default_exact_messages(#[case] err: Error, #[case] expected: &str) {
655 let formatter = DefaultMessageFormatter;
656 assert_eq!(formatter.format_message(&err), expected);
657 }
658
659 #[rstest::rstest]
660 #[case::serde_invalid_value(
661 Error::SerdeInvalidValue {
662 unexpected: "null".to_owned(),
663 expected: "string".to_owned(),
664 location: loc(),
665 },
666 &["invalid value", "null", "string"]
667 )]
668 #[case::serde_unknown_variant(
669 Error::SerdeUnknownVariant {
670 variant: "foo".to_owned(),
671 expected: vec!["bar", "baz"],
672 location: loc(),
673 },
674 &["unknown variant", "foo"]
675 )]
676 #[case::serde_unknown_field(
677 Error::SerdeUnknownField {
678 field: "xyz".to_owned(),
679 expected: vec!["a", "b"],
680 location: loc(),
681 },
682 &["unknown field", "xyz"]
683 )]
684 #[case::io_error(
685 Error::IOError { cause: std::io::Error::other("disk full") },
686 &["IO error", "disk full"]
687 )]
688 fn default_contains_messages(#[case] err: Error, #[case] needles: &[&str]) {
689 let formatter = DefaultMessageFormatter;
690 let msg = formatter.format_message(&err);
691 for needle in needles {
692 assert!(msg.contains(needle), "got: {msg}, missing: {needle}");
693 }
694 }
695
696 #[rstest::rstest]
697 #[case::unset_with_message(
698 Error::PropertyRequiredButUnset {
699 name: "DB_HOST".to_owned(),
700 message: "set DB_HOST in .env".to_owned(),
701 location: loc(),
702 },
703 "missing property `DB_HOST`: set DB_HOST in .env",
704 )]
705 #[case::unset_empty_message(
706 Error::PropertyRequiredButUnset {
707 name: "DB_HOST".to_owned(),
708 message: String::new(),
709 location: loc(),
710 },
711 "missing property `DB_HOST`",
712 )]
713 #[case::empty_with_message(
714 Error::PropertyRequiredButEmpty {
715 name: "DB_HOST".to_owned(),
716 message: "must not be blank".to_owned(),
717 location: loc(),
718 },
719 "empty property `DB_HOST`: must not be blank",
720 )]
721 #[case::empty_empty_message(
722 Error::PropertyRequiredButEmpty {
723 name: "DB_HOST".to_owned(),
724 message: String::new(),
725 location: loc(),
726 },
727 "empty property `DB_HOST`",
728 )]
729 fn default_property_required_messages(#[case] err: Error, #[case] expected: &str) {
730 let formatter = DefaultMessageFormatter;
731 assert_eq!(formatter.format_message(&err), expected);
732 }
733
734 #[test]
735 fn default_alias_error_both_unknown() {
736 let formatter = DefaultMessageFormatter;
737 let err = Error::AliasError {
738 msg: "alias msg".to_owned(),
739 locations: Locations::UNKNOWN,
740 };
741 assert_eq!(formatter.format_message(&err), "alias msg");
742 }
743
744 #[test]
745 fn default_alias_error_ref_known_def_unknown() {
746 let formatter = DefaultMessageFormatter;
747 let ref_loc = Location::new(1, 0);
748 let err = Error::AliasError {
749 msg: "alias msg".to_owned(),
750 locations: Locations {
751 reference_location: ref_loc,
752 defined_location: Location::UNKNOWN,
753 },
754 };
755 assert_eq!(formatter.format_message(&err), "alias msg");
757 }
758
759 #[test]
760 fn default_alias_error_both_known_different() {
761 let formatter = DefaultMessageFormatter;
762 let ref_loc = Location::new(1, 0);
763 let def_loc = Location::new(5, 0);
764 let err = Error::AliasError {
765 msg: "alias msg".to_owned(),
766 locations: Locations {
767 reference_location: ref_loc,
768 defined_location: def_loc,
769 },
770 };
771 let msg = formatter.format_message(&err);
773 assert!(msg.starts_with("alias msg"), "got: {msg}");
774 }
775
776 #[rstest::rstest]
781 #[case::with_snippet_delegates(
782 Error::WithSnippet {
783 regions: vec![],
784 crop_radius: 3,
785 error: Box::new(Error::Eof { location: loc() }),
786 },
787 "unexpected end of file"
788 )]
789 #[case::eof(Error::Eof { location: loc() }, "unexpected end of file")]
790 #[case::multiple_documents(
791 Error::MultipleDocuments { hint: "use from_str_multidoc", location: loc() },
792 "only single YAML document expected but multiple found"
793 )]
794 #[case::invalid_utf8_input(Error::InvalidUtf8Input, "YAML parser input is not valid UTF-8")]
795 #[case::invalid_boolean_strict(
796 Error::InvalidBooleanStrict { location: loc() },
797 "invalid boolean (true or false expected)"
798 )]
799 #[case::null_into_string(
800 Error::NullIntoString { location: loc() },
801 "null is not allowed here"
802 )]
803 #[case::invalid_char_null(
804 Error::InvalidCharNull { location: loc() },
805 "null is not allowed here"
806 )]
807 #[case::invalid_char_not_single_scalar(
808 Error::InvalidCharNotSingleScalar { location: loc() },
809 "only single character allowed here"
810 )]
811 #[case::bytes_not_supported_missing_binary_tag(
812 Error::BytesNotSupportedMissingBinaryTag { location: loc() },
813 "missing !!binary tag"
814 )]
815 #[case::expected_empty_mapping_for_unit_struct(
816 Error::ExpectedEmptyMappingForUnitStruct { location: loc() },
817 "expected empty mapping here"
818 )]
819 #[case::unexpected_container_end_while_skipping(
820 Error::UnexpectedContainerEndWhileSkippingNode { location: loc() },
821 "unexpected container end"
822 )]
823 #[case::alias_replay_counter_overflow(
824 Error::AliasReplayCounterOverflow { location: loc() },
825 "YAML document too large or too complex"
826 )]
827 #[case::unknown_anchor(
828 Error::UnknownAnchor { location: loc() },
829 "reference to unknown value"
830 )]
831 #[case::merge_key_not_allowed(
832 Error::MergeKeyNotAllowed { location: loc() },
833 "merge key not allowed here"
834 )]
835 #[case::recursive_references_require_weak_types(
836 Error::RecursiveReferencesRequireWeakTypes { location: loc() },
837 "Recursive reference not allowed here"
838 )]
839 #[case::quoting_required(
840 Error::QuotingRequired { value: "yes".to_owned(), location: loc() },
841 "value requires quoting"
842 )]
843 #[case::cannot_borrow_transformed_string(
844 Error::CannotBorrowTransformedString {
845 reason: TransformReason::EscapeSequence,
846 location: loc(),
847 },
848 "Only single string with no escape sequences is allowed here"
849 )]
850 fn user_exact_messages(#[case] err: Error, #[case] expected: &str) {
851 let formatter = UserMessageFormatter;
852 assert_eq!(formatter.format_message(&err), expected);
853 }
854
855 #[rstest::rstest]
856 #[case::binary_not_utf8(Error::BinaryNotUtf8 { location: loc() }, &["!!binary"])]
857 #[case::alias_replay_limit_exceeded(
858 Error::AliasReplayLimitExceeded {
859 total_replayed_events: 1000,
860 max_total_replayed_events: 500,
861 location: loc(),
862 },
863 &["too large or too complex", "1000"]
864 )]
865 #[case::alias_expansion_limit_exceeded(
866 Error::AliasExpansionLimitExceeded {
867 anchor_id: 7,
868 expansions: 200,
869 max_expansions_per_anchor: 100,
870 location: loc(),
871 },
872 &["too large or too complex", "7"]
873 )]
874 #[case::alias_replay_stack_depth_exceeded(
875 Error::AliasReplayStackDepthExceeded {
876 depth: 50,
877 max_depth: 20,
878 location: loc(),
879 },
880 &["too large or too complex", "50"]
881 )]
882 #[case::duplicate_mapping_key_with_key(
883 Error::DuplicateMappingKey { key: Some("mykey".to_owned()), location: loc() },
884 &["mykey", "duplicate"]
885 )]
886 #[case::duplicate_mapping_key_without_key(
887 Error::DuplicateMappingKey { key: None, location: loc() },
888 &["duplicate"]
889 )]
890 #[case::budget(
891 Error::Budget {
892 breach: crate::budget::BudgetBreach::Events { events: 9999 },
893 location: loc(),
894 },
895 &["too large or too complex"]
896 )]
897 #[case::falls_through_to_default_for_unhandled(
898 Error::SerdeInvalidType {
899 unexpected: "seq".to_owned(),
900 expected: "map".to_owned(),
901 location: loc(),
902 },
903 &["invalid type"]
904 )]
905 fn user_contains_messages(#[case] err: Error, #[case] needles: &[&str]) {
906 let formatter = UserMessageFormatter;
907 let msg = formatter.format_message(&err);
908 for needle in needles {
909 assert!(msg.contains(needle), "got: {msg}, missing: {needle}");
910 }
911 }
912
913 #[test]
918 fn user_with_localizer_delegates() {
919 use crate::localizer::DefaultEnglishLocalizer;
920 let localizer = DefaultEnglishLocalizer;
921 let formatter = UserMessageFormatter.with_localizer(&localizer);
922 let err = Error::Eof { location: loc() };
923 assert_eq!(formatter.format_message(&err), "unexpected end of file");
924 }
925
926 #[test]
931 fn default_with_localizer_delegates() {
932 use crate::localizer::DefaultEnglishLocalizer;
933 let localizer = DefaultEnglishLocalizer;
934 let formatter = DefaultMessageFormatter.with_localizer(&localizer);
935 let err = Error::Eof { location: loc() };
936 assert_eq!(formatter.format_message(&err), "unexpected end of input");
937 }
938}