Skip to main content

serde_saphyr/de/
message_formatters.rs

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, localizer::ExternalMessageSource, path_map::format_path_with_resolved_leaf,
10};
11
12/// Default developer-oriented message formatter.
13///
14/// This formatter at places produces recommendations on how to adjust settings and API
15/// calls for the parsing to work, so normally should not be user-facing. Use UserMessageFormatter
16/// for user-facing content, or implement custom MessageFormatter for full control over output.
17#[derive(Debug, Default, Clone, Copy)]
18pub struct DefaultMessageFormatter;
19
20/// Alias for the default developer-oriented formatter.
21pub type DeveloperMessageFormatter = DefaultMessageFormatter;
22
23fn default_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
24    match err {
25        Error::WithSnippet { error, .. } => default_format_message(formatter, error),
26        Error::ExternalMessage {
27            source,
28            msg,
29            code,
30            params,
31            ..
32        } => {
33            let l10n = formatter.localizer();
34            l10n.override_external_message(ExternalMessage {
35                source: *source,
36                original: msg.as_str(),
37                code: code.as_deref(),
38                params,
39            })
40            .unwrap_or(Cow::Borrowed(msg.as_str()))
41        }
42        Error::Message { msg, .. }
43        | Error::HookError { msg, .. }
44        | Error::SerdeVariantId { msg, .. } => Cow::Borrowed(msg.as_str()),
45        Error::UnresolvedProperty { name, .. } => Cow::Owned(format!("missing property `{name}`")),
46        Error::InvalidPropertyName { name, .. } => Cow::Owned(format!("Invalid name: '{name}'")),
47        Error::Eof { .. } => Cow::Borrowed("unexpected end of input"),
48        Error::MultipleDocuments { hint, .. } => {
49            Cow::Owned(format!("multiple YAML documents detected; {hint}"))
50        }
51        Error::Unexpected { expected, .. } => {
52            Cow::Owned(format!("unexpected event: expected {expected}"))
53        }
54        Error::MergeValueNotMapOrSeqOfMaps { .. } => {
55            Cow::Borrowed("YAML merge value must be mapping or sequence of mappings")
56        }
57        Error::MergeKeyNotAllowed { .. } => {
58            Cow::Borrowed("YAML merge keys are not allowed by configured policy")
59        }
60        Error::InvalidBinaryBase64 { .. } => Cow::Borrowed("invalid !!binary base64"),
61        Error::InvalidUtf8Input => Cow::Borrowed("input is not valid UTF-8"),
62        Error::BinaryNotUtf8 { .. } => Cow::Borrowed(
63            "!!binary scalar is not valid UTF-8 so cannot be stored into string. \
64                 If you just use !!binary for documentation/annotation, set ignore_binary_tag_for_string in Options",
65        ),
66        Error::TaggedScalarCannotDeserializeIntoString { .. } => {
67            Cow::Borrowed("cannot deserialize tagged scalar into string")
68        }
69        Error::UnexpectedSequenceEnd { .. } => Cow::Borrowed("unexpected sequence end"),
70        Error::UnexpectedMappingEnd { .. } => Cow::Borrowed("unexpected mapping end"),
71        Error::InvalidBooleanStrict { .. } => {
72            Cow::Borrowed("invalid boolean (strict mode expects true/false)")
73        }
74        Error::InvalidCharNull { .. } => {
75            Cow::Borrowed("invalid char: cannot deserialize null; use Option<char>")
76        }
77        Error::InvalidCharNotSingleScalar { .. } => {
78            Cow::Borrowed("invalid char: expected a single Unicode scalar value")
79        }
80        Error::NullIntoString { .. } => {
81            Cow::Borrowed("cannot deserialize null into string; use Option<String>")
82        }
83        Error::BytesNotSupportedMissingBinaryTag { .. } => {
84            Cow::Borrowed("bytes not supported (missing !!binary tag)")
85        }
86        Error::UnexpectedValueForUnit { .. } => Cow::Borrowed("unexpected value for unit"),
87        Error::ExpectedEmptyMappingForUnitStruct { .. } => {
88            Cow::Borrowed("expected empty mapping for unit struct")
89        }
90        Error::UnexpectedContainerEndWhileSkippingNode { .. } => {
91            Cow::Borrowed("unexpected container end while skipping node")
92        }
93        Error::InternalSeedReusedForMapKey { .. } => {
94            Cow::Borrowed("internal error: seed reused for map key")
95        }
96        Error::ValueRequestedBeforeKey { .. } => Cow::Borrowed("value requested before key"),
97        Error::ExpectedStringKeyForExternallyTaggedEnum { .. } => {
98            Cow::Borrowed("expected string key for externally tagged enum")
99        }
100        Error::ExternallyTaggedEnumExpectedScalarOrMapping { .. } => {
101            Cow::Borrowed("externally tagged enum expected scalar or mapping")
102        }
103        Error::UnexpectedValueForUnitEnumVariant { .. } => {
104            Cow::Borrowed("unexpected value for unit enum variant")
105        }
106        Error::AliasReplayCounterOverflow { .. } => Cow::Borrowed("alias replay counter overflow"),
107        Error::AliasReplayLimitExceeded {
108            total_replayed_events,
109            max_total_replayed_events,
110            ..
111        } => Cow::Owned(format!(
112            "alias replay limit exceeded: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
113        )),
114        Error::AliasExpansionLimitExceeded {
115            anchor_id,
116            expansions,
117            max_expansions_per_anchor,
118            ..
119        } => Cow::Owned(format!(
120            "alias expansion limit exceeded for anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
121        )),
122        Error::AliasReplayStackDepthExceeded {
123            depth, max_depth, ..
124        } => Cow::Owned(format!(
125            "alias replay stack depth exceeded: depth={depth} > {max_depth}"
126        )),
127        Error::FoldedBlockScalarMustIndentContent { .. } => {
128            Cow::Borrowed("folded block scalars must indent their content")
129        }
130        Error::InternalDepthUnderflow { .. } => Cow::Borrowed("internal depth underflow"),
131        Error::InternalRecursionStackEmpty { .. } => {
132            Cow::Borrowed("internal recursion stack empty")
133        }
134        Error::RecursiveReferencesRequireWeakTypes { .. } => {
135            Cow::Borrowed("recursive references require weak recursion types")
136        }
137        Error::InvalidScalar { ty, .. } => Cow::Owned(format!("invalid {ty}")),
138        Error::SerdeInvalidType {
139            unexpected,
140            expected,
141            ..
142        } => Cow::Owned(format!("invalid type: {unexpected}, expected {expected}")),
143        Error::SerdeInvalidValue {
144            unexpected,
145            expected,
146            ..
147        } => Cow::Owned(format!("invalid value: {unexpected}, expected {expected}")),
148        Error::SerdeUnknownVariant {
149            variant, expected, ..
150        } => Cow::Owned(format!(
151            "unknown variant `{variant}`, expected one of {}",
152            expected.join(", ")
153        )),
154        Error::SerdeUnknownField {
155            field, expected, ..
156        } => Cow::Owned(format!(
157            "unknown field `{field}`, expected one of {}",
158            expected.join(", ")
159        )),
160        Error::SerdeMissingField { field, .. } => Cow::Owned(format!("missing field `{field}`")),
161        Error::UnexpectedContainerEndWhileReadingKeyNode { .. } => {
162            Cow::Borrowed("unexpected container end while reading key")
163        }
164        Error::DuplicateMappingKey { key, .. } => match key {
165            Some(k) => Cow::Owned(format!(
166                "duplicate mapping key: {k}, set DuplicateKeyPolicy in Options if acceptable"
167            )),
168            None => Cow::Borrowed(
169                "duplicate mapping key, set DuplicateKeyPolicy in Options if acceptable",
170            ),
171        },
172        Error::TaggedEnumMismatch { tagged, target, .. } => Cow::Owned(format!(
173            "tagged enum `{tagged}` does not match target enum `{target}`",
174        )),
175        Error::ExpectedMappingEndAfterEnumVariantValue { .. } => {
176            Cow::Borrowed("expected end of mapping after enum variant value")
177        }
178        Error::ContainerEndMismatch { .. } => Cow::Borrowed("list or mapping end with no start"),
179        Error::UnknownAnchor { .. } => Cow::Borrowed("alias references unknown anchor"),
180        Error::CyclicInclude { id, stack, .. } => {
181            let mut full_msg = format!("cyclic include detected: {id}");
182            if !stack.is_empty() {
183                full_msg.push_str("\nwhile processing include from ");
184                full_msg.push_str(&stack.join(" -> "));
185            }
186            Cow::Owned(full_msg)
187        }
188        Error::UnsupportedIncludeForm { .. } => {
189            Cow::Borrowed("!include currently only supports the scalar form: !include <path>")
190        }
191        Error::ResolverError {
192            target,
193            error,
194            stack,
195            ..
196        } => {
197            let mut full_msg = format!("failed to resolve include {target:?}");
198            if !stack.is_empty() {
199                full_msg.push_str("\nwhile processing include from ");
200                full_msg.push_str(&stack.join(" -> "));
201            }
202            full_msg.push('\n');
203            let msg = match error {
204                crate::input_source::IncludeResolveError::Io(e) => e.to_string(),
205                crate::input_source::IncludeResolveError::Message(m) => m.clone(),
206                crate::input_source::IncludeResolveError::SizeLimitExceeded(size, limit) => {
207                    format!("include size {size} bytes exceeds remaining size limit {limit} bytes")
208                }
209                crate::input_source::IncludeResolveError::FileInclude(problem) => {
210                    match &**problem {
211                        crate::input_source::ResolveProblem::ResolveFailed {
212                            spec,
213                            base_dir,
214                            err,
215                        } => {
216                            format!(
217                                "failed to resolve include '{}' from '{}': {}",
218                                spec, base_dir, err
219                            )
220                        }
221                        crate::input_source::ResolveProblem::TargetNotRegularFile { target } => {
222                            format!("include target '{}' is not a regular file", target)
223                        }
224                        crate::input_source::ResolveProblem::TargetIsRootFile { spec } => {
225                            format!(
226                                "include target '{}' resolves to the configured root file itself",
227                                spec
228                            )
229                        }
230                        crate::input_source::ResolveProblem::ParentIdNotAbsoluteCanonical {
231                            parent_id,
232                        } => {
233                            format!(
234                                "SafeFileResolver expected parent include id to be an absolute canonical path, got '{}'",
235                                parent_id
236                            )
237                        }
238                        crate::input_source::ResolveProblem::ParentResolveFailed {
239                            parent_id,
240                            from_name,
241                            err,
242                        } => {
243                            format!(
244                                "failed to resolve parent include source '{}' (from '{}'): {}",
245                                parent_id, from_name, err
246                            )
247                        }
248                        crate::input_source::ResolveProblem::ParentNotRegularFile { parent } => {
249                            format!("include parent '{}' is not a regular file", parent)
250                        }
251                        crate::input_source::ResolveProblem::ParentHasNoDirectory { parent } => {
252                            format!(
253                                "include parent '{}' does not have a parent directory",
254                                parent
255                            )
256                        }
257                        crate::input_source::ResolveProblem::ResolvesOutsideRoot { spec, root } => {
258                            format!(
259                                "include '{}' resolves outside the configured root '{}'",
260                                spec, root
261                            )
262                        }
263                        crate::input_source::ResolveProblem::TraversesSymlink { spec } => {
264                            format!(
265                                "include '{}' traverses a symlink, which is disabled by policy",
266                                spec
267                            )
268                        }
269                        crate::input_source::ResolveProblem::AbsolutePathNotAllowed { spec } => {
270                            format!("absolute include paths are not allowed: {}", spec)
271                        }
272                        crate::input_source::ResolveProblem::EmptyPath => {
273                            "include path must not be empty".to_string()
274                        }
275                        crate::input_source::ResolveProblem::InvalidExtension { spec } => {
276                            format!(
277                                "include target '{}' does not have a valid YAML extension (.yml or .yaml)",
278                                spec
279                            )
280                        }
281                        crate::input_source::ResolveProblem::HiddenFile { spec } => {
282                            format!(
283                                "include target '{}' is a hidden file, which is not allowed",
284                                spec
285                            )
286                        }
287                        crate::input_source::ResolveProblem::EmptyFragment => {
288                            "include fragment must not be empty".to_string()
289                        }
290                        crate::input_source::ResolveProblem::FragmentContainsHash { spec } => {
291                            format!("include fragment must not contain '#': {}", spec)
292                        }
293                    }
294                }
295            };
296            full_msg.push_str(&msg);
297            Cow::Owned(full_msg)
298        }
299        Error::Budget { breach, .. } => Cow::Owned(format!("budget breached: {breach:?}")),
300        Error::QuotingRequired { value, .. } => {
301            Cow::Owned(format!("The string value [{value}] must be quoted"))
302        }
303        Error::CannotBorrowTransformedString { reason, .. } => Cow::Owned(format!(
304            "input does not contain value verbatim so cannot deserialize into &str ({reason}); use String or Cow<str> instead",
305        )),
306        Error::IndentationError {
307            required, actual, ..
308        } => Cow::Owned(format!(
309            "indentation error: expected {required}, found {actual} spaces"
310        )),
311        Error::IOError { cause } => Cow::Owned(format!("IO error: {cause}")),
312        Error::AliasError { msg, locations } => {
313            let l10n = formatter.localizer();
314            let ref_loc = locations.reference_location;
315            let def_loc = locations.defined_location;
316            match (ref_loc, def_loc) {
317                (Location::UNKNOWN, Location::UNKNOWN) => Cow::Borrowed(msg.as_str()),
318                (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
319                    Cow::Borrowed(msg.as_str())
320                }
321                (_r, d) => Cow::Owned(format!("{msg}{}", l10n.alias_defined_at(d))),
322            }
323        }
324
325        #[cfg(feature = "garde")]
326        Error::ValidationError { issues, locations } => {
327            let l10n = formatter.localizer();
328
329            let mut lines = Vec::with_capacity(issues.len());
330            for issue in issues {
331                let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Garde);
332                let path_key = &issue.path;
333                let original_leaf = path_key
334                    .leaf_string()
335                    .unwrap_or_else(|| l10n.root_path_label().into_owned());
336
337                let (locs, resolved_leaf) = locations
338                    .search(path_key)
339                    .unwrap_or((Locations::UNKNOWN, original_leaf));
340
341                let loc = if locs.reference_location != Location::UNKNOWN {
342                    locs.reference_location
343                } else {
344                    locs.defined_location
345                };
346
347                let resolved_path = format_path_with_resolved_leaf(path_key, &resolved_leaf);
348
349                lines.push(l10n.validation_issue_line(
350                    &resolved_path,
351                    &entry,
352                    (loc != Location::UNKNOWN).then_some(loc),
353                ));
354            }
355            Cow::Owned(l10n.join_validation_issues(&lines))
356        }
357        #[cfg(feature = "garde")]
358        Error::ValidationErrors { errors } => Cow::Owned(format!(
359            "validation failed for {} document(s)",
360            errors.len()
361        )),
362        #[cfg(feature = "validator")]
363        Error::ValidatorError { issues, locations } => {
364            let l10n = formatter.localizer();
365
366            let mut lines = Vec::with_capacity(issues.len());
367            for issue in issues {
368                let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Validator);
369                let path_key = &issue.path;
370                let original_leaf = path_key
371                    .leaf_string()
372                    .unwrap_or_else(|| l10n.root_path_label().into_owned());
373
374                let (locs, resolved_leaf) = locations
375                    .search(path_key)
376                    .unwrap_or((Locations::UNKNOWN, original_leaf));
377
378                let loc = if locs.reference_location != Location::UNKNOWN {
379                    locs.reference_location
380                } else {
381                    locs.defined_location
382                };
383
384                let resolved_path = format_path_with_resolved_leaf(path_key, &resolved_leaf);
385
386                lines.push(l10n.validation_issue_line(
387                    &resolved_path,
388                    &entry,
389                    (loc != Location::UNKNOWN).then_some(loc),
390                ));
391            }
392            Cow::Owned(l10n.join_validation_issues(&lines))
393        }
394        #[cfg(feature = "validator")]
395        Error::ValidatorErrors { errors } => Cow::Owned(format!(
396            "validation failed for {} document(s)",
397            errors.len()
398        )),
399    }
400}
401
402impl MessageFormatter for DefaultMessageFormatter {
403    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
404        default_format_message(self, err)
405    }
406}
407
408pub struct DefaultMessageFormatterWithLocalizer<'a> {
409    localizer: &'a dyn Localizer,
410}
411
412impl MessageFormatter for DefaultMessageFormatterWithLocalizer<'_> {
413    fn localizer(&self) -> &dyn Localizer {
414        self.localizer
415    }
416
417    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
418        default_format_message(self, err)
419    }
420}
421
422impl DefaultMessageFormatter {
423    /// Return a formatter that uses a custom [`Localizer`].
424    ///
425    /// This allows reusing the built-in developer-oriented messages while customizing
426    /// wording that is produced outside `format_message` (location suffixes, validation
427    /// issue composition, snippet labels, etc.).
428    pub fn with_localizer<'a>(
429        &self,
430        localizer: &'a dyn Localizer,
431    ) -> DefaultMessageFormatterWithLocalizer<'a> {
432        DefaultMessageFormatterWithLocalizer { localizer }
433    }
434}
435
436fn user_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
437    if let Error::WithSnippet { error, .. } = err {
438        return user_format_message(formatter, error);
439    }
440
441    match err {
442        // handled by early return above
443        Error::WithSnippet { .. } => unreachable!(),
444
445        Error::Eof { .. } => Cow::Borrowed("unexpected end of file"),
446        Error::MultipleDocuments { .. } => {
447            Cow::Borrowed("only single YAML document expected but multiple found")
448        }
449        Error::InvalidUtf8Input => Cow::Borrowed("YAML parser input is not valid UTF-8"),
450        Error::BinaryNotUtf8 { .. } => {
451            Cow::Borrowed("!!binary scalar is not valid UTF-8 so cannot be stored into string.")
452        }
453        Error::InvalidBooleanStrict { .. } => {
454            Cow::Borrowed("invalid boolean (true or false expected)")
455        }
456        Error::NullIntoString { .. } | Error::InvalidCharNull { .. } => {
457            Cow::Borrowed("null is not allowed here")
458        }
459        Error::InvalidCharNotSingleScalar { .. } => {
460            Cow::Borrowed("only single character allowed here")
461        }
462        Error::BytesNotSupportedMissingBinaryTag { .. } => Cow::Borrowed("missing !!binary tag"),
463        Error::ExpectedEmptyMappingForUnitStruct { .. } => {
464            Cow::Borrowed("expected empty mapping here")
465        }
466        Error::UnexpectedContainerEndWhileSkippingNode { .. } => {
467            Cow::Borrowed("unexpected container end")
468        }
469        Error::AliasReplayCounterOverflow { .. } => {
470            Cow::Borrowed("YAML document too large or too complex")
471        }
472        Error::AliasReplayLimitExceeded {
473            total_replayed_events,
474            max_total_replayed_events,
475            ..
476        } => Cow::Owned(format!(
477            "YAML document too large or too complex: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
478        )),
479        Error::AliasExpansionLimitExceeded {
480            anchor_id,
481            expansions,
482            max_expansions_per_anchor,
483            ..
484        } => Cow::Owned(format!(
485            "YAML document too large or too complex: anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
486        )),
487        Error::AliasReplayStackDepthExceeded {
488            depth, max_depth, ..
489        } => Cow::Owned(format!(
490            "YAML document too large or too complex: depth={depth} > {max_depth}"
491        )),
492        Error::UnknownAnchor { .. } => Cow::Borrowed("reference to unknown value"),
493        Error::MergeKeyNotAllowed { .. } => Cow::Borrowed("merge key not allowed here"),
494        Error::CyclicInclude { .. } => Cow::Borrowed("cyclic include detected"),
495        Error::UnsupportedIncludeForm { .. } => {
496            Cow::Borrowed("!include currently only supports the scalar form: !include <path>")
497        }
498        Error::ResolverError { .. } => Cow::Borrowed("failed to resolve include"),
499        Error::RecursiveReferencesRequireWeakTypes { .. } => {
500            Cow::Borrowed("Recursive reference not allowed here")
501        }
502        Error::DuplicateMappingKey { key, .. } => match key {
503            Some(k) => Cow::Owned(format!("duplicate mapping key: {k} not allowed here")),
504            None => Cow::Borrowed("duplicate mapping key not allowed here"),
505        },
506        Error::QuotingRequired { .. } => Cow::Borrowed("value requires quoting"),
507        Error::Budget { breach, .. } => Cow::Owned(format!(
508            "YAML document too large or too complex: limits breached: {breach:?}"
509        )),
510        Error::CannotBorrowTransformedString { .. } => {
511            Cow::Borrowed("Only single string with no escape sequences is allowed here")
512        }
513        Error::IndentationError {
514            required, actual, ..
515        } => Cow::Owned(format!(
516            "incorrect indentation: expected {required}, found {actual} spaces"
517        )),
518
519        // All cases when the standard message is good enough.
520        _ => default_format_message(formatter, err),
521    }
522}
523
524impl MessageFormatter for UserMessageFormatter {
525    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
526        user_format_message(self, err)
527    }
528}
529
530pub struct UserMessageFormatterWithLocalizer<'a> {
531    localizer: &'a dyn Localizer,
532}
533
534impl MessageFormatter for UserMessageFormatterWithLocalizer<'_> {
535    fn localizer(&self) -> &dyn Localizer {
536        self.localizer
537    }
538
539    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
540        user_format_message(self, err)
541    }
542}
543
544impl UserMessageFormatter {
545    /// Return a formatter that uses a custom [`Localizer`].
546    ///
547    /// This allows reusing the built-in user-facing messages while customizing wording
548    /// that is produced outside `format_message` (location suffixes, validation issue
549    /// composition, snippet labels, etc.).
550    pub fn with_localizer<'a>(
551        &self,
552        localizer: &'a dyn Localizer,
553    ) -> UserMessageFormatterWithLocalizer<'a> {
554        UserMessageFormatterWithLocalizer { localizer }
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::Location;
562    use crate::de_error::{Error, MessageFormatter, TransformReason};
563    use crate::location::Locations;
564
565    fn loc() -> Location {
566        Location::UNKNOWN
567    }
568
569    // -----------------------------------------------------------------------
570    // DefaultMessageFormatter – uncovered arms
571    // -----------------------------------------------------------------------
572
573    #[test]
574    fn default_with_snippet_delegates() {
575        let formatter = DefaultMessageFormatter;
576        let inner = Error::Eof { location: loc() };
577        let err = Error::WithSnippet {
578            regions: vec![],
579            crop_radius: 3,
580            error: Box::new(inner),
581        };
582        assert_eq!(formatter.format_message(&err), "unexpected end of input");
583    }
584
585    #[test]
586    fn default_hook_error() {
587        let formatter = DefaultMessageFormatter;
588        let err = Error::HookError {
589            msg: "hook msg".to_owned(),
590            location: loc(),
591        };
592        assert_eq!(formatter.format_message(&err), "hook msg");
593    }
594
595    #[test]
596    fn default_serde_variant_id() {
597        let formatter = DefaultMessageFormatter;
598        let err = Error::SerdeVariantId {
599            msg: "variant id msg".to_owned(),
600            location: loc(),
601        };
602        assert_eq!(formatter.format_message(&err), "variant id msg");
603    }
604
605    #[test]
606    fn default_invalid_binary_base64() {
607        let formatter = DefaultMessageFormatter;
608        let err = Error::InvalidBinaryBase64 { location: loc() };
609        assert_eq!(formatter.format_message(&err), "invalid !!binary base64");
610    }
611
612    #[test]
613    fn default_merge_key_not_allowed() {
614        let formatter = DefaultMessageFormatter;
615        let err = Error::MergeKeyNotAllowed { location: loc() };
616        assert_eq!(
617            formatter.format_message(&err),
618            "YAML merge keys are not allowed by configured policy"
619        );
620    }
621
622    #[test]
623    fn default_unexpected_sequence_end() {
624        let formatter = DefaultMessageFormatter;
625        let err = Error::UnexpectedSequenceEnd { location: loc() };
626        assert_eq!(formatter.format_message(&err), "unexpected sequence end");
627    }
628
629    #[test]
630    fn default_unexpected_mapping_end() {
631        let formatter = DefaultMessageFormatter;
632        let err = Error::UnexpectedMappingEnd { location: loc() };
633        assert_eq!(formatter.format_message(&err), "unexpected mapping end");
634    }
635
636    #[test]
637    fn default_unexpected_container_end_while_skipping() {
638        let formatter = DefaultMessageFormatter;
639        let err = Error::UnexpectedContainerEndWhileSkippingNode { location: loc() };
640        assert_eq!(
641            formatter.format_message(&err),
642            "unexpected container end while skipping node"
643        );
644    }
645
646    #[test]
647    fn default_internal_seed_reused() {
648        let formatter = DefaultMessageFormatter;
649        let err = Error::InternalSeedReusedForMapKey { location: loc() };
650        assert_eq!(
651            formatter.format_message(&err),
652            "internal error: seed reused for map key"
653        );
654    }
655
656    #[test]
657    fn default_value_requested_before_key() {
658        let formatter = DefaultMessageFormatter;
659        let err = Error::ValueRequestedBeforeKey { location: loc() };
660        assert_eq!(formatter.format_message(&err), "value requested before key");
661    }
662
663    #[test]
664    fn default_alias_replay_counter_overflow() {
665        let formatter = DefaultMessageFormatter;
666        let err = Error::AliasReplayCounterOverflow { location: loc() };
667        assert_eq!(
668            formatter.format_message(&err),
669            "alias replay counter overflow"
670        );
671    }
672
673    #[test]
674    fn default_folded_block_scalar() {
675        let formatter = DefaultMessageFormatter;
676        let err = Error::FoldedBlockScalarMustIndentContent { location: loc() };
677        assert_eq!(
678            formatter.format_message(&err),
679            "folded block scalars must indent their content"
680        );
681    }
682
683    #[test]
684    fn default_internal_depth_underflow() {
685        let formatter = DefaultMessageFormatter;
686        let err = Error::InternalDepthUnderflow { location: loc() };
687        assert_eq!(formatter.format_message(&err), "internal depth underflow");
688    }
689
690    #[test]
691    fn default_internal_recursion_stack_empty() {
692        let formatter = DefaultMessageFormatter;
693        let err = Error::InternalRecursionStackEmpty { location: loc() };
694        assert_eq!(
695            formatter.format_message(&err),
696            "internal recursion stack empty"
697        );
698    }
699
700    #[test]
701    fn default_recursive_references_require_weak_types() {
702        let formatter = DefaultMessageFormatter;
703        let err = Error::RecursiveReferencesRequireWeakTypes { location: loc() };
704        assert_eq!(
705            formatter.format_message(&err),
706            "recursive references require weak recursion types"
707        );
708    }
709
710    #[test]
711    fn default_serde_invalid_value() {
712        let formatter = DefaultMessageFormatter;
713        let err = Error::SerdeInvalidValue {
714            unexpected: "null".to_owned(),
715            expected: "string".to_owned(),
716            location: loc(),
717        };
718        let msg = formatter.format_message(&err);
719        assert!(msg.contains("invalid value"), "got: {msg}");
720        assert!(msg.contains("null"), "got: {msg}");
721        assert!(msg.contains("string"), "got: {msg}");
722    }
723
724    #[test]
725    fn default_serde_unknown_variant() {
726        let formatter = DefaultMessageFormatter;
727        let err = Error::SerdeUnknownVariant {
728            variant: "foo".to_owned(),
729            expected: vec!["bar", "baz"],
730            location: loc(),
731        };
732        let msg = formatter.format_message(&err);
733        assert!(msg.contains("unknown variant"), "got: {msg}");
734        assert!(msg.contains("foo"), "got: {msg}");
735    }
736
737    #[test]
738    fn default_serde_unknown_field() {
739        let formatter = DefaultMessageFormatter;
740        let err = Error::SerdeUnknownField {
741            field: "xyz".to_owned(),
742            expected: vec!["a", "b"],
743            location: loc(),
744        };
745        let msg = formatter.format_message(&err);
746        assert!(msg.contains("unknown field"), "got: {msg}");
747        assert!(msg.contains("xyz"), "got: {msg}");
748    }
749
750    #[test]
751    fn default_unexpected_container_end_while_reading_key() {
752        let formatter = DefaultMessageFormatter;
753        let err = Error::UnexpectedContainerEndWhileReadingKeyNode { location: loc() };
754        assert_eq!(
755            formatter.format_message(&err),
756            "unexpected container end while reading key"
757        );
758    }
759
760    #[test]
761    fn default_expected_mapping_end_after_enum_variant() {
762        let formatter = DefaultMessageFormatter;
763        let err = Error::ExpectedMappingEndAfterEnumVariantValue { location: loc() };
764        assert_eq!(
765            formatter.format_message(&err),
766            "expected end of mapping after enum variant value"
767        );
768    }
769
770    #[test]
771    fn default_container_end_mismatch() {
772        let formatter = DefaultMessageFormatter;
773        let err = Error::ContainerEndMismatch { location: loc() };
774        assert_eq!(
775            formatter.format_message(&err),
776            "list or mapping end with no start"
777        );
778    }
779
780    #[test]
781    fn default_io_error() {
782        let formatter = DefaultMessageFormatter;
783        let err = Error::IOError {
784            cause: std::io::Error::other("disk full"),
785        };
786        let msg = formatter.format_message(&err);
787        assert!(msg.contains("IO error"), "got: {msg}");
788        assert!(msg.contains("disk full"), "got: {msg}");
789    }
790
791    #[test]
792    fn default_unresolved_property() {
793        let formatter = DefaultMessageFormatter;
794        let err = Error::UnresolvedProperty {
795            name: "MISSING".to_owned(),
796            location: loc(),
797        };
798        assert_eq!(formatter.format_message(&err), "missing property `MISSING`");
799    }
800
801    #[test]
802    fn default_invalid_property_name() {
803        let formatter = DefaultMessageFormatter;
804        let err = Error::InvalidPropertyName {
805            name: "${ab-cd}".to_owned(),
806            location: loc(),
807        };
808        assert_eq!(formatter.format_message(&err), "Invalid name: '${ab-cd}'");
809    }
810
811    #[test]
812    fn default_alias_error_both_unknown() {
813        let formatter = DefaultMessageFormatter;
814        let err = Error::AliasError {
815            msg: "alias msg".to_owned(),
816            locations: Locations::UNKNOWN,
817        };
818        assert_eq!(formatter.format_message(&err), "alias msg");
819    }
820
821    #[test]
822    fn default_alias_error_ref_known_def_unknown() {
823        let formatter = DefaultMessageFormatter;
824        let ref_loc = Location::new(1, 0);
825        let err = Error::AliasError {
826            msg: "alias msg".to_owned(),
827            locations: Locations {
828                reference_location: ref_loc,
829                defined_location: Location::UNKNOWN,
830            },
831        };
832        // r != UNKNOWN and d == UNKNOWN → returns msg as-is
833        assert_eq!(formatter.format_message(&err), "alias msg");
834    }
835
836    #[test]
837    fn default_alias_error_both_known_different() {
838        let formatter = DefaultMessageFormatter;
839        let ref_loc = Location::new(1, 0);
840        let def_loc = Location::new(5, 0);
841        let err = Error::AliasError {
842            msg: "alias msg".to_owned(),
843            locations: Locations {
844                reference_location: ref_loc,
845                defined_location: def_loc,
846            },
847        };
848        // _r != UNKNOWN, d != UNKNOWN, d != r → appends defined-at suffix
849        let msg = formatter.format_message(&err);
850        assert!(msg.starts_with("alias msg"), "got: {msg}");
851    }
852
853    // -----------------------------------------------------------------------
854    // UserMessageFormatter – all arms
855    // -----------------------------------------------------------------------
856
857    #[test]
858    fn user_with_snippet_delegates() {
859        let formatter = UserMessageFormatter;
860        let inner = Error::Eof { location: loc() };
861        let err = Error::WithSnippet {
862            regions: vec![],
863            crop_radius: 3,
864            error: Box::new(inner),
865        };
866        assert_eq!(formatter.format_message(&err), "unexpected end of file");
867    }
868
869    #[test]
870    fn user_eof() {
871        let formatter = UserMessageFormatter;
872        let err = Error::Eof { location: loc() };
873        assert_eq!(formatter.format_message(&err), "unexpected end of file");
874    }
875
876    #[test]
877    fn user_multiple_documents() {
878        let formatter = UserMessageFormatter;
879        let err = Error::MultipleDocuments {
880            hint: "use from_str_multidoc",
881            location: loc(),
882        };
883        assert_eq!(
884            formatter.format_message(&err),
885            "only single YAML document expected but multiple found"
886        );
887    }
888
889    #[test]
890    fn user_invalid_utf8_input() {
891        let formatter = UserMessageFormatter;
892        let err = Error::InvalidUtf8Input;
893        assert_eq!(
894            formatter.format_message(&err),
895            "YAML parser input is not valid UTF-8"
896        );
897    }
898
899    #[test]
900    fn user_binary_not_utf8() {
901        let formatter = UserMessageFormatter;
902        let err = Error::BinaryNotUtf8 { location: loc() };
903        assert!(formatter.format_message(&err).contains("!!binary"));
904    }
905
906    #[test]
907    fn user_invalid_boolean_strict() {
908        let formatter = UserMessageFormatter;
909        let err = Error::InvalidBooleanStrict { location: loc() };
910        assert_eq!(
911            formatter.format_message(&err),
912            "invalid boolean (true or false expected)"
913        );
914    }
915
916    #[test]
917    fn user_null_into_string() {
918        let formatter = UserMessageFormatter;
919        let err = Error::NullIntoString { location: loc() };
920        assert_eq!(formatter.format_message(&err), "null is not allowed here");
921    }
922
923    #[test]
924    fn user_invalid_char_null() {
925        let formatter = UserMessageFormatter;
926        let err = Error::InvalidCharNull { location: loc() };
927        assert_eq!(formatter.format_message(&err), "null is not allowed here");
928    }
929
930    #[test]
931    fn user_invalid_char_not_single_scalar() {
932        let formatter = UserMessageFormatter;
933        let err = Error::InvalidCharNotSingleScalar { location: loc() };
934        assert_eq!(
935            formatter.format_message(&err),
936            "only single character allowed here"
937        );
938    }
939
940    #[test]
941    fn user_bytes_not_supported_missing_binary_tag() {
942        let formatter = UserMessageFormatter;
943        let err = Error::BytesNotSupportedMissingBinaryTag { location: loc() };
944        assert_eq!(formatter.format_message(&err), "missing !!binary tag");
945    }
946
947    #[test]
948    fn user_expected_empty_mapping_for_unit_struct() {
949        let formatter = UserMessageFormatter;
950        let err = Error::ExpectedEmptyMappingForUnitStruct { location: loc() };
951        assert_eq!(
952            formatter.format_message(&err),
953            "expected empty mapping here"
954        );
955    }
956
957    #[test]
958    fn user_unexpected_container_end_while_skipping() {
959        let formatter = UserMessageFormatter;
960        let err = Error::UnexpectedContainerEndWhileSkippingNode { location: loc() };
961        assert_eq!(formatter.format_message(&err), "unexpected container end");
962    }
963
964    #[test]
965    fn user_alias_replay_counter_overflow() {
966        let formatter = UserMessageFormatter;
967        let err = Error::AliasReplayCounterOverflow { location: loc() };
968        assert_eq!(
969            formatter.format_message(&err),
970            "YAML document too large or too complex"
971        );
972    }
973
974    #[test]
975    fn user_alias_replay_limit_exceeded() {
976        let formatter = UserMessageFormatter;
977        let err = Error::AliasReplayLimitExceeded {
978            total_replayed_events: 1000,
979            max_total_replayed_events: 500,
980            location: loc(),
981        };
982        let msg = formatter.format_message(&err);
983        assert!(msg.contains("too large or too complex"), "got: {msg}");
984        assert!(msg.contains("1000"), "got: {msg}");
985    }
986
987    #[test]
988    fn user_alias_expansion_limit_exceeded() {
989        let formatter = UserMessageFormatter;
990        let err = Error::AliasExpansionLimitExceeded {
991            anchor_id: 7,
992            expansions: 200,
993            max_expansions_per_anchor: 100,
994            location: loc(),
995        };
996        let msg = formatter.format_message(&err);
997        assert!(msg.contains("too large or too complex"), "got: {msg}");
998        assert!(msg.contains("7"), "got: {msg}");
999    }
1000
1001    #[test]
1002    fn user_alias_replay_stack_depth_exceeded() {
1003        let formatter = UserMessageFormatter;
1004        let err = Error::AliasReplayStackDepthExceeded {
1005            depth: 50,
1006            max_depth: 20,
1007            location: loc(),
1008        };
1009        let msg = formatter.format_message(&err);
1010        assert!(msg.contains("too large or too complex"), "got: {msg}");
1011        assert!(msg.contains("50"), "got: {msg}");
1012    }
1013
1014    #[test]
1015    fn user_unknown_anchor() {
1016        let formatter = UserMessageFormatter;
1017        let err = Error::UnknownAnchor { location: loc() };
1018        assert_eq!(formatter.format_message(&err), "reference to unknown value");
1019    }
1020
1021    #[test]
1022    fn user_merge_key_not_allowed() {
1023        let formatter = UserMessageFormatter;
1024        let err = Error::MergeKeyNotAllowed { location: loc() };
1025        assert_eq!(formatter.format_message(&err), "merge key not allowed here");
1026    }
1027
1028    #[test]
1029    fn user_recursive_references_require_weak_types() {
1030        let formatter = UserMessageFormatter;
1031        let err = Error::RecursiveReferencesRequireWeakTypes { location: loc() };
1032        assert_eq!(
1033            formatter.format_message(&err),
1034            "Recursive reference not allowed here"
1035        );
1036    }
1037
1038    #[test]
1039    fn user_duplicate_mapping_key_with_key() {
1040        let formatter = UserMessageFormatter;
1041        let err = Error::DuplicateMappingKey {
1042            key: Some("mykey".to_owned()),
1043            location: loc(),
1044        };
1045        let msg = formatter.format_message(&err);
1046        assert!(msg.contains("mykey"), "got: {msg}");
1047        assert!(msg.contains("duplicate"), "got: {msg}");
1048    }
1049
1050    #[test]
1051    fn user_duplicate_mapping_key_without_key() {
1052        let formatter = UserMessageFormatter;
1053        let err = Error::DuplicateMappingKey {
1054            key: None,
1055            location: loc(),
1056        };
1057        let msg = formatter.format_message(&err);
1058        assert!(msg.contains("duplicate"), "got: {msg}");
1059    }
1060
1061    #[test]
1062    fn user_quoting_required() {
1063        let formatter = UserMessageFormatter;
1064        let err = Error::QuotingRequired {
1065            value: "yes".to_owned(),
1066            location: loc(),
1067        };
1068        assert_eq!(formatter.format_message(&err), "value requires quoting");
1069    }
1070
1071    #[test]
1072    fn user_budget() {
1073        use crate::budget::BudgetBreach;
1074        let formatter = UserMessageFormatter;
1075        let err = Error::Budget {
1076            breach: BudgetBreach::Events { events: 9999 },
1077            location: loc(),
1078        };
1079        let msg = formatter.format_message(&err);
1080        assert!(msg.contains("too large or too complex"), "got: {msg}");
1081    }
1082
1083    #[test]
1084    fn user_cannot_borrow_transformed_string() {
1085        let formatter = UserMessageFormatter;
1086        let err = Error::CannotBorrowTransformedString {
1087            reason: TransformReason::EscapeSequence,
1088            location: loc(),
1089        };
1090        assert_eq!(
1091            formatter.format_message(&err),
1092            "Only single string with no escape sequences is allowed here"
1093        );
1094    }
1095
1096    #[test]
1097    fn user_falls_through_to_default_for_unhandled() {
1098        // SerdeInvalidType is not explicitly handled by user_format_message → falls through to default
1099        let formatter = UserMessageFormatter;
1100        let err = Error::SerdeInvalidType {
1101            unexpected: "seq".to_owned(),
1102            expected: "map".to_owned(),
1103            location: loc(),
1104        };
1105        let msg = formatter.format_message(&err);
1106        assert!(msg.contains("invalid type"), "got: {msg}");
1107    }
1108
1109    // -----------------------------------------------------------------------
1110    // UserMessageFormatterWithLocalizer
1111    // -----------------------------------------------------------------------
1112
1113    #[test]
1114    fn user_with_localizer_delegates() {
1115        use crate::localizer::DefaultEnglishLocalizer;
1116        let localizer = DefaultEnglishLocalizer;
1117        let formatter = UserMessageFormatter.with_localizer(&localizer);
1118        let err = Error::Eof { location: loc() };
1119        assert_eq!(formatter.format_message(&err), "unexpected end of file");
1120    }
1121
1122    // -----------------------------------------------------------------------
1123    // DefaultMessageFormatterWithLocalizer
1124    // -----------------------------------------------------------------------
1125
1126    #[test]
1127    fn default_with_localizer_delegates() {
1128        use crate::localizer::DefaultEnglishLocalizer;
1129        let localizer = DefaultEnglishLocalizer;
1130        let formatter = DefaultMessageFormatter.with_localizer(&localizer);
1131        let err = Error::Eof { location: loc() };
1132        assert_eq!(formatter.format_message(&err), "unexpected end of input");
1133    }
1134}