Skip to main content

ferrocat_icu/
diagnostic_codes.rs

1//! Stable machine-readable diagnostic codes emitted by `ferrocat-icu`.
2//!
3//! The constants in this module are the canonical spellings used in
4//! [`crate::IcuDiagnostic::code`] and [`crate::MessageMetadataDiagnostic::code`].
5//! CI, editor integrations, and build tooling can match these constants instead
6//! of parsing human-readable diagnostic messages.
7
8// This block has a feature-gated fallback copy in
9// crates/ferrocat-po/src/diagnostic_codes.rs; a test there keeps both in sync.
10// sync(diagnostic-code-type): begin
11use std::fmt;
12use std::ops::Deref;
13
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17/// Stable machine-readable diagnostic code.
18///
19/// The wire format remains the plain code string. This wrapper makes diagnostic
20/// code fields distinct from arbitrary human-readable strings while preserving
21/// string comparisons through [`Self::as_str`], [`AsRef<str>`], and `PartialEq<&str>`.
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24#[cfg_attr(feature = "serde", serde(transparent))]
25pub struct DiagnosticCode(String);
26
27impl DiagnosticCode {
28    /// Creates a diagnostic code from its canonical string spelling.
29    #[must_use]
30    pub fn new(code: impl Into<String>) -> Self {
31        Self(code.into())
32    }
33
34    /// Returns the canonical string spelling.
35    #[must_use]
36    pub fn as_str(&self) -> &str {
37        &self.0
38    }
39}
40
41impl fmt::Display for DiagnosticCode {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str(self.as_str())
44    }
45}
46
47impl AsRef<str> for DiagnosticCode {
48    fn as_ref(&self) -> &str {
49        self.as_str()
50    }
51}
52
53impl Deref for DiagnosticCode {
54    type Target = str;
55
56    fn deref(&self) -> &Self::Target {
57        self.as_str()
58    }
59}
60
61impl From<&str> for DiagnosticCode {
62    fn from(code: &str) -> Self {
63        Self::new(code)
64    }
65}
66
67impl From<String> for DiagnosticCode {
68    fn from(code: String) -> Self {
69        Self::new(code)
70    }
71}
72
73impl PartialEq<&str> for DiagnosticCode {
74    fn eq(&self, other: &&str) -> bool {
75        self.as_str() == *other
76    }
77}
78
79impl PartialEq<DiagnosticCode> for &str {
80    fn eq(&self, other: &DiagnosticCode) -> bool {
81        *self == other.as_str()
82    }
83}
84// sync(diagnostic-code-type): end
85
86/// ICU compatibility diagnostic codes.
87pub mod icu {
88    /// Formatter kind is not supported by the target runtime.
89    pub const UNSUPPORTED_FORMATTER_KIND: &str = "icu.unsupported_formatter_kind";
90    /// Formatter style is not supported by the target runtime.
91    pub const UNSUPPORTED_FORMATTER_STYLE: &str = "icu.unsupported_formatter_style";
92    /// Translation omits an argument used by the source message.
93    pub const MISSING_ARGUMENT: &str = "icu.missing_argument";
94    /// Translation changes an argument kind used by the source message.
95    pub const ARGUMENT_KIND_CHANGED: &str = "icu.argument_kind_changed";
96    /// Translation adds an argument that is not present in the source message.
97    pub const EXTRA_ARGUMENT: &str = "icu.extra_argument";
98    /// Translation changes a formatter style used by the source message.
99    pub const FORMATTER_STYLE_CHANGED: &str = "icu.formatter_style_changed";
100    /// Translation omits a rich-text tag used by the source message.
101    pub const MISSING_TAG: &str = "icu.missing_tag";
102    /// Translation adds a rich-text tag that is not present in the source message.
103    pub const EXTRA_TAG: &str = "icu.extra_tag";
104    /// Translation omits a selector from a source `select` argument.
105    pub const MISSING_SELECT_SELECTOR: &str = "icu.missing_select_selector";
106    /// Translation adds a selector to a source `select` argument.
107    pub const EXTRA_SELECT_SELECTOR: &str = "icu.extra_select_selector";
108    /// Translation changes a source plural offset.
109    pub const PLURAL_OFFSET_CHANGED: &str = "icu.plural_offset_changed";
110    /// Translation omits a selector from a source plural argument.
111    pub const MISSING_PLURAL_SELECTOR: &str = "icu.missing_plural_selector";
112    /// ICU formatter uses an opaque pattern style.
113    pub const PATTERN_STYLE_DISCOURAGED: &str = "icu.pattern_style_discouraged";
114
115    /// All ICU compatibility diagnostic codes emitted by this crate.
116    pub const ALL: &[&str] = &[
117        UNSUPPORTED_FORMATTER_KIND,
118        UNSUPPORTED_FORMATTER_STYLE,
119        MISSING_ARGUMENT,
120        ARGUMENT_KIND_CHANGED,
121        EXTRA_ARGUMENT,
122        FORMATTER_STYLE_CHANGED,
123        MISSING_TAG,
124        EXTRA_TAG,
125        MISSING_SELECT_SELECTOR,
126        EXTRA_SELECT_SELECTOR,
127        PLURAL_OFFSET_CHANGED,
128        MISSING_PLURAL_SELECTOR,
129        PATTERN_STYLE_DISCOURAGED,
130    ];
131}
132
133/// Semantic message metadata diagnostic codes.
134pub mod metadata {
135    /// Metadata `msgid` is not valid ICU MessageFormat v1.
136    pub const INVALID_MSGID: &str = "metadata.invalid_msgid";
137    /// Metadata omits an argument parsed from `msgid`.
138    pub const MISSING_ARGUMENT: &str = "metadata.missing_argument";
139    /// Metadata declares an argument that is not used by `msgid`.
140    pub const EXTRA_ARGUMENT: &str = "metadata.extra_argument";
141    /// Metadata declares an argument kind that does not match `msgid`.
142    pub const ARGUMENT_KIND_MISMATCH: &str = "metadata.argument_kind_mismatch";
143    /// Metadata omits a rich-text tag parsed from `msgid`.
144    pub const MISSING_TAG: &str = "metadata.missing_tag";
145    /// Metadata declares a rich-text tag that is not used by `msgid`.
146    pub const EXTRA_TAG: &str = "metadata.extra_tag";
147    /// Metadata omits a selector parsed from `msgid`.
148    pub const MISSING_SELECTOR: &str = "metadata.missing_selector";
149    /// Metadata declares a selector that is not used by `msgid`.
150    pub const EXTRA_SELECTOR: &str = "metadata.extra_selector";
151    /// Metadata declares a selector kind that does not match `msgid`.
152    pub const SELECTOR_KIND_MISMATCH: &str = "metadata.selector_kind_mismatch";
153    /// Metadata omits a selector case parsed from `msgid`.
154    pub const MISSING_SELECTOR_CASE: &str = "metadata.missing_selector_case";
155    /// Metadata declares a selector case that is not used by `msgid`.
156    pub const EXTRA_SELECTOR_CASE: &str = "metadata.extra_selector_case";
157    /// Metadata declares a selector offset that does not match `msgid`.
158    pub const SELECTOR_OFFSET_MISMATCH: &str = "metadata.selector_offset_mismatch";
159
160    /// All semantic metadata diagnostic codes emitted by this crate.
161    pub const ALL: &[&str] = &[
162        INVALID_MSGID,
163        MISSING_ARGUMENT,
164        EXTRA_ARGUMENT,
165        ARGUMENT_KIND_MISMATCH,
166        MISSING_TAG,
167        EXTRA_TAG,
168        MISSING_SELECTOR,
169        EXTRA_SELECTOR,
170        SELECTOR_KIND_MISMATCH,
171        MISSING_SELECTOR_CASE,
172        EXTRA_SELECTOR_CASE,
173        SELECTOR_OFFSET_MISMATCH,
174    ];
175}
176
177#[cfg(test)]
178mod tests {
179    use super::DiagnosticCode;
180
181    #[test]
182    fn diagnostic_code_preserves_canonical_string_access() {
183        let code = DiagnosticCode::from(String::from("icu.missing_argument"));
184
185        assert_eq!(code.as_str(), "icu.missing_argument");
186        assert_eq!(code.as_ref(), "icu.missing_argument");
187        assert_eq!(&*code, "icu.missing_argument");
188        assert_eq!(code.to_string(), "icu.missing_argument");
189        assert_eq!(code, "icu.missing_argument");
190        assert_eq!("icu.missing_argument", code);
191    }
192}