1use crate::budget::BudgetBreach;
2use crate::de_error::{Error, MessageFormatter, UserMessageFormatter, render_message_text};
3use crate::localizer::{ExternalMessage, Localizer};
4
5use std::borrow::Cow;
6
7#[cfg(any(feature = "garde", feature = "validator"))]
8use crate::{
9 Location,
10 de_error::ValidationIssue,
11 localizer::ExternalMessageSource,
12 location::Locations,
13 path_map::{PathMap, format_path_with_resolved_leaf},
14};
15
16#[derive(Debug, Default, Clone, Copy)]
22pub struct DefaultMessageFormatter;
23
24pub type DeveloperMessageFormatter = DefaultMessageFormatter;
26
27#[cfg(any(feature = "garde", feature = "validator"))]
28fn format_validation_issue_lines(
29 l10n: &dyn Localizer,
30 source: &ExternalMessageSource,
31 issues: &[ValidationIssue],
32 locations: &PathMap,
33) -> Vec<String> {
34 let mut lines = Vec::with_capacity(issues.len());
35 for issue in issues {
36 let entry = issue.display_entry_overridden(l10n, (*source).clone());
37 let path_key = &issue.path;
38 let original_leaf = path_key
39 .leaf_string()
40 .unwrap_or_else(|| l10n.root_path_label().into_owned());
41
42 let (locs, resolved_leaf) = locations
43 .search_with_ancestor_fallback(path_key)
44 .unwrap_or((Locations::UNKNOWN, original_leaf));
45
46 let loc = if locs.reference_location == Location::UNKNOWN {
47 locs.defined_location
48 } else {
49 locs.reference_location
50 };
51
52 let resolved_path = format_path_with_resolved_leaf(path_key, &resolved_leaf);
53
54 lines.push(l10n.validation_issue_line(
55 &resolved_path,
56 &entry,
57 (loc != Location::UNKNOWN).then_some(loc),
58 ));
59 }
60 lines
61}
62
63#[cfg(any(feature = "garde", feature = "validator"))]
64fn default_validation_issue_lines(l10n: &dyn Localizer, err: &Error) -> Option<Vec<String>> {
65 let Error::ValidationError {
66 source,
67 issues,
68 locations,
69 } = err
70 else {
71 return None;
72 };
73 Some(format_validation_issue_lines(
74 l10n,
75 &source.external_message_source(),
76 issues,
77 locations,
78 ))
79}
80
81fn default_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
82 match err {
83 Error::WithSnippet { error, .. } => default_format_message(formatter, error),
84 Error::ExternalMessage {
85 source,
86 msg,
87 code,
88 params,
89 ..
90 } => {
91 let l10n = formatter.localizer();
92 l10n.override_external_message(ExternalMessage {
93 source: source.as_ref().clone(),
94 original: msg.as_str(),
95 code: code.as_deref(),
96 params,
97 })
98 .unwrap_or(Cow::Borrowed(msg.as_str()))
99 }
100 Error::Message { msg, .. }
101 | Error::InvalidOptions { msg, .. }
102 | Error::HookError { msg, .. }
103 | Error::SerdeVariantId { msg, .. } => Cow::Borrowed(msg.as_str()),
104 Error::UnresolvedProperty { name, .. } => Cow::Owned(format!("missing property `{name}`")),
105 Error::InvalidPropertyName { name, .. } => Cow::Owned(format!("Invalid name: '{name}'")),
106 Error::PropertyRequiredButUnset { name, message, .. } if message.is_empty() => {
107 Cow::Owned(format!("missing property `{name}`"))
108 }
109 Error::PropertyRequiredButUnset { name, message, .. } => {
110 Cow::Owned(format!("missing property `{name}`: {message}"))
111 }
112 Error::PropertyRequiredButEmpty { name, message, .. } if message.is_empty() => {
113 Cow::Owned(format!("empty property `{name}`"))
114 }
115 Error::PropertyRequiredButEmpty { name, message, .. } => {
116 Cow::Owned(format!("empty property `{name}`: {message}"))
117 }
118 Error::Budget {
119 breach: BudgetBreach::PropertyExpansionDepth { depth, max_depth },
120 ..
121 } => Cow::Owned(format!(
122 "property expansion nesting depth {depth} exceeds limit {max_depth}"
123 )),
124 Error::Budget {
125 breach: BudgetBreach::PropertyInterpolationWork { work, max_work },
126 ..
127 } => Cow::Owned(format!(
128 "property interpolation work {work} exceeds limit {max_work}"
129 )),
130 Error::Eof { .. } => Cow::Borrowed("unexpected end of input"),
131 Error::MultipleDocuments { hint, .. } => {
132 Cow::Owned(format!("multiple YAML documents detected; {hint}"))
133 }
134 Error::Unexpected { expected, .. } => Cow::Owned(format!("expected {expected}")),
135 Error::MergeValueNotMapOrSeqOfMaps { .. } => {
136 Cow::Borrowed("YAML merge value must be mapping or sequence of mappings")
137 }
138 Error::MergeKeyNotAllowed { .. } => {
139 Cow::Borrowed("YAML merge keys are not allowed by configured policy")
140 }
141 Error::UnsupportedTag { tag, .. } => Cow::Owned(format!("unsupported tag `{tag}`")),
142 Error::TaggedStructFieldName { tag, .. } => Cow::Owned(format!(
143 "tag `{tag}` on a struct field name is not allowed by configured policy"
144 )),
145 Error::InvalidBinaryBase64 { .. } => Cow::Borrowed("invalid !!binary base64"),
146 Error::InvalidUtf8Input => Cow::Borrowed("input is not valid UTF-8"),
147 Error::BinaryNotUtf8 { .. } => Cow::Borrowed(
148 "!!binary scalar is not valid UTF-8 so cannot be stored into string. \
149 If you just use !!binary for documentation/annotation, set ignore_binary_tag_for_string in Options",
150 ),
151 Error::TaggedScalarCannotDeserializeIntoString { .. } => {
152 Cow::Borrowed("cannot deserialize tagged scalar into string")
153 }
154 Error::UnexpectedSequenceEnd { .. } => Cow::Borrowed("unexpected sequence end"),
155 Error::UnexpectedMappingEnd { .. } => Cow::Borrowed("unexpected mapping end"),
156 Error::InvalidBooleanStrict { .. } => {
157 Cow::Borrowed("invalid boolean (strict mode expects true/false)")
158 }
159 Error::InvalidCharNull { .. } => {
160 Cow::Borrowed("invalid char: cannot deserialize null; use Option<char>")
161 }
162 Error::InvalidCharNotSingleScalar { .. } => {
163 Cow::Borrowed("invalid char: expected a single Unicode scalar value")
164 }
165 Error::NullIntoString { .. } => {
166 Cow::Borrowed("cannot deserialize null into string; use Option<String>")
167 }
168 Error::BytesNotSupportedMissingBinaryTag { .. } => {
169 Cow::Borrowed("bytes not supported (missing !!binary tag)")
170 }
171 Error::UnexpectedValueForUnit { .. } => Cow::Borrowed("unexpected value for unit"),
172 Error::ExpectedEmptyMappingForUnitStruct { .. } => {
173 Cow::Borrowed("expected empty mapping for unit struct")
174 }
175 Error::UnexpectedContainerEndWhileSkippingNode { .. } => {
176 Cow::Borrowed("unexpected container end while skipping node")
177 }
178 Error::InternalSeedReusedForMapKey { .. } => {
179 Cow::Borrowed("internal error: seed reused for map key")
180 }
181 Error::ValueRequestedBeforeKey { .. } => Cow::Borrowed("value requested before key"),
182 Error::ExpectedStringKeyForExternallyTaggedEnum { .. } => {
183 Cow::Borrowed("expected string key for externally tagged enum")
184 }
185 Error::ExternallyTaggedEnumExpectedScalarOrMapping { .. } => {
186 Cow::Borrowed("externally tagged enum expected scalar or mapping")
187 }
188 Error::UnexpectedValueForUnitEnumVariant { .. } => {
189 Cow::Borrowed("unexpected value for unit enum variant")
190 }
191 Error::AliasReplayCounterOverflow { .. } => Cow::Borrowed("alias replay counter overflow"),
192 Error::AliasReplayLimitExceeded {
193 total_replayed_events,
194 max_total_replayed_events,
195 ..
196 } => Cow::Owned(format!(
197 "alias replay limit exceeded: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
198 )),
199 Error::AliasExpansionLimitExceeded {
200 anchor_id,
201 expansions,
202 max_expansions_per_anchor,
203 ..
204 } => Cow::Owned(format!(
205 "alias expansion limit exceeded for anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
206 )),
207 Error::AliasReplayStackDepthExceeded {
208 depth, max_depth, ..
209 } => Cow::Owned(format!(
210 "alias replay stack depth exceeded: depth={depth} > {max_depth}"
211 )),
212 Error::FoldedBlockScalarMustIndentContent { .. } => {
213 Cow::Borrowed("folded block scalars must indent their content")
214 }
215 Error::InternalDepthUnderflow { .. } => Cow::Borrowed("internal depth underflow"),
216 Error::InternalRecursionStackEmpty { .. } => {
217 Cow::Borrowed("internal recursion stack empty")
218 }
219 Error::RecursiveReferencesRequireWeakTypes { .. } => {
220 Cow::Borrowed("recursive references require weak recursion types")
221 }
222 Error::InvalidScalar { ty, .. } => Cow::Owned(format!("invalid {ty}")),
223 Error::NonFiniteFloat { value, .. } => Cow::Owned(format!(
224 "non-finite float `{value}` rejected by the configured policy"
225 )),
226 Error::SerdeInvalidType {
227 unexpected,
228 expected,
229 ..
230 } => Cow::Owned(format!("invalid type: {unexpected}, expected {expected}")),
231 Error::SerdeInvalidValue {
232 unexpected,
233 expected,
234 ..
235 } => Cow::Owned(format!("invalid value: {unexpected}, expected {expected}")),
236 Error::SerdeInvalidLength { len, expected, .. } => {
237 Cow::Owned(format!("invalid length {len}, expected {expected}"))
238 }
239 Error::SerdeUnknownVariant {
240 variant, expected, ..
241 } => Cow::Owned(format!(
242 "unknown variant `{variant}`, expected one of {}",
243 expected.join(", ")
244 )),
245 Error::SerdeUnknownField {
246 field, expected, ..
247 } => Cow::Owned(format!(
248 "unknown field `{field}`, expected one of {}",
249 expected.join(", ")
250 )),
251 Error::SerdeMissingField { field, .. } => Cow::Owned(format!("missing field `{field}`")),
252 Error::UnexpectedContainerEndWhileReadingKeyNode { .. } => {
253 Cow::Borrowed("unexpected container end while reading key")
254 }
255 Error::DuplicateMappingKey { key, .. } => match key {
256 Some(k) => Cow::Owned(format!(
257 "duplicate mapping key: {k}, set DuplicateKeyPolicy in Options if acceptable"
258 )),
259 None => Cow::Borrowed(
260 "duplicate mapping key, set DuplicateKeyPolicy in Options if acceptable",
261 ),
262 },
263 Error::TaggedEnumMismatch { tagged, target, .. } => Cow::Owned(format!(
264 "tagged enum `{tagged}` does not match target enum `{target}`",
265 )),
266 Error::ExpectedMappingEndAfterEnumVariantValue { .. } => {
267 Cow::Borrowed("expected end of mapping after enum variant value")
268 }
269 Error::ContainerEndMismatch { .. } => Cow::Borrowed("list or mapping end with no start"),
270 Error::UnknownAnchor { .. } => Cow::Borrowed("alias references unknown anchor"),
271 Error::CyclicInclude { id, stack, .. } => {
272 let mut full_msg = format!("cyclic include detected: {id}");
273 if !stack.is_empty() {
274 full_msg.push_str("\nwhile processing include from ");
275 full_msg.push_str(&stack.join(" -> "));
276 }
277 Cow::Owned(full_msg)
278 }
279 Error::UnsupportedIncludeForm { .. } => {
280 Cow::Borrowed("!include currently only supports the scalar form: !include <path>")
281 }
282 Error::ResolverError {
283 target,
284 error,
285 stack,
286 ..
287 } => {
288 let mut full_msg = format!("failed to resolve include {target:?}");
289 if !stack.is_empty() {
290 full_msg.push_str("\nwhile processing include from ");
291 full_msg.push_str(&stack.join(" -> "));
292 }
293 full_msg.push('\n');
294 let msg = match error {
295 crate::input_source::IncludeResolveError::Io(e) => e.to_string(),
296 crate::input_source::IncludeResolveError::Message(m) => m.clone(),
297 crate::input_source::IncludeResolveError::SizeLimitExceeded(size, limit) => {
298 format!("include size {size} bytes exceeds remaining size limit {limit} bytes")
299 }
300 crate::input_source::IncludeResolveError::FileInclude(problem) => {
301 match &**problem {
302 crate::input_source::ResolveProblem::ResolveFailed {
303 spec,
304 base_dir,
305 err,
306 } => {
307 format!("failed to resolve include '{spec}' from '{base_dir}': {err}")
308 }
309 crate::input_source::ResolveProblem::TargetNotRegularFile { target } => {
310 format!("include target '{target}' is not a regular file")
311 }
312 crate::input_source::ResolveProblem::TargetIsRootFile { spec } => {
313 format!(
314 "include target '{spec}' resolves to the configured root file itself"
315 )
316 }
317 crate::input_source::ResolveProblem::ParentIdNotAbsoluteCanonical {
318 parent_id,
319 } => {
320 format!(
321 "SafeFileResolver expected parent include id to be an absolute canonical path, got '{parent_id}'"
322 )
323 }
324 crate::input_source::ResolveProblem::ParentResolveFailed {
325 parent_id,
326 from_name,
327 err,
328 } => {
329 format!(
330 "failed to resolve parent include source '{parent_id}' (from '{from_name}'): {err}"
331 )
332 }
333 crate::input_source::ResolveProblem::ParentNotRegularFile { parent } => {
334 format!("include parent '{parent}' is not a regular file")
335 }
336 crate::input_source::ResolveProblem::ParentHasNoDirectory { parent } => {
337 format!("include parent '{parent}' does not have a parent directory")
338 }
339 crate::input_source::ResolveProblem::ResolvesOutsideRoot { spec, root } => {
340 format!(
341 "include '{spec}' resolves outside the configured root '{root}'"
342 )
343 }
344 crate::input_source::ResolveProblem::TraversesSymlink { spec } => {
345 format!(
346 "include '{spec}' traverses a symlink, which is disabled by policy"
347 )
348 }
349 crate::input_source::ResolveProblem::AbsolutePathNotAllowed { spec } => {
350 format!("absolute include paths are not allowed: {spec}")
351 }
352 crate::input_source::ResolveProblem::EmptyPath => {
353 "include path must not be empty".to_string()
354 }
355 crate::input_source::ResolveProblem::InvalidExtension { spec } => {
356 format!(
357 "include target '{spec}' does not have a valid YAML extension (.yml or .yaml)"
358 )
359 }
360 crate::input_source::ResolveProblem::HiddenFile { spec } => {
361 format!(
362 "include target '{spec}' is a hidden file, which is not allowed"
363 )
364 }
365 crate::input_source::ResolveProblem::EmptyFragment => {
366 "include fragment must not be empty".to_string()
367 }
368 crate::input_source::ResolveProblem::FragmentContainsHash { spec } => {
369 format!("include fragment must not contain '#': {spec}")
370 }
371 crate::input_source::ResolveProblem::NonUtf8Path => {
372 "include target resolves to a non-UTF-8 path, which is not supported"
373 .to_string()
374 }
375 }
376 }
377 };
378 full_msg.push_str(&msg);
379 Cow::Owned(full_msg)
380 }
381 Error::Budget { breach, .. } => Cow::Owned(format!("budget breached: {breach:?}")),
382 Error::QuotingRequired { value, .. } => {
383 Cow::Owned(format!("The string value [{value}] must be quoted"))
384 }
385 Error::CannotBorrowTransformedString { reason, .. } => Cow::Owned(format!(
386 "input does not contain value verbatim so cannot deserialize into &str ({reason}); use String or Cow<str> instead",
387 )),
388 Error::IndentationError {
389 required, actual, ..
390 } => Cow::Owned(format!(
391 "indentation error: expected {required}, found {actual} spaces"
392 )),
393 Error::IOError { .. } => Cow::Borrowed("IO error"),
394 Error::AliasError { error, .. } => render_message_text(formatter, error),
395
396 #[cfg(any(feature = "garde", feature = "validator"))]
397 Error::ValidationError {
398 source,
399 issues,
400 locations,
401 } => {
402 let l10n = formatter.localizer();
403 Cow::Owned(l10n.join_validation_issues(&format_validation_issue_lines(
404 l10n,
405 &source.external_message_source(),
406 issues,
407 locations,
408 )))
409 }
410 #[cfg(any(feature = "garde", feature = "validator"))]
411 Error::ValidationErrors { errors, .. } => Cow::Owned(
412 formatter
413 .localizer()
414 .validation_failed_documents(errors.len()),
415 ),
416 }
417}
418
419fn builtin_standalone_message<'a>(
420 formatter: &dyn MessageFormatter,
421 err: &'a Error,
422) -> Cow<'a, str> {
423 let message = formatter.format_message(err);
424 match err {
425 Error::IOError { cause } => Cow::Owned(format!("{message}: {cause}")),
426 _ => message,
427 }
428}
429
430impl MessageFormatter for DefaultMessageFormatter {
431 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
432 default_format_message(self, err)
433 }
434
435 fn format_standalone_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
436 builtin_standalone_message(self, err)
437 }
438
439 #[cfg(any(feature = "garde", feature = "validator"))]
440 fn format_validation_issue_lines(&self, err: &Error) -> Option<Vec<String>> {
441 default_validation_issue_lines(self.localizer(), err)
442 }
443}
444
445struct DefaultMessageFormatterWithLocalizer<'a> {
446 localizer: &'a dyn Localizer,
447}
448
449impl MessageFormatter for DefaultMessageFormatterWithLocalizer<'_> {
450 fn localizer(&self) -> &dyn Localizer {
451 self.localizer
452 }
453
454 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
455 default_format_message(self, err)
456 }
457
458 fn format_standalone_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
459 builtin_standalone_message(self, err)
460 }
461
462 #[cfg(any(feature = "garde", feature = "validator"))]
463 fn format_validation_issue_lines(&self, err: &Error) -> Option<Vec<String>> {
464 default_validation_issue_lines(self.localizer(), err)
465 }
466}
467
468impl DefaultMessageFormatter {
469 #[must_use]
475 pub fn with_localizer<'a>(&self, localizer: &'a dyn Localizer) -> impl MessageFormatter + 'a {
476 DefaultMessageFormatterWithLocalizer { localizer }
477 }
478}
479
480fn user_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
481 if let Error::WithSnippet { error, .. } = err {
482 return user_format_message(formatter, error);
483 }
484
485 match err {
486 Error::WithSnippet { .. } => unreachable!(),
488
489 Error::Eof { .. } => Cow::Borrowed("unexpected end of file"),
490 Error::MultipleDocuments { .. } => {
491 Cow::Borrowed("only single YAML document expected but multiple found")
492 }
493 Error::InvalidUtf8Input => Cow::Borrowed("YAML parser input is not valid UTF-8"),
494 Error::BinaryNotUtf8 { .. } => {
495 Cow::Borrowed("!!binary scalar is not valid UTF-8 so cannot be stored into string.")
496 }
497 Error::InvalidBooleanStrict { .. } => {
498 Cow::Borrowed("invalid boolean (true or false expected)")
499 }
500 Error::NullIntoString { .. } | Error::InvalidCharNull { .. } => {
501 Cow::Borrowed("null is not allowed here")
502 }
503 Error::InvalidCharNotSingleScalar { .. } => {
504 Cow::Borrowed("only single character allowed here")
505 }
506 Error::BytesNotSupportedMissingBinaryTag { .. } => Cow::Borrowed("missing !!binary tag"),
507 Error::ExpectedEmptyMappingForUnitStruct { .. } => {
508 Cow::Borrowed("expected empty mapping here")
509 }
510 Error::UnexpectedContainerEndWhileSkippingNode { .. } => {
511 Cow::Borrowed("unexpected container end")
512 }
513 Error::AliasReplayCounterOverflow { .. } => {
514 Cow::Borrowed("YAML document too large or too complex")
515 }
516 Error::AliasReplayLimitExceeded {
517 total_replayed_events,
518 max_total_replayed_events,
519 ..
520 } => Cow::Owned(format!(
521 "YAML document too large or too complex: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
522 )),
523 Error::AliasExpansionLimitExceeded {
524 anchor_id,
525 expansions,
526 max_expansions_per_anchor,
527 ..
528 } => Cow::Owned(format!(
529 "YAML document too large or too complex: anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
530 )),
531 Error::AliasReplayStackDepthExceeded {
532 depth, max_depth, ..
533 } => Cow::Owned(format!(
534 "YAML document too large or too complex: depth={depth} > {max_depth}"
535 )),
536 Error::UnknownAnchor { .. } => Cow::Borrowed("reference to unknown value"),
537 Error::MergeKeyNotAllowed { .. } => Cow::Borrowed("merge key not allowed here"),
538 Error::CyclicInclude { .. } => Cow::Borrowed("cyclic include detected"),
539 Error::UnsupportedIncludeForm { .. } => {
540 Cow::Borrowed("!include currently only supports the scalar form: !include <path>")
541 }
542 Error::ResolverError { .. } => Cow::Borrowed("failed to resolve include"),
543 Error::RecursiveReferencesRequireWeakTypes { .. } => {
544 Cow::Borrowed("Recursive reference not allowed here")
545 }
546 Error::DuplicateMappingKey { key, .. } => match key {
547 Some(k) => Cow::Owned(format!("duplicate mapping key `{k}` is not allowed here")),
548 None => Cow::Borrowed("duplicate mapping key not allowed here"),
549 },
550 Error::QuotingRequired { .. } => Cow::Borrowed("value requires quoting"),
551 Error::Budget {
552 breach: BudgetBreach::PropertyExpansionDepth { depth, max_depth },
553 ..
554 } => Cow::Owned(format!(
555 "YAML document too large or too complex: property expansion nesting depth {depth} exceeds limit {max_depth}"
556 )),
557 Error::Budget {
558 breach: BudgetBreach::PropertyInterpolationWork { work, max_work },
559 ..
560 } => Cow::Owned(format!(
561 "YAML document too large or too complex: property interpolation work {work} exceeds limit {max_work}"
562 )),
563 Error::Budget { breach, .. } => Cow::Owned(format!(
564 "YAML document too large or too complex: limits breached: {breach:?}"
565 )),
566 Error::CannotBorrowTransformedString { .. } => {
567 Cow::Borrowed("Only single string with no escape sequences is allowed here")
568 }
569 Error::IndentationError {
570 required, actual, ..
571 } => Cow::Owned(format!(
572 "incorrect indentation: expected {required}, found {actual} spaces"
573 )),
574 Error::NonFiniteFloat { value, .. } => {
575 Cow::Owned(format!("value `{value}` is not a finite number"))
576 }
577
578 _ => default_format_message(formatter, err),
580 }
581}
582
583impl MessageFormatter for UserMessageFormatter {
584 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
585 user_format_message(self, err)
586 }
587
588 fn format_standalone_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
589 builtin_standalone_message(self, err)
590 }
591
592 #[cfg(any(feature = "garde", feature = "validator"))]
593 fn format_validation_issue_lines(&self, err: &Error) -> Option<Vec<String>> {
594 default_validation_issue_lines(self.localizer(), err)
595 }
596}
597
598struct UserMessageFormatterWithLocalizer<'a> {
599 localizer: &'a dyn Localizer,
600}
601
602impl MessageFormatter for UserMessageFormatterWithLocalizer<'_> {
603 fn localizer(&self) -> &dyn Localizer {
604 self.localizer
605 }
606
607 fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
608 user_format_message(self, err)
609 }
610
611 fn format_standalone_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
612 builtin_standalone_message(self, err)
613 }
614
615 #[cfg(any(feature = "garde", feature = "validator"))]
616 fn format_validation_issue_lines(&self, err: &Error) -> Option<Vec<String>> {
617 default_validation_issue_lines(self.localizer(), err)
618 }
619}
620
621impl UserMessageFormatter {
622 #[must_use]
628 pub fn with_localizer<'a>(&self, localizer: &'a dyn Localizer) -> impl MessageFormatter + 'a {
629 UserMessageFormatterWithLocalizer { localizer }
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use crate::Location;
637 use crate::de_error::{Error, MessageFormatter, TransformReason};
638 use crate::location::Locations;
639
640 fn loc() -> Location {
641 Location::UNKNOWN
642 }
643
644 #[rstest::rstest]
649 #[case::with_snippet_delegates(
650 Error::WithSnippet {
651 regions: vec![],
652 crop_radius: 3,
653 error: Box::new(Error::Eof { location: loc() }),
654 },
655 "unexpected end of input"
656 )]
657 #[case::hook_error(
658 Error::HookError { msg: "hook msg".to_owned(), location: loc() },
659 "hook msg"
660 )]
661 #[case::serde_variant_id(
662 Error::SerdeVariantId { msg: "variant id msg".to_owned(), location: loc() },
663 "variant id msg"
664 )]
665 #[case::invalid_binary_base64(
666 Error::InvalidBinaryBase64 { location: loc() },
667 "invalid !!binary base64"
668 )]
669 #[case::merge_key_not_allowed(
670 Error::MergeKeyNotAllowed { location: loc() },
671 "YAML merge keys are not allowed by configured policy"
672 )]
673 #[case::unexpected_sequence_end(
674 Error::UnexpectedSequenceEnd { location: loc() },
675 "unexpected sequence end"
676 )]
677 #[case::unexpected_mapping_end(
678 Error::UnexpectedMappingEnd { location: loc() },
679 "unexpected mapping end"
680 )]
681 #[case::unexpected_container_end_while_skipping(
682 Error::UnexpectedContainerEndWhileSkippingNode { location: loc() },
683 "unexpected container end while skipping node"
684 )]
685 #[case::internal_seed_reused(
686 Error::InternalSeedReusedForMapKey { location: loc() },
687 "internal error: seed reused for map key"
688 )]
689 #[case::value_requested_before_key(
690 Error::ValueRequestedBeforeKey { location: loc() },
691 "value requested before key"
692 )]
693 #[case::alias_replay_counter_overflow(
694 Error::AliasReplayCounterOverflow { location: loc() },
695 "alias replay counter overflow"
696 )]
697 #[case::folded_block_scalar(
698 Error::FoldedBlockScalarMustIndentContent { location: loc() },
699 "folded block scalars must indent their content"
700 )]
701 #[case::internal_depth_underflow(
702 Error::InternalDepthUnderflow { location: loc() },
703 "internal depth underflow"
704 )]
705 #[case::internal_recursion_stack_empty(
706 Error::InternalRecursionStackEmpty { location: loc() },
707 "internal recursion stack empty"
708 )]
709 #[case::recursive_references_require_weak_types(
710 Error::RecursiveReferencesRequireWeakTypes { location: loc() },
711 "recursive references require weak recursion types"
712 )]
713 #[case::unexpected_container_end_while_reading_key(
714 Error::UnexpectedContainerEndWhileReadingKeyNode { location: loc() },
715 "unexpected container end while reading key"
716 )]
717 #[case::expected_mapping_end_after_enum_variant(
718 Error::ExpectedMappingEndAfterEnumVariantValue { location: loc() },
719 "expected end of mapping after enum variant value"
720 )]
721 #[case::container_end_mismatch(
722 Error::ContainerEndMismatch { location: loc() },
723 "list or mapping end with no start"
724 )]
725 #[case::unresolved_property(
726 Error::UnresolvedProperty { name: "MISSING".to_owned(), location: loc() },
727 "missing property `MISSING`"
728 )]
729 #[case::invalid_property_name(
730 Error::InvalidPropertyName { name: "${ab-cd}".to_owned(), location: loc() },
731 "Invalid name: '${ab-cd}'"
732 )]
733 #[case::property_expansion_depth_limit(
734 Error::Budget {
735 breach: BudgetBreach::PropertyExpansionDepth {
736 depth: 65,
737 max_depth: 64,
738 },
739 location: loc(),
740 },
741 "property expansion nesting depth 65 exceeds limit 64"
742 )]
743 #[case::property_expansion_work_limit(
744 Error::Budget {
745 breach: BudgetBreach::PropertyInterpolationWork {
746 work: 101,
747 max_work: 100,
748 },
749 location: loc(),
750 },
751 "property interpolation work 101 exceeds limit 100"
752 )]
753 #[case::io_error(
754 Error::IOError { cause: std::io::Error::other("disk full") },
755 "IO error"
756 )]
757 fn default_exact_messages(#[case] err: Error, #[case] expected: &str) {
758 let formatter = DefaultMessageFormatter;
759 assert_eq!(formatter.format_message(&err), expected);
760 }
761
762 #[rstest::rstest]
763 #[case::serde_invalid_value(
764 Error::SerdeInvalidValue {
765 unexpected: "null".to_owned(),
766 expected: "string".to_owned(),
767 location: loc(),
768 },
769 &["invalid value", "null", "string"]
770 )]
771 #[case::serde_unknown_variant(
772 Error::SerdeUnknownVariant {
773 variant: "foo".to_owned(),
774 expected: vec!["bar", "baz"],
775 location: loc(),
776 },
777 &["unknown variant", "foo"]
778 )]
779 #[case::serde_unknown_field(
780 Error::SerdeUnknownField {
781 field: "xyz".to_owned(),
782 expected: vec!["a", "b"],
783 location: loc(),
784 },
785 &["unknown field", "xyz"]
786 )]
787 fn default_contains_messages(#[case] err: Error, #[case] needles: &[&str]) {
788 let formatter = DefaultMessageFormatter;
789 let msg = formatter.format_message(&err);
790 for needle in needles {
791 assert!(msg.contains(needle), "got: {msg}, missing: {needle}");
792 }
793 }
794
795 #[test]
796 fn default_resolver_messages_preserve_include_context_and_specific_cause() {
797 use crate::input_source::{IncludeResolveError, ResolveProblem};
798
799 let file_problems = [
800 (
801 ResolveProblem::ResolveFailed {
802 spec: "child.yaml".to_owned(),
803 base_dir: "/config".to_owned(),
804 err: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
805 },
806 "failed to resolve include 'child.yaml' from '/config': missing",
807 ),
808 (
809 ResolveProblem::TargetNotRegularFile {
810 target: "/config/child.yaml".to_owned(),
811 },
812 "include target '/config/child.yaml' is not a regular file",
813 ),
814 (
815 ResolveProblem::TargetIsRootFile {
816 spec: "root.yaml".to_owned(),
817 },
818 "include target 'root.yaml' resolves to the configured root file itself",
819 ),
820 (
821 ResolveProblem::ParentIdNotAbsoluteCanonical {
822 parent_id: "relative/root.yaml".to_owned(),
823 },
824 "expected parent include id to be an absolute canonical path",
825 ),
826 (
827 ResolveProblem::ParentResolveFailed {
828 parent_id: "/config/root.yaml".to_owned(),
829 from_name: "root.yaml".to_owned(),
830 err: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
831 },
832 "failed to resolve parent include source '/config/root.yaml' (from 'root.yaml'): denied",
833 ),
834 (
835 ResolveProblem::ParentNotRegularFile {
836 parent: "/config".to_owned(),
837 },
838 "include parent '/config' is not a regular file",
839 ),
840 (
841 ResolveProblem::ParentHasNoDirectory {
842 parent: "root.yaml".to_owned(),
843 },
844 "include parent 'root.yaml' does not have a parent directory",
845 ),
846 (
847 ResolveProblem::ResolvesOutsideRoot {
848 spec: "../secret.yaml".to_owned(),
849 root: "/config".to_owned(),
850 },
851 "include '../secret.yaml' resolves outside the configured root '/config'",
852 ),
853 (
854 ResolveProblem::TraversesSymlink {
855 spec: "linked.yaml".to_owned(),
856 },
857 "include 'linked.yaml' traverses a symlink",
858 ),
859 (
860 ResolveProblem::AbsolutePathNotAllowed {
861 spec: "/etc/secret.yaml".to_owned(),
862 },
863 "absolute include paths are not allowed: /etc/secret.yaml",
864 ),
865 (ResolveProblem::EmptyPath, "include path must not be empty"),
866 (
867 ResolveProblem::InvalidExtension {
868 spec: "child.txt".to_owned(),
869 },
870 "include target 'child.txt' does not have a valid YAML extension",
871 ),
872 (
873 ResolveProblem::HiddenFile {
874 spec: ".secret.yaml".to_owned(),
875 },
876 "include target '.secret.yaml' is a hidden file",
877 ),
878 (
879 ResolveProblem::EmptyFragment,
880 "include fragment must not be empty",
881 ),
882 (
883 ResolveProblem::FragmentContainsHash {
884 spec: "child.yaml#one#two".to_owned(),
885 },
886 "include fragment must not contain '#': child.yaml#one#two",
887 ),
888 (
889 ResolveProblem::NonUtf8Path,
890 "include target resolves to a non-UTF-8 path",
891 ),
892 ];
893
894 for (problem, expected_cause) in file_problems {
895 let error = Error::ResolverError {
896 target: "child.yaml".to_owned(),
897 error: IncludeResolveError::FileInclude(Box::new(problem)),
898 stack: vec!["root.yaml".to_owned(), "parent.yaml".to_owned()],
899 location: loc(),
900 };
901 let message = DefaultMessageFormatter.format_message(&error);
902 assert!(
903 message.starts_with("failed to resolve include \"child.yaml\""),
904 "got: {message}"
905 );
906 assert!(
907 message.contains("while processing include from root.yaml -> parent.yaml"),
908 "got: {message}"
909 );
910 assert!(message.contains(expected_cause), "got: {message}");
911 }
912
913 let direct_causes = [
914 (
915 IncludeResolveError::Io(std::io::Error::other("reader failed")),
916 "reader failed",
917 ),
918 (
919 IncludeResolveError::Message("resolver refused".to_owned()),
920 "resolver refused",
921 ),
922 (
923 IncludeResolveError::SizeLimitExceeded(17, 16),
924 "include size 17 bytes exceeds remaining size limit 16 bytes",
925 ),
926 ];
927 for (cause, expected_cause) in direct_causes {
928 let error = Error::ResolverError {
929 target: "child.yaml".to_owned(),
930 error: cause,
931 stack: Vec::new(),
932 location: loc(),
933 };
934 let message = DefaultMessageFormatter.format_message(&error);
935 assert!(message.contains(expected_cause), "got: {message}");
936 assert!(!message.contains("while processing include from"));
937 }
938 }
939
940 #[test]
941 fn default_cyclic_include_message_only_adds_a_nonempty_stack() {
942 let without_stack = Error::CyclicInclude {
943 id: "root.yaml".to_owned(),
944 stack: Vec::new(),
945 location: loc(),
946 };
947 assert_eq!(
948 DefaultMessageFormatter.format_message(&without_stack),
949 "cyclic include detected: root.yaml"
950 );
951
952 let with_stack = Error::CyclicInclude {
953 id: "root.yaml".to_owned(),
954 stack: vec!["root.yaml".to_owned(), "child.yaml".to_owned()],
955 location: loc(),
956 };
957 assert_eq!(
958 DefaultMessageFormatter.format_message(&with_stack),
959 "cyclic include detected: root.yaml\nwhile processing include from root.yaml -> child.yaml"
960 );
961 }
962
963 #[rstest::rstest]
964 #[case::unset_with_message(
965 Error::PropertyRequiredButUnset {
966 name: "DB_HOST".to_owned(),
967 message: "set DB_HOST in .env".to_owned(),
968 location: loc(),
969 },
970 "missing property `DB_HOST`: set DB_HOST in .env",
971 )]
972 #[case::unset_empty_message(
973 Error::PropertyRequiredButUnset {
974 name: "DB_HOST".to_owned(),
975 message: String::new(),
976 location: loc(),
977 },
978 "missing property `DB_HOST`",
979 )]
980 #[case::empty_with_message(
981 Error::PropertyRequiredButEmpty {
982 name: "DB_HOST".to_owned(),
983 message: "must not be blank".to_owned(),
984 location: loc(),
985 },
986 "empty property `DB_HOST`: must not be blank",
987 )]
988 #[case::empty_empty_message(
989 Error::PropertyRequiredButEmpty {
990 name: "DB_HOST".to_owned(),
991 message: String::new(),
992 location: loc(),
993 },
994 "empty property `DB_HOST`",
995 )]
996 fn default_property_required_messages(#[case] err: Error, #[case] expected: &str) {
997 let formatter = DefaultMessageFormatter;
998 assert_eq!(formatter.format_message(&err), expected);
999 }
1000
1001 #[test]
1002 #[allow(deprecated)] fn default_alias_error_both_unknown() {
1004 let formatter = DefaultMessageFormatter;
1005 let err = Error::AliasError {
1006 msg: "alias msg".to_owned(),
1007 error: Box::new(Error::msg("alias msg")),
1008 locations: Locations::UNKNOWN,
1009 };
1010 assert_eq!(formatter.format_message(&err), "alias msg");
1011 }
1012
1013 #[test]
1014 #[allow(deprecated)] fn default_alias_error_ref_known_def_unknown() {
1016 let formatter = DefaultMessageFormatter;
1017 let ref_loc = Location::new(1, 0);
1018 let err = Error::AliasError {
1019 msg: "alias msg".to_owned(),
1020 error: Box::new(Error::msg("alias msg")),
1021 locations: Locations {
1022 reference_location: ref_loc,
1023 defined_location: Location::UNKNOWN,
1024 },
1025 };
1026 assert_eq!(formatter.format_message(&err), "alias msg");
1027 }
1028
1029 #[test]
1030 #[allow(deprecated)] fn default_alias_error_both_known_different() {
1032 let formatter = DefaultMessageFormatter;
1033 let ref_loc = Location::new(1, 0);
1034 let def_loc = Location::new(5, 0);
1035 let err = Error::AliasError {
1036 msg: "alias msg".to_owned(),
1037 error: Box::new(Error::msg("alias msg")),
1038 locations: Locations {
1039 reference_location: ref_loc,
1040 defined_location: def_loc,
1041 },
1042 };
1043 assert_eq!(formatter.format_message(&err), "alias msg");
1045 }
1046
1047 #[rstest::rstest]
1052 #[case::with_snippet_delegates(
1053 Error::WithSnippet {
1054 regions: vec![],
1055 crop_radius: 3,
1056 error: Box::new(Error::Eof { location: loc() }),
1057 },
1058 "unexpected end of file"
1059 )]
1060 #[case::eof(Error::Eof { location: loc() }, "unexpected end of file")]
1061 #[case::multiple_documents(
1062 Error::MultipleDocuments { hint: "use from_str_multidoc", location: loc() },
1063 "only single YAML document expected but multiple found"
1064 )]
1065 #[case::invalid_utf8_input(Error::InvalidUtf8Input, "YAML parser input is not valid UTF-8")]
1066 #[case::invalid_boolean_strict(
1067 Error::InvalidBooleanStrict { location: loc() },
1068 "invalid boolean (true or false expected)"
1069 )]
1070 #[case::non_finite_float(
1071 Error::NonFiniteFloat { value: ".inf".to_owned(), location: loc() },
1072 "value `.inf` is not a finite number"
1073 )]
1074 #[case::null_into_string(
1075 Error::NullIntoString { location: loc() },
1076 "null is not allowed here"
1077 )]
1078 #[case::invalid_char_null(
1079 Error::InvalidCharNull { location: loc() },
1080 "null is not allowed here"
1081 )]
1082 #[case::invalid_char_not_single_scalar(
1083 Error::InvalidCharNotSingleScalar { location: loc() },
1084 "only single character allowed here"
1085 )]
1086 #[case::bytes_not_supported_missing_binary_tag(
1087 Error::BytesNotSupportedMissingBinaryTag { location: loc() },
1088 "missing !!binary tag"
1089 )]
1090 #[case::expected_empty_mapping_for_unit_struct(
1091 Error::ExpectedEmptyMappingForUnitStruct { location: loc() },
1092 "expected empty mapping here"
1093 )]
1094 #[case::unexpected_container_end_while_skipping(
1095 Error::UnexpectedContainerEndWhileSkippingNode { location: loc() },
1096 "unexpected container end"
1097 )]
1098 #[case::alias_replay_counter_overflow(
1099 Error::AliasReplayCounterOverflow { location: loc() },
1100 "YAML document too large or too complex"
1101 )]
1102 #[case::unknown_anchor(
1103 Error::UnknownAnchor { location: loc() },
1104 "reference to unknown value"
1105 )]
1106 #[case::merge_key_not_allowed(
1107 Error::MergeKeyNotAllowed { location: loc() },
1108 "merge key not allowed here"
1109 )]
1110 #[case::recursive_references_require_weak_types(
1111 Error::RecursiveReferencesRequireWeakTypes { location: loc() },
1112 "Recursive reference not allowed here"
1113 )]
1114 #[case::quoting_required(
1115 Error::QuotingRequired { value: "yes".to_owned(), location: loc() },
1116 "value requires quoting"
1117 )]
1118 #[case::cannot_borrow_transformed_string(
1119 Error::CannotBorrowTransformedString {
1120 reason: TransformReason::EscapeSequence,
1121 location: loc(),
1122 },
1123 "Only single string with no escape sequences is allowed here"
1124 )]
1125 #[case::indentation_error(
1126 Error::IndentationError {
1127 required: crate::indentation::RequireIndent::Divisible(4),
1128 actual: 6,
1129 location: loc(),
1130 },
1131 "incorrect indentation: expected divisible by 4, found 6 spaces"
1132 )]
1133 fn user_exact_messages(#[case] err: Error, #[case] expected: &str) {
1134 let formatter = UserMessageFormatter;
1135 assert_eq!(formatter.format_message(&err), expected);
1136 }
1137
1138 #[rstest::rstest]
1139 #[case::binary_not_utf8(Error::BinaryNotUtf8 { location: loc() }, &["!!binary"])]
1140 #[case::alias_replay_limit_exceeded(
1141 Error::AliasReplayLimitExceeded {
1142 total_replayed_events: 1000,
1143 max_total_replayed_events: 500,
1144 location: loc(),
1145 },
1146 &["too large or too complex", "1000"]
1147 )]
1148 #[case::alias_expansion_limit_exceeded(
1149 Error::AliasExpansionLimitExceeded {
1150 anchor_id: 7,
1151 expansions: 200,
1152 max_expansions_per_anchor: 100,
1153 location: loc(),
1154 },
1155 &["too large or too complex", "7"]
1156 )]
1157 #[case::alias_replay_stack_depth_exceeded(
1158 Error::AliasReplayStackDepthExceeded {
1159 depth: 50,
1160 max_depth: 20,
1161 location: loc(),
1162 },
1163 &["too large or too complex", "50"]
1164 )]
1165 #[case::duplicate_mapping_key_with_key(
1166 Error::DuplicateMappingKey { key: Some("mykey".to_owned()), location: loc() },
1167 &["mykey", "duplicate"]
1168 )]
1169 #[case::duplicate_mapping_key_without_key(
1170 Error::DuplicateMappingKey { key: None, location: loc() },
1171 &["duplicate"]
1172 )]
1173 #[case::budget(
1174 Error::Budget {
1175 breach: crate::budget::BudgetBreach::Events { events: 9999 },
1176 location: loc(),
1177 },
1178 &["too large or too complex"]
1179 )]
1180 #[case::falls_through_to_default_for_unhandled(
1181 Error::SerdeInvalidType {
1182 unexpected: "seq".to_owned(),
1183 expected: "map".to_owned(),
1184 location: loc(),
1185 },
1186 &["invalid type"]
1187 )]
1188 fn user_contains_messages(#[case] err: Error, #[case] needles: &[&str]) {
1189 let formatter = UserMessageFormatter;
1190 let msg = formatter.format_message(&err);
1191 for needle in needles {
1192 assert!(msg.contains(needle), "got: {msg}, missing: {needle}");
1193 }
1194 }
1195
1196 #[test]
1201 fn user_with_localizer_delegates() {
1202 use crate::localizer::DefaultEnglishLocalizer;
1203 let localizer = DefaultEnglishLocalizer;
1204 let formatter = UserMessageFormatter.with_localizer(&localizer);
1205 let err = Error::Eof { location: loc() };
1206 assert_eq!(formatter.format_message(&err), "unexpected end of file");
1207 assert_eq!(formatter.localizer().root_path_label(), "<root>");
1208 }
1209
1210 #[test]
1215 fn default_with_localizer_delegates() {
1216 use crate::localizer::DefaultEnglishLocalizer;
1217 let localizer = DefaultEnglishLocalizer;
1218 let formatter = DefaultMessageFormatter.with_localizer(&localizer);
1219 let err = Error::Eof { location: loc() };
1220 assert_eq!(formatter.format_message(&err), "unexpected end of input");
1221 assert_eq!(formatter.localizer().root_path_label(), "<root>");
1222 }
1223
1224 #[cfg(any(feature = "garde", feature = "validator"))]
1225 #[test]
1226 fn validation_message_prefers_reference_location() {
1227 use crate::de_error::{ValidationIssue, ValidationSource};
1228 use crate::path_map::{PathKey, PathMap};
1229
1230 let path = PathKey::new().join_key("name");
1231 let mut locations = PathMap::new();
1232 locations.insert(
1233 path.clone(),
1234 Locations {
1235 reference_location: Location::new(7, 8),
1236 defined_location: Location::new(2, 3),
1237 },
1238 );
1239 let error = Error::ValidationError {
1240 source: ValidationSource::Validator,
1241 issues: vec![ValidationIssue::new(path, "length").with_message("too short")],
1242 locations,
1243 };
1244
1245 assert_eq!(
1246 DefaultMessageFormatter.format_message(&error),
1247 "validation error at name: too short at line 7, column 8"
1248 );
1249 }
1250}