Skip to main content

serde_saphyr/de/
localizer.rs

1//! Localization / wording customization.
2//!
3//! The [`Localizer`] trait is the central hook for customizing *crate-authored wording*.
4//! It is intentionally designed to be low-boilerplate:
5//!
6//! - Every method has a reasonable English default.
7//! - You can override only the pieces you care about, while inheriting all other defaults.
8//!
9//! This crate may also show *external* message text coming from dependencies (for example
10//! `saphyr-parser` scan errors, or validator messages). Where such texts are used, the
11//! rendering pipeline should provide a best-effort opportunity to override them via
12//! [`Localizer::override_external_message`].
13//!
14//! ## Example: override a single phrase
15//!
16//! ```rust
17//! use serde_saphyr::{Error, Location};
18//! use serde_saphyr::localizer::{Localizer, DEFAULT_ENGLISH_LOCALIZER};
19//! use std::borrow::Cow;
20//!
21//! /// A wrapper that overrides only location suffix wording, delegating everything else.
22//! struct Pirate<'a> {
23//!     base: &'a dyn Localizer,
24//! }
25//!
26//! impl Localizer for Pirate<'_> {
27//!     fn attach_location<'b>(&self, base: Cow<'b, str>, loc: Location) -> Cow<'b, str> {
28//!         if loc == Location::UNKNOWN {
29//!             return base;
30//!         }
31//!         // Note: you can also delegate to `self.base.attach_location(...)` if you want.
32//!         Cow::Owned(format!(
33//!             "{base}. Bug lurks on line {}, then {} runes in",
34//!             loc.line(),
35//!             loc.column()
36//!         ))
37//!     }
38//! }
39//!
40//! // This snippet shows the customization building blocks; the crate's rendering APIs
41//! // obtain a `Localizer` via the `MessageFormatter`.
42//! # let _ = (Error::InvalidUtf8Input, &DEFAULT_ENGLISH_LOCALIZER);
43//! ```
44
45use crate::Location;
46use std::borrow::Cow;
47
48/// Where an “external” message comes from.
49///
50/// External messages are those primarily produced by dependencies (parser / validators).
51#[non_exhaustive]
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ExternalMessageSource {
54    /// Text produced by `saphyr-parser` (e.g. scanning errors).
55    SaphyrParser,
56    /// Text produced by `garde` validation rules.
57    Garde,
58    /// Text produced by `validator` validation rules.
59    Validator,
60}
61
62/// A best-effort description of an external message.
63///
64/// The crate should pass as much stable metadata as it has (e.g. `code` and `params` for
65/// `validator`) so the localizer can override *specific* messages without string matching.
66#[derive(Debug, Clone)]
67pub struct ExternalMessage<'a> {
68    pub source: ExternalMessageSource,
69    /// The original text as provided by the external library.
70    pub original: &'a str,
71    /// Stable-ish identifier when available (e.g. validator error code).
72    pub code: Option<&'a str>,
73    /// Optional structured parameters when available.
74    pub params: &'a [(String, String)],
75}
76
77/// All crate-authored wording customization points.
78///
79/// Implementors should typically override *only a few* methods.
80/// Everything else should default to English (via the default method bodies).
81pub trait Localizer {
82    // ---------------- Common tiny building blocks ----------------
83
84    /// Attach a location suffix to `base`.
85    ///
86    /// Renderers must use this instead of hard-coding English wording like
87    /// `" at line X, column Y"`.
88    ///
89    /// Default:
90    /// - If `loc == Location::UNKNOWN`: returns `base` unchanged.
91    /// - Otherwise: returns `"{base} at line {line}, column {column}"`.
92    fn attach_location<'a>(&self, base: Cow<'a, str>, loc: Location) -> Cow<'a, str> {
93        if loc == Location::UNKNOWN {
94            base
95        } else {
96            Cow::Owned(format!(
97                "{base} at line {}, column {}",
98                loc.line, loc.column
99            ))
100        }
101    }
102
103    /// Label used when a path has no leaf.
104    ///
105    /// Default <root>
106    fn root_path_label(&self) -> Cow<'static, str> {
107        Cow::Borrowed("<root>")
108    }
109
110    /// Suffix for alias-related errors when a distinct defined-location is available.
111    ///
112    /// Default wording matches the crate's historical English output:
113    /// `" (defined at line X, column Y)"`.
114    ///
115    /// Default: `format!(" (defined at line {line}, column {column})", ...)`.
116    fn alias_defined_at(&self, defined: Location) -> String {
117        format!(
118            " (defined at line {}, column {})",
119            defined.line, defined.column
120        )
121    }
122
123    // ---------------- Validation (plain text) glue ----------------
124
125    /// Render one validation issue line.
126    ///
127    /// The crate provides `resolved_path`, `entry` and the chosen `loc`.
128    ///
129    /// Default:
130    /// - Base text: `"validation error at {resolved_path}: {entry}"`.
131    /// - If `loc` is `Some` and not `Location::UNKNOWN`, appends a location suffix via
132    ///   [`Localizer::attach_location`].
133    fn validation_issue_line(
134        &self,
135        resolved_path: &str,
136        entry: &str,
137        loc: Option<Location>,
138    ) -> String {
139        let base = format!("validation error at {resolved_path}: {entry}");
140        match loc {
141            Some(l) if l != Location::UNKNOWN => {
142                self.attach_location(Cow::Owned(base), l).into_owned()
143            }
144            _ => base,
145        }
146    }
147
148    /// Join multiple validation issues into one message.
149    ///
150    /// Default: joins `lines` with a single newline (`"\n"`).
151    fn join_validation_issues(&self, lines: &[String]) -> String {
152        lines.join("\n")
153    }
154
155    // ---------------- Validation snippets / diagnostic labels ----------------
156
157    /// Label used for a snippet window when the location is known and considered the
158    /// “definition” site.
159    ///
160    /// Default: `"(defined)"`.
161    fn defined(&self) -> Cow<'static, str> {
162        Cow::Borrowed("(defined)")
163    }
164
165    /// Label used for a snippet window when we only have a “defined here” location.
166    ///
167    /// Default: `"(defined here)"`.
168    fn defined_here(&self) -> Cow<'static, str> {
169        Cow::Borrowed("(defined here)")
170    }
171
172    /// Label used for the primary snippet window when an aliased/anchored value is used
173    /// at a different location than where it was defined.
174    ///
175    /// Default: `"the value is used here"`.
176    fn value_used_here(&self) -> Cow<'static, str> {
177        Cow::Borrowed("the value is used here")
178    }
179
180    /// Label used for the secondary snippet window that points at the anchor definition.
181    ///
182    /// Default: `"defined here"`.
183    fn defined_window(&self) -> Cow<'static, str> {
184        Cow::Borrowed("defined here")
185    }
186
187    /// Compose the base validation message used in snippet rendering.
188    ///
189    /// Default: `"validation error: {entry} for `{resolved_path}`"`.
190    fn validation_base_message(&self, entry: &str, resolved_path: &str) -> String {
191        format!("validation error: {entry} for `{resolved_path}`")
192    }
193
194    /// Compose the “invalid here” prefix for the primary snippet message.
195    ///
196    /// Default: `"invalid here, {base}"`.
197    fn invalid_here(&self, base: &str) -> String {
198        format!("invalid here, {base}")
199    }
200
201    /// Intro line printed between the primary and secondary snippet windows for
202    /// anchor/alias (“indirect value”) cases.
203    ///
204    /// Default:
205    /// `"  | This value comes indirectly from the anchor at line {line} column {column}:"`.
206    fn value_comes_from_the_anchor(&self, def: Location) -> String {
207        format!(
208            "  | This value comes indirectly from the anchor at line {} column {}:",
209            def.line, def.column
210        )
211    }
212
213    // ---------------- External overrides ----------------
214
215    /// Optional hook to override the location prefix used for snippet titles
216    ///
217    /// Default:
218    /// - If `loc == Location::UNKNOWN`: returns an empty string.
219    /// - Otherwise: returns `"line {line} column {column}"`.
220    fn snippet_location_prefix(&self, loc: Location) -> String {
221        if loc == Location::UNKNOWN {
222            String::new()
223        } else {
224            format!("line {} column {}", loc.line(), loc.column())
225        }
226    }
227
228    /// Best-effort hook to override/translate dependency-provided message text.
229    ///
230    /// Default: returns `None` (keep the external message as-is).
231    fn override_external_message<'a>(&self, _msg: ExternalMessage<'a>) -> Option<Cow<'a, str>> {
232        None
233    }
234}
235
236/// Default English localizer used by the crate.
237#[derive(Debug, Default, Clone, Copy)]
238pub struct DefaultEnglishLocalizer;
239
240impl Localizer for DefaultEnglishLocalizer {}
241
242/// A single shared instance of the default English localizer.
243///
244/// This avoids repeated instantiation and provides a convenient reference for wrappers.
245pub static DEFAULT_ENGLISH_LOCALIZER: DefaultEnglishLocalizer = DefaultEnglishLocalizer;