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