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