Skip to main content

serde_saphyr/
message_formatters.rs

1use crate::de_error::{Error, MessageFormatter, UserMessageFormatter};
2use crate::localizer::{ExternalMessage, Localizer};
3use crate::Location;
4
5use std::borrow::Cow;
6
7#[cfg(any(feature = "garde", feature = "validator"))]
8use crate::localizer::ExternalMessageSource;
9
10#[cfg(any(feature = "garde", feature = "validator"))]
11use crate::path_map::format_path_with_resolved_leaf;
12
13#[cfg(any(feature = "garde", feature = "validator"))]
14use crate::Locations;
15
16#[cfg(feature = "garde")]
17use crate::de_error::collect_garde_issues;
18
19#[cfg(feature = "validator")]
20use crate::de_error::collect_validator_issues;
21
22/// Default developer-oriented message formatter.
23///
24/// This formatter at places produces recommendations on how to adjust settings and API
25/// calls for the parsing to work, so normally should not be user-facing. Use UserMessageFormatter
26/// for user-facing content, or implement custom MessageFormatter for full control over output.
27#[derive(Debug, Default, Clone, Copy)]
28pub struct DefaultMessageFormatter;
29
30/// Alias for the default developer-oriented formatter.
31pub type DeveloperMessageFormatter = DefaultMessageFormatter;
32
33fn default_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
34    match err {
35        Error::WithSnippet { error, .. } => default_format_message(formatter, error),
36        Error::ExternalMessage {
37            source,
38            msg,
39            code,
40            params,
41            ..
42        } => {
43            let l10n = formatter.localizer();
44            l10n.override_external_message(ExternalMessage {
45                source: *source,
46                original: msg.as_str(),
47                code: code.as_deref(),
48                params,
49            })
50            .unwrap_or(Cow::Borrowed(msg.as_str()))
51        }
52        Error::Message { msg, .. }
53        | Error::HookError { msg, .. }
54        | Error::SerdeVariantId { msg, .. } => Cow::Borrowed(msg.as_str()),
55        Error::Eof { .. } => Cow::Borrowed("unexpected end of input"),
56        Error::MultipleDocuments { hint, .. } => {
57            Cow::Owned(format!("multiple YAML documents detected; {hint}"))
58        }
59        Error::Unexpected { expected, .. } => {
60            Cow::Owned(format!("unexpected event: expected {expected}"))
61        }
62        Error::MergeValueNotMapOrSeqOfMaps { .. } => {
63            Cow::Borrowed("YAML merge value must be mapping or sequence of mappings")
64        }
65        Error::InvalidBinaryBase64 { .. } => Cow::Borrowed("invalid !!binary base64"),
66        Error::InvalidUtf8Input => Cow::Borrowed("input is not valid UTF-8"),
67        Error::BinaryNotUtf8 { .. } => Cow::Borrowed(
68            "!!binary scalar is not valid UTF-8 so cannot be stored into string. \
69                 If you just use !!binary for documentation/annotation, set ignore_binary_tag_for_string in Options",
70        ),
71        Error::TaggedScalarCannotDeserializeIntoString { .. } => {
72            Cow::Borrowed("cannot deserialize tagged scalar into string")
73        }
74        Error::UnexpectedSequenceEnd { .. } => Cow::Borrowed("unexpected sequence end"),
75        Error::UnexpectedMappingEnd { .. } => Cow::Borrowed("unexpected mapping end"),
76        Error::InvalidBooleanStrict { .. } => {
77            Cow::Borrowed("invalid boolean (strict mode expects true/false)")
78        }
79        Error::InvalidCharNull { .. } => {
80            Cow::Borrowed("invalid char: cannot deserialize null; use Option<char>")
81        }
82        Error::InvalidCharNotSingleScalar { .. } => {
83            Cow::Borrowed("invalid char: expected a single Unicode scalar value")
84        }
85        Error::NullIntoString { .. } => {
86            Cow::Borrowed("cannot deserialize null into string; use Option<String>")
87        }
88        Error::BytesNotSupportedMissingBinaryTag { .. } => {
89            Cow::Borrowed("bytes not supported (missing !!binary tag)")
90        }
91        Error::UnexpectedValueForUnit { .. } => Cow::Borrowed("unexpected value for unit"),
92        Error::ExpectedEmptyMappingForUnitStruct { .. } => {
93            Cow::Borrowed("expected empty mapping for unit struct")
94        }
95        Error::UnexpectedContainerEndWhileSkippingNode { .. } => {
96            Cow::Borrowed("unexpected container end while skipping node")
97        }
98        Error::InternalSeedReusedForMapKey { .. } => {
99            Cow::Borrowed("internal error: seed reused for map key")
100        }
101        Error::ValueRequestedBeforeKey { .. } => Cow::Borrowed("value requested before key"),
102        Error::ExpectedStringKeyForExternallyTaggedEnum { .. } => {
103            Cow::Borrowed("expected string key for externally tagged enum")
104        }
105        Error::ExternallyTaggedEnumExpectedScalarOrMapping { .. } => {
106            Cow::Borrowed("externally tagged enum expected scalar or mapping")
107        }
108        Error::UnexpectedValueForUnitEnumVariant { .. } => {
109            Cow::Borrowed("unexpected value for unit enum variant")
110        }
111        Error::AliasReplayCounterOverflow { .. } => Cow::Borrowed("alias replay counter overflow"),
112        Error::AliasReplayLimitExceeded {
113            total_replayed_events,
114            max_total_replayed_events,
115            ..
116        } => Cow::Owned(format!(
117            "alias replay limit exceeded: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
118        )),
119        Error::AliasExpansionLimitExceeded {
120            anchor_id,
121            expansions,
122            max_expansions_per_anchor,
123            ..
124        } => Cow::Owned(format!(
125            "alias expansion limit exceeded for anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
126        )),
127        Error::AliasReplayStackDepthExceeded {
128            depth,
129            max_depth,
130            ..
131        } => Cow::Owned(format!(
132            "alias replay stack depth exceeded: depth={depth} > {max_depth}"
133        )),
134        Error::FoldedBlockScalarMustIndentContent { .. } => {
135            Cow::Borrowed("folded block scalars must indent their content")
136        }
137        Error::InternalDepthUnderflow { .. } => Cow::Borrowed("internal depth underflow"),
138        Error::InternalRecursionStackEmpty { .. } => {
139            Cow::Borrowed("internal recursion stack empty")
140        }
141        Error::RecursiveReferencesRequireWeakTypes { .. } => {
142            Cow::Borrowed("recursive references require weak recursion types")
143        }
144        Error::InvalidScalar { ty, .. } => Cow::Owned(format!("invalid {ty}")),
145        Error::SerdeInvalidType {
146            unexpected,
147            expected,
148            ..
149        } => Cow::Owned(format!("invalid type: {unexpected}, expected {expected}")),
150        Error::SerdeInvalidValue {
151            unexpected,
152            expected,
153            ..
154        } => Cow::Owned(format!("invalid value: {unexpected}, expected {expected}")),
155        Error::SerdeUnknownVariant {
156            variant,
157            expected,
158            ..
159        } => Cow::Owned(format!(
160            "unknown variant `{variant}`, expected one of {}",
161            expected.join(", ")
162        )),
163        Error::SerdeUnknownField {
164            field,
165            expected,
166            ..
167        } => Cow::Owned(format!(
168            "unknown field `{field}`, expected one of {}",
169            expected.join(", ")
170        )),
171        Error::SerdeMissingField { field, .. } => Cow::Owned(format!("missing field `{field}`")),
172        Error::UnexpectedContainerEndWhileReadingKeyNode { .. } => {
173            Cow::Borrowed("unexpected container end while reading key")
174        }
175        Error::DuplicateMappingKey { key, .. } => match key {
176            Some(k) => Cow::Owned(format!(
177                "duplicate mapping key: {k}, set DuplicateKeyPolicy in Options if acceptable"
178            )),
179            None => Cow::Borrowed(
180                "duplicate mapping key, set DuplicateKeyPolicy in Options if acceptable",
181            ),
182        },
183        Error::TaggedEnumMismatch { tagged, target, .. } => Cow::Owned(format!(
184            "tagged enum `{tagged}` does not match target enum `{target}`",
185        )),
186        Error::ExpectedMappingEndAfterEnumVariantValue { .. } => {
187            Cow::Borrowed("expected end of mapping after enum variant value")
188        }
189        Error::ContainerEndMismatch { .. } => Cow::Borrowed("list or mapping end with no start"),
190        Error::UnknownAnchor { .. } => Cow::Borrowed("alias references unknown anchor"),
191        Error::Budget { breach, .. } => Cow::Owned(format!("budget breached: {breach:?}")),
192        Error::QuotingRequired { value, .. } => {
193            Cow::Owned(format!("The string value [{value}] must be quoted"))
194        }
195        Error::CannotBorrowTransformedString { reason, .. } => Cow::Owned(format!(
196            "input does not contain value verbatim so cannot deserialize into &str ({reason}); use String or Cow<str> instead",
197        )),
198        Error::IOError { cause } => Cow::Owned(format!("IO error: {cause}")),
199        Error::AliasError { msg, locations } => {
200            let l10n = formatter.localizer();
201            let ref_loc = locations.reference_location;
202            let def_loc = locations.defined_location;
203            match (ref_loc, def_loc) {
204                (Location::UNKNOWN, Location::UNKNOWN) => Cow::Borrowed(msg.as_str()),
205                (r, d) if r != Location::UNKNOWN && (d == Location::UNKNOWN || d == r) => {
206                    Cow::Borrowed(msg.as_str())
207                }
208                (_r, d) => Cow::Owned(format!("{msg}{}", l10n.alias_defined_at(d))),
209            }
210        }
211
212        #[cfg(feature = "garde")]
213        Error::ValidationError { report, locations } => {
214            let l10n = formatter.localizer();
215
216            let issues = collect_garde_issues(report);
217            let mut lines = Vec::with_capacity(issues.len());
218            for issue in issues {
219                let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Garde);
220                let path_key = issue.path;
221                let original_leaf = path_key
222                    .leaf_string()
223                    .unwrap_or_else(|| l10n.root_path_label().into_owned());
224
225                let (locs, resolved_leaf) = locations
226                    .search(&path_key)
227                    .unwrap_or((Locations::UNKNOWN, original_leaf));
228
229                let loc = if locs.reference_location != Location::UNKNOWN {
230                    locs.reference_location
231                } else {
232                    locs.defined_location
233                };
234
235                let resolved_path = format_path_with_resolved_leaf(&path_key, &resolved_leaf);
236
237                lines.push(l10n.validation_issue_line(
238                    &resolved_path,
239                    &entry,
240                    (loc != Location::UNKNOWN).then_some(loc),
241                ));
242            }
243            Cow::Owned(l10n.join_validation_issues(&lines))
244        }
245        #[cfg(feature = "garde")]
246        Error::ValidationErrors { errors } => {
247            Cow::Owned(format!("validation failed for {} document(s)", errors.len()))
248        }
249        #[cfg(feature = "validator")]
250        Error::ValidatorError { errors, locations } => {
251            let l10n = formatter.localizer();
252
253            let issues = collect_validator_issues(errors);
254            let mut lines = Vec::with_capacity(issues.len());
255            for issue in issues {
256                let entry = issue.display_entry_overridden(l10n, ExternalMessageSource::Validator);
257                let path_key = issue.path;
258                let original_leaf = path_key
259                    .leaf_string()
260                    .unwrap_or_else(|| l10n.root_path_label().into_owned());
261
262                let (locs, resolved_leaf) = locations
263                    .search(&path_key)
264                    .unwrap_or((Locations::UNKNOWN, original_leaf));
265
266                let loc = if locs.reference_location != Location::UNKNOWN {
267                    locs.reference_location
268                } else {
269                    locs.defined_location
270                };
271
272                let resolved_path = format_path_with_resolved_leaf(&path_key, &resolved_leaf);
273
274                lines.push(l10n.validation_issue_line(
275                    &resolved_path,
276                    &entry,
277                    (loc != Location::UNKNOWN).then_some(loc),
278                ));
279            }
280            Cow::Owned(l10n.join_validation_issues(&lines))
281        }
282        #[cfg(feature = "validator")]
283        Error::ValidatorErrors { errors } => {
284            Cow::Owned(format!("validation failed for {} document(s)", errors.len()))
285        }
286    }
287}
288
289impl MessageFormatter for DefaultMessageFormatter {
290    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
291        default_format_message(self, err)
292    }
293}
294
295pub struct DefaultMessageFormatterWithLocalizer<'a> {
296    localizer: &'a dyn Localizer,
297}
298
299impl MessageFormatter for DefaultMessageFormatterWithLocalizer<'_> {
300    fn localizer(&self) -> &dyn Localizer {
301        self.localizer
302    }
303
304    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
305        default_format_message(self, err)
306    }
307}
308
309impl DefaultMessageFormatter {
310    /// Return a formatter that uses a custom [`Localizer`].
311    ///
312    /// This allows reusing the built-in developer-oriented messages while customizing
313    /// wording that is produced outside `format_message` (location suffixes, validation
314    /// issue composition, snippet labels, etc.).
315    pub fn with_localizer<'a>(
316        &self,
317        localizer: &'a dyn Localizer,
318    ) -> DefaultMessageFormatterWithLocalizer<'a> {
319        DefaultMessageFormatterWithLocalizer { localizer }
320    }
321}
322
323fn user_format_message<'a>(formatter: &dyn MessageFormatter, err: &'a Error) -> Cow<'a, str> {
324    if let Error::WithSnippet { error, .. } = err {
325        return user_format_message(formatter, error);
326    }
327
328    match err {
329        // handled by early return above
330        Error::WithSnippet { .. } => unreachable!(),
331
332        Error::Eof { .. } => Cow::Borrowed("unexpected end of file"),
333        Error::MultipleDocuments { .. } => {
334            Cow::Borrowed("only single YAML document expected but multiple found")
335        }
336        Error::InvalidUtf8Input => Cow::Borrowed("YAML parser input is not valid UTF-8"),
337        Error::BinaryNotUtf8 { .. } => {
338            Cow::Borrowed("!!binary scalar is not valid UTF-8 so cannot be stored into string.")
339        }
340        Error::InvalidBooleanStrict { .. } => Cow::Borrowed("invalid boolean (true or false expected)"),
341        Error::NullIntoString { .. } | Error::InvalidCharNull { .. } => Cow::Borrowed("null is not allowed here"),
342        Error::InvalidCharNotSingleScalar { .. } => Cow::Borrowed("only single character allowed here"),
343        Error::BytesNotSupportedMissingBinaryTag { .. } => Cow::Borrowed("missing !!binary tag"),
344        Error::ExpectedEmptyMappingForUnitStruct { .. } => Cow::Borrowed("expected empty mapping here"),
345        Error::UnexpectedContainerEndWhileSkippingNode { .. } => Cow::Borrowed("unexpected container end"),
346        Error::AliasReplayCounterOverflow { .. } => Cow::Borrowed("YAML document too large or too complex"),
347        Error::AliasReplayLimitExceeded {
348            total_replayed_events,
349            max_total_replayed_events,
350            ..
351        } => Cow::Owned(format!(
352            "YAML document too large or too complex: total_replayed_events={total_replayed_events} > {max_total_replayed_events}"
353        )),
354        Error::AliasExpansionLimitExceeded {
355            anchor_id,
356            expansions,
357            max_expansions_per_anchor,
358            ..
359        } => Cow::Owned(format!(
360            "YAML document too large or too complex: anchor id {anchor_id}: {expansions} > {max_expansions_per_anchor}"
361        )),
362        Error::AliasReplayStackDepthExceeded { depth, max_depth, .. } => Cow::Owned(format!(
363            "YAML document too large or too complex: depth={depth} > {max_depth}"
364        )),
365        Error::UnknownAnchor { .. } => Cow::Borrowed("reference to unknown value"),
366        Error::RecursiveReferencesRequireWeakTypes { .. } => Cow::Borrowed("Recursive reference not allowed here"),
367        Error::DuplicateMappingKey { key, .. } => match key {
368            Some(k) => Cow::Owned(format!("duplicate mapping key: {k} not allowed here")),
369            None => Cow::Borrowed("duplicate mapping key not allowed here"),
370        },
371        Error::QuotingRequired { .. } => Cow::Borrowed("value requires quoting"),
372        Error::Budget { breach, .. } => Cow::Owned(format!(
373            "YAML document too large or too complex: limits breached: {breach:?}"
374        )),
375        Error::CannotBorrowTransformedString { .. } => {
376            Cow::Borrowed("Only single string with no escape sequences is allowed here")
377        }
378
379        // All cases when the standard message is good enough.
380        _ => default_format_message(formatter, err),
381    }
382}
383
384impl MessageFormatter for UserMessageFormatter {
385    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
386        user_format_message(self, err)
387    }
388}
389
390pub struct UserMessageFormatterWithLocalizer<'a> {
391    localizer: &'a dyn Localizer,
392}
393
394impl MessageFormatter for UserMessageFormatterWithLocalizer<'_> {
395    fn localizer(&self) -> &dyn Localizer {
396        self.localizer
397    }
398
399    fn format_message<'a>(&self, err: &'a Error) -> Cow<'a, str> {
400        user_format_message(self, err)
401    }
402}
403
404impl UserMessageFormatter {
405    /// Return a formatter that uses a custom [`Localizer`].
406    ///
407    /// This allows reusing the built-in user-facing messages while customizing wording
408    /// that is produced outside `format_message` (location suffixes, validation issue
409    /// composition, snippet labels, etc.).
410    pub fn with_localizer<'a>(
411        &self,
412        localizer: &'a dyn Localizer,
413    ) -> UserMessageFormatterWithLocalizer<'a> {
414        UserMessageFormatterWithLocalizer { localizer }
415    }
416}
417