noyalib/i18n.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 Noyalib. All rights reserved.
3
4//! Pluggable error-message formatters for user-facing rendering.
5//!
6//! [`crate::Error`]'s `Display` impl is developer-facing — it
7//! preserves the noyalib-internal vocabulary (`!!binary`, "merge
8//! key", "recursion depth limit") that's useful for debugging but
9//! noisy for end-users of a config-loading binary. The
10//! `MessageFormatter` trait lets callers plug in their own
11//! formatting strategy: localisation tables, simplification,
12//! richer formatting, or anything else.
13//!
14//! Two implementations ship in-tree:
15//!
16//! * `DefaultFormatter` — preserves the standard developer-facing
17//! message verbatim. Equivalent to `format!("{err}")`.
18//! * `UserFormatter` — collapses noyalib's diagnostic vocabulary
19//! into short user-friendly sentences ("The configuration file
20//! has a syntax error on line 5.") suitable for surfacing in
21//! GUIs and `--help`-style command output.
22//!
23//! Use [`crate::Error::render_with_formatter`] to render an error
24//! through a chosen formatter.
25//!
26//! # Examples
27//!
28//! ```
29//! use noyalib::i18n::{DefaultFormatter, UserFormatter};
30//! use noyalib::{from_str, Value};
31//!
32//! let err = from_str::<Value>("a: [unclosed").unwrap_err();
33//! let dev = err.render_with_formatter(&DefaultFormatter);
34//! let user = err.render_with_formatter(&UserFormatter);
35//! assert!(!dev.is_empty());
36//! assert!(!user.is_empty());
37//! ```
38
39use crate::error::Error;
40use crate::prelude::*;
41
42/// Pluggable formatter for converting an [`Error`] into a
43/// user-visible message.
44///
45/// Implement this trait to plug in localisation, simplification,
46/// or rich formatting strategies. The trait is `Send + Sync` so a
47/// single formatter instance can be shared across threads.
48pub trait MessageFormatter: Send + Sync {
49 /// Render the supplied error as a single-string message.
50 fn format(&self, error: &Error) -> String;
51}
52
53/// Default formatter — preserves the standard developer-facing
54/// message verbatim.
55///
56/// Equivalent to `format!("{err}")`. The reference implementation
57/// `MessageFormatter` consumers should compare against.
58///
59/// # Examples
60///
61/// ```
62/// use noyalib::i18n::{DefaultFormatter, MessageFormatter};
63/// use noyalib::{from_str, Value};
64///
65/// let err = from_str::<Value>("a: [unclosed").unwrap_err();
66/// let s = DefaultFormatter.format(&err);
67/// assert_eq!(s, err.to_string());
68/// ```
69#[derive(Debug, Default, Clone, Copy)]
70pub struct DefaultFormatter;
71
72impl MessageFormatter for DefaultFormatter {
73 fn format(&self, error: &Error) -> String {
74 error.to_string()
75 }
76}
77
78/// User-facing formatter — collapses noyalib's diagnostic
79/// vocabulary into short, plain-language sentences appropriate
80/// for non-developer audiences (CLI `--help` text, GUI alert
81/// dialogs).
82///
83/// Maps the major [`Error`] variants onto user-readable
84/// templates:
85///
86/// | Variant family | User message |
87/// | :--- | :--- |
88/// | `Parse`, `ParseWithLocation` | `"The configuration file has a syntax error at line N."` |
89/// | `Deserialize`, `DeserializeWithLocation` | `"The configuration file does not match the expected shape."` |
90/// | `Io` | `"Could not read the configuration file."` |
91/// | `RecursionLimitExceeded`, `Budget`, `RepetitionLimitExceeded` | `"The configuration file is too large or deeply nested."` |
92/// | `DuplicateKey` | `"A configuration key appears twice."` |
93/// | `UnknownAnchor`, `UnknownAnchorAt` | `"A configuration reference points at something that does not exist."` |
94/// | `MissingField` | `"A required configuration field is missing."` |
95/// | `TypeMismatch` | `"A configuration value has the wrong type."` |
96/// | other | `"The configuration file is invalid."` |
97///
98/// Line numbers are included when the source location is
99/// available; sensitive field names and noyalib internal terms
100/// (`!!binary`, "merge key") are stripped.
101///
102/// # Examples
103///
104/// ```
105/// use noyalib::i18n::{MessageFormatter, UserFormatter};
106/// use noyalib::{from_str, Value};
107///
108/// let err = from_str::<Value>("a: [unclosed").unwrap_err();
109/// let msg = UserFormatter.format(&err);
110/// assert!(msg.contains("syntax error"));
111/// ```
112#[derive(Debug, Default, Clone, Copy)]
113pub struct UserFormatter;
114
115impl MessageFormatter for UserFormatter {
116 fn format(&self, error: &Error) -> String {
117 match error {
118 Error::Parse(_) => "The configuration file has a syntax error.".to_string(),
119 Error::ParseWithLocation { location, .. } => format!(
120 "The configuration file has a syntax error at line {}.",
121 location.line()
122 ),
123 Error::Deserialize(_) => {
124 "The configuration file does not match the expected shape.".to_string()
125 }
126 Error::DeserializeWithLocation { location, .. } => format!(
127 "The configuration file does not match the expected shape at line {}.",
128 location.line()
129 ),
130 #[cfg(feature = "std")]
131 Error::Io(_) => "Could not read the configuration file.".to_string(),
132 Error::RecursionLimitExceeded { .. }
133 | Error::Budget(_)
134 | Error::RepetitionLimitExceeded => {
135 "The configuration file is too large or deeply nested.".to_string()
136 }
137 Error::DuplicateKey(_) => "A configuration key appears twice.".to_string(),
138 Error::UnknownAnchor(_) | Error::UnknownAnchorAt { .. } => {
139 "A configuration reference points at something that does not exist.".to_string()
140 }
141 Error::MissingField(_) => "A required configuration field is missing.".to_string(),
142 Error::TypeMismatch { .. } => "A configuration value has the wrong type.".to_string(),
143 _ => "The configuration file is invalid.".to_string(),
144 }
145 }
146}
147
148impl Error {
149 /// Render this error via a custom [`MessageFormatter`].
150 ///
151 /// Pairs with [`DefaultFormatter`] (developer-facing,
152 /// verbatim) and [`UserFormatter`] (user-facing, simplified).
153 /// Callers needing localisation or rich formatting plug in
154 /// their own `MessageFormatter` impl.
155 ///
156 /// # Examples
157 ///
158 /// ```
159 /// use noyalib::i18n::UserFormatter;
160 /// use noyalib::{from_str, Value};
161 ///
162 /// let err = from_str::<Value>("a: [unclosed").unwrap_err();
163 /// let msg = err.render_with_formatter(&UserFormatter);
164 /// assert!(msg.contains("syntax error"));
165 /// ```
166 #[must_use]
167 pub fn render_with_formatter(&self, formatter: &dyn MessageFormatter) -> String {
168 formatter.format(self)
169 }
170}