ocpi_tariffs/
string.rs

1//! Case Insensitive String. Only printable ASCII allowed.
2
3use std::{borrow::Cow, fmt, ops::Deref};
4
5use crate::{
6    json,
7    warning::{self, IntoCaveat},
8    Caveat, Verdict,
9};
10
11/// The warnings that can happen when parsing a case-insensitive string.
12#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
13pub enum WarningKind {
14    /// There should be no escape codes in a `CiString`.
15    ContainsEscapeCodes,
16
17    /// There should only be printable ASCII bytes in a `CiString`.
18    ContainsNonPrintableASCII,
19
20    /// The JSON value given is not a string.
21    InvalidType,
22
23    /// The length of the string exceeds the specs constraint.
24    InvalidLengthMax { length: usize },
25
26    /// The length of the string is not equal to the specs constraint.
27    InvalidLengthExact { length: usize },
28
29    /// The casing of the string is not common practice.
30    ///
31    /// Note: This is not enforced by the string types in this module, but can be used
32    /// by linting code to signal that the casing of a given string is unorthodox.
33    PreferUppercase,
34}
35
36impl fmt::Display for WarningKind {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            Self::ContainsEscapeCodes => f.write_str("The string contains escape codes."),
40            Self::ContainsNonPrintableASCII => {
41                f.write_str("The string contains non-printable bytes.")
42            }
43            Self::InvalidType => f.write_str("The value should be a string."),
44            Self::InvalidLengthMax { length } => {
45                write!(
46                    f,
47                    "The string is longer than the max length `{length}` defined in the spec.",
48                )
49            }
50            Self::InvalidLengthExact { length } => {
51                write!(f, "The string should be length `{length}`.")
52            }
53            Self::PreferUppercase => {
54                write!(f, "Upper case is preffered")
55            }
56        }
57    }
58}
59
60impl warning::Kind for WarningKind {
61    fn id(&self) -> Cow<'static, str> {
62        match self {
63            Self::ContainsEscapeCodes => "contains_escape_codes".into(),
64            Self::ContainsNonPrintableASCII => "contains_non_printable_ascii".into(),
65            Self::InvalidType => "invalid_type".into(),
66            Self::InvalidLengthMax { .. } => "invalid_length_max".into(),
67            Self::InvalidLengthExact { .. } => "invalid_length_exact".into(),
68            Self::PreferUppercase => "prefer_upper_case".into(),
69        }
70    }
71}
72
73/// String that can have `[0..=MAX_LEN]` bytes.
74///
75/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
76/// Case insensitivity is not enforced.
77///
78/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
79/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
80#[derive(Copy, Clone, Debug)]
81pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);
82
83impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
84    type Target = str;
85
86    fn deref(&self) -> &Self::Target {
87        self.0
88    }
89}
90
91impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(f, "{}", self.0)
94    }
95}
96
97impl<const MAX_LEN: usize> IntoCaveat for CiMaxLen<'_, MAX_LEN> {
98    fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
99        Caveat::new(self, warnings)
100    }
101}
102
103impl<'buf, 'elem: 'buf, const MAX_LEN: usize> json::FromJson<'elem, 'buf>
104    for CiMaxLen<'buf, MAX_LEN>
105{
106    type WarningKind = WarningKind;
107
108    fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::WarningKind> {
109        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
110
111        if s.len() > MAX_LEN {
112            warnings.with_elem(WarningKind::InvalidLengthMax { length: MAX_LEN }, elem);
113        }
114
115        Ok(Self(s.0).into_caveat(warnings))
116    }
117}
118
119/// String that can have `LEN` bytes exactly.
120///
121/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
122/// Case insensitivity is not enforced.
123///
124/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
125/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
126#[derive(Copy, Clone, Debug)]
127pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);
128
129impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
130    type Target = str;
131
132    fn deref(&self) -> &Self::Target {
133        self.0
134    }
135}
136
137impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        write!(f, "{}", self.0)
140    }
141}
142
143impl<const LEN: usize> IntoCaveat for CiExactLen<'_, LEN> {
144    fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
145        Caveat::new(self, warnings)
146    }
147}
148
149impl<'buf, 'elem: 'buf, const LEN: usize> json::FromJson<'elem, 'buf> for CiExactLen<'buf, LEN> {
150    type WarningKind = WarningKind;
151
152    fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::WarningKind> {
153        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
154
155        if s.len() != LEN {
156            warnings.with_elem(WarningKind::InvalidLengthExact { length: LEN }, elem);
157        }
158
159        Ok(Self(s.0).into_caveat(warnings))
160    }
161}
162
163/// Case Insensitive String. Only printable ASCII allowed. (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed)
164///
165/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
166/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
167#[derive(Copy, Clone, Debug)]
168struct Base<'buf>(&'buf str);
169
170impl Deref for Base<'_> {
171    type Target = str;
172
173    fn deref(&self) -> &Self::Target {
174        self.0
175    }
176}
177
178impl fmt::Display for Base<'_> {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        write!(f, "{}", self.0)
181    }
182}
183
184impl IntoCaveat for Base<'_> {
185    fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
186        Caveat::new(self, warnings)
187    }
188}
189
190impl<'buf, 'elem: 'buf> json::FromJson<'elem, 'buf> for Base<'buf> {
191    type WarningKind = WarningKind;
192
193    fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::WarningKind> {
194        let mut warnings = warning::Set::new();
195        let Some(id) = elem.as_raw_str() else {
196            warnings.with_elem(WarningKind::InvalidType, elem);
197            return Err(warnings);
198        };
199
200        // We don't care about the details of any warnings the escapes in the Id may have.
201        // The Id should simply not have any escapes.
202        let s = id.has_escapes(elem).ignore_warnings();
203        let s = match s {
204            json::decode::PendingStr::NoEscapes(s) => {
205                if check_printable(s) {
206                    warnings.with_elem(WarningKind::ContainsNonPrintableASCII, elem);
207                }
208                s
209            }
210            json::decode::PendingStr::HasEscapes(escape_str) => {
211                warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
212                // We decode the escapes to check if any of the escapes result in non-printable ASCII.
213                let decoded = escape_str.decode_escapes(elem).ignore_warnings();
214
215                if check_printable(&decoded) {
216                    warnings.with_elem(WarningKind::ContainsNonPrintableASCII, elem);
217                }
218
219                escape_str.into_raw()
220            }
221        };
222
223        Ok(Self(s).into_caveat(warnings))
224    }
225}
226
227fn check_printable(s: &str) -> bool {
228    s.chars()
229        .any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
230}
231
232#[cfg(test)]
233mod test {
234    use assert_matches::assert_matches;
235
236    use crate::{
237        json::{self, FromJson},
238        warning,
239    };
240
241    use super::{CiExactLen, CiMaxLen, WarningKind};
242
243    const LEN: usize = 3;
244
245    #[test]
246    fn should_parse_max_len() {
247        let input = "hel";
248        let (output, warnings) = test_max_len(input);
249        assert_matches!(warnings.into_kind_vec().as_slice(), []);
250        assert_eq!(output, input);
251    }
252
253    #[test]
254    fn should_fail_on_max_len() {
255        let input = "hello";
256        let (output, warnings) = test_max_len(input);
257        let length = assert_matches!(
258            warnings.into_kind_vec().as_slice(),
259            [WarningKind::InvalidLengthMax { length }] => *length
260        );
261        assert_eq!(length, LEN);
262        assert_eq!(output, input);
263    }
264
265    #[test]
266    fn should_parse_exact_len() {
267        let input = "hel";
268        let (output, warnings) = test_expect_len(input);
269        assert_matches!(warnings.into_kind_vec().as_slice(), []);
270        assert_eq!(output, input);
271    }
272
273    #[test]
274    fn should_fail_on_exact_len() {
275        let input = "hello";
276        let (output, warnings) = test_expect_len(input);
277        let length = assert_matches!(
278            warnings.into_kind_vec().as_slice(),
279            [WarningKind::InvalidLengthExact { length }] => *length
280        );
281        assert_eq!(length, LEN);
282        assert_eq!(output, input);
283    }
284
285    #[track_caller]
286    fn test_max_len(s: &str) -> (String, warning::Set<WarningKind>) {
287        let quoted_input = format!(r#""{s}""#);
288        let elem = json::parse(&quoted_input).unwrap();
289        let output = CiMaxLen::<'_, LEN>::from_json(&elem).unwrap();
290        let (output, warnings) = output.into_parts();
291        (output.to_string(), warnings)
292    }
293
294    #[track_caller]
295    fn test_expect_len(s: &str) -> (String, warning::Set<WarningKind>) {
296        let quoted_input = format!(r#""{s}""#);
297        let elem = json::parse(&quoted_input).unwrap();
298        let output = CiExactLen::<'_, LEN>::from_json(&elem).unwrap();
299        let (output, warnings) = output.into_parts();
300        (output.to_string(), warnings)
301    }
302}