1#[cfg(test)]
4pub(crate) mod test;
5
6mod data;
7
8use std::fmt;
9
10#[doc(inline)]
11pub use data::Code;
12
13use crate::{
14 from_warning_all, json,
15 warning::{self, GatherWarnings as _},
16 IntoCaveat as _, Verdict,
17};
18
19#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
21pub enum Warning {
22 ContainsEscapeCodes,
24
25 Decode(json::decode::Warning),
27
28 PreferUpperCase,
30
31 InvalidCode,
33
34 InvalidType { type_found: json::ValueKind },
36
37 InvalidLength,
39
40 InvalidCodeXTS,
42
43 InvalidCodeXXX,
45}
46
47impl Warning {
48 fn invalid_type(elem: &json::Element<'_>) -> Self {
49 Self::InvalidType {
50 type_found: elem.value().kind(),
51 }
52 }
53}
54
55impl fmt::Display for Warning {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::ContainsEscapeCodes => write!(
59 f,
60 "The currency-code contains escape-codes but it does not need them.",
61 ),
62 Self::Decode(warning) => fmt::Display::fmt(warning, f),
63 Self::PreferUpperCase => write!(
64 f,
65 "The currency-code follows the ISO 4217 standard which states: the chars should be uppercase.",
66 ),
67 Self::InvalidCode => {
68 write!(f, "The currency-code is not a valid ISO 4217 code.")
69 }
70 Self::InvalidType { .. } => write!(f, "The currency-code should be a string."),
71 Self::InvalidLength => write!(f, "The currency-code follows the ISO 4217 standard which states: the code should be three chars."),
72 Self::InvalidCodeXTS => write!(
73 f,
74 "The currency-code is `XTS`. This is a code for testing only",
75 ),
76 Self::InvalidCodeXXX => write!(
77 f,
78 "The currency-code is `XXX`. This means there is no currency",
79 ),
80 }
81 }
82}
83
84impl crate::Warning for Warning {
85 fn id(&self) -> warning::Id {
86 match self {
87 Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
88 Self::Decode(kind) => kind.id(),
89 Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
90 Self::InvalidCode => warning::Id::from_static("invalid_code"),
91 Self::InvalidType { type_found } => {
92 warning::Id::from_string(format!("invalid_type({type_found})"))
93 }
94 Self::InvalidLength => warning::Id::from_static("invalid_length"),
95 Self::InvalidCodeXTS => warning::Id::from_static("invalid_code_xts"),
96 Self::InvalidCodeXXX => warning::Id::from_static("invalid_code_xxx"),
97 }
98 }
99}
100
101from_warning_all!(json::decode::Warning => Warning::Decode);
102
103impl json::FromJson<'_> for Code {
104 type Warning = Warning;
105
106 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
107 let mut warnings = warning::Set::new();
108 let value = elem.as_value();
109
110 let Some(s) = value.to_raw_str() else {
111 return warnings.bail(elem, Warning::invalid_type(elem));
112 };
113
114 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
115
116 let s = match pending_str {
117 json::PendingStr::NoEscapes(s) => s,
118 json::PendingStr::HasEscapes(_) => {
119 return warnings.bail(elem, Warning::ContainsEscapeCodes);
120 }
121 };
122
123 let bytes = s.as_bytes();
124
125 let [a, b, c] = bytes else {
127 return warnings.bail(elem, Warning::InvalidLength);
128 };
129
130 let triplet: [u8; 3] = [
131 a.to_ascii_uppercase(),
132 b.to_ascii_uppercase(),
133 c.to_ascii_uppercase(),
134 ];
135
136 if triplet != bytes {
137 warnings.insert(elem, Warning::PreferUpperCase);
138 }
139
140 let Some(code) = Code::from_alpha_3(triplet) else {
141 return warnings.bail(elem, Warning::InvalidCode);
142 };
143
144 if matches!(code, Code::Xts) {
145 warnings.insert(elem, Warning::InvalidCodeXTS);
146 } else if matches!(code, Code::Xxx) {
147 warnings.insert(elem, Warning::InvalidCodeXXX);
148 }
149
150 Ok(code.into_caveat(warnings))
151 }
152}
153
154impl fmt::Display for Code {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 f.write_str(self.into_str())
157 }
158}
159
160macro_rules! currency_codes {
162 [$(($name:ident, $alpha3:literal, $symbol:literal)),*] => {
163 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
165 pub enum Code {
166 $($name),*
167 }
168
169 impl Code {
170 pub(super) const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
172 match &code {
173 $($alpha3 => Some(Self::$name),)*
174 _ => None
175 }
176 }
177
178 pub fn into_str(self) -> &'static str {
180 let bytes = match self {
181 $(Self::$name => $alpha3),*
182 };
183 std::str::from_utf8(bytes).expect("The currency code bytes are known to be valid UTF8 as they are embedded into the binary")
184 }
185
186 pub fn into_symbol(self) -> &'static str {
188 match self {
189 $(Self::$name => $symbol),*
190 }
191 }
192 }
193 };
194}
195
196pub(crate) use currency_codes;