Skip to main content

ocpi_tariffs/
string.rs

1//! Case Insensitive String. Only printable ASCII allowed.
2
3use std::{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 Warning {
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 Warning {
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 crate::Warning for Warning {
61    fn id(&self) -> warning::Id {
62        match self {
63            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
64            Self::ContainsNonPrintableASCII => {
65                warning::Id::from_static("contains_non_printable_ascii")
66            }
67            Self::InvalidType => warning::Id::from_static("invalid_type"),
68            Self::InvalidLengthMax { .. } => warning::Id::from_static("invalid_length_max"),
69            Self::InvalidLengthExact { .. } => warning::Id::from_static("invalid_length_exact"),
70            Self::PreferUppercase => warning::Id::from_static("prefer_upper_case"),
71        }
72    }
73}
74
75/// String that can have `[0..=MAX_LEN]` bytes.
76///
77/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
78/// Case insensitivity is not enforced.
79///
80/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
81/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
82#[derive(Copy, Clone, Debug)]
83pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);
84
85impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
86    type Target = str;
87
88    fn deref(&self) -> &Self::Target {
89        self.0
90    }
91}
92
93impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "{}", self.0)
96    }
97}
98
99impl<const MAX_LEN: usize> IntoCaveat for CiMaxLen<'_, MAX_LEN> {
100    fn into_caveat<W: crate::Warning>(self, warnings: warning::Set<W>) -> Caveat<Self, W> {
101        Caveat::new(self, warnings)
102    }
103}
104
105impl<'buf, 'elem: 'buf, const MAX_LEN: usize> json::FromJson<'elem, 'buf>
106    for CiMaxLen<'buf, MAX_LEN>
107{
108    type Warning = Warning;
109
110    fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::Warning> {
111        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
112
113        if s.len() > MAX_LEN {
114            warnings.with_elem(Warning::InvalidLengthMax { length: MAX_LEN }, elem);
115        }
116
117        Ok(Self(s.0).into_caveat(warnings))
118    }
119}
120
121/// String that can have `LEN` bytes exactly.
122///
123/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
124/// Case insensitivity is not enforced.
125///
126/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
127/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
128#[derive(Copy, Clone, Debug)]
129pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);
130
131impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
132    type Target = str;
133
134    fn deref(&self) -> &Self::Target {
135        self.0
136    }
137}
138
139impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "{}", self.0)
142    }
143}
144
145impl<const LEN: usize> IntoCaveat for CiExactLen<'_, LEN> {
146    fn into_caveat<W: crate::Warning>(self, warnings: warning::Set<W>) -> Caveat<Self, W> {
147        Caveat::new(self, warnings)
148    }
149}
150
151impl<'buf, 'elem: 'buf, const LEN: usize> json::FromJson<'elem, 'buf> for CiExactLen<'buf, LEN> {
152    type Warning = Warning;
153
154    fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::Warning> {
155        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
156
157        if s.len() != LEN {
158            warnings.with_elem(Warning::InvalidLengthExact { length: LEN }, elem);
159        }
160
161        Ok(Self(s.0).into_caveat(warnings))
162    }
163}
164
165/// Case Insensitive String. Only printable ASCII allowed. (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed)
166///
167/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
168/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
169#[derive(Copy, Clone, Debug)]
170struct Base<'buf>(&'buf str);
171
172impl Deref for Base<'_> {
173    type Target = str;
174
175    fn deref(&self) -> &Self::Target {
176        self.0
177    }
178}
179
180impl fmt::Display for Base<'_> {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        write!(f, "{}", self.0)
183    }
184}
185
186impl IntoCaveat for Base<'_> {
187    fn into_caveat<W: crate::Warning>(self, warnings: warning::Set<W>) -> Caveat<Self, W> {
188        Caveat::new(self, warnings)
189    }
190}
191
192impl<'buf, 'elem: 'buf> json::FromJson<'elem, 'buf> for Base<'buf> {
193    type Warning = Warning;
194
195    fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::Warning> {
196        let mut warnings = warning::Set::new();
197        let Some(id) = elem.as_raw_str() else {
198            return warnings.bail(Warning::InvalidType, elem);
199        };
200
201        // We don't care about the details of any warnings the escapes in the Id may have.
202        // The Id should simply not have any escapes.
203        let s = id.has_escapes(elem).ignore_warnings();
204        let s = match s {
205            json::decode::PendingStr::NoEscapes(s) => {
206                if check_printable(s) {
207                    warnings.with_elem(Warning::ContainsNonPrintableASCII, elem);
208                }
209                s
210            }
211            json::decode::PendingStr::HasEscapes(escape_str) => {
212                warnings.with_elem(Warning::ContainsEscapeCodes, elem);
213                // We decode the escapes to check if any of the escapes result in non-printable ASCII.
214                let decoded = escape_str.decode_escapes(elem).ignore_warnings();
215
216                if check_printable(&decoded) {
217                    warnings.with_elem(Warning::ContainsNonPrintableASCII, elem);
218                }
219
220                escape_str.into_raw()
221            }
222        };
223
224        Ok(Self(s).into_caveat(warnings))
225    }
226}
227
228fn check_printable(s: &str) -> bool {
229    s.chars()
230        .any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
231}
232
233#[cfg(test)]
234mod test {
235    #![allow(
236        clippy::indexing_slicing,
237        reason = "unwraps are allowed anywhere in tests"
238    )]
239
240    use assert_matches::assert_matches;
241
242    use crate::{
243        json::{self, FromJson},
244        warning,
245    };
246
247    use super::{CiExactLen, CiMaxLen, Warning};
248
249    const LEN: usize = 3;
250
251    #[test]
252    fn should_parse_max_len() {
253        let input = "hel";
254        let (output, warnings) = test_max_len(input);
255        assert!(warnings.is_empty(), "{warnings:#?}");
256        assert_eq!(output, input);
257    }
258
259    #[test]
260    fn should_fail_on_max_len() {
261        let input = "hello";
262        let (output, warnings) = test_max_len(input);
263        let warnings = warnings.into_path_as_str_map();
264        let warnings = &warnings["$"];
265        let length = assert_matches!(
266            warnings.as_slice(),
267            [Warning::InvalidLengthMax { length }] => *length
268        );
269        assert_eq!(length, LEN);
270        assert_eq!(output, input);
271    }
272
273    #[test]
274    fn should_parse_exact_len() {
275        let input = "hel";
276        let (output, warnings) = test_expect_len(input);
277        assert!(warnings.is_empty(), "{warnings:#?}");
278        assert_eq!(output, input);
279    }
280
281    #[test]
282    fn should_fail_on_exact_len() {
283        let input = "hello";
284        let (output, warnings) = test_expect_len(input);
285        let warnings = warnings.into_path_as_str_map();
286        let warnings = &warnings["$"];
287        let length = assert_matches!(
288            warnings.as_slice(),
289            [Warning::InvalidLengthExact { length }] => *length
290        );
291        assert_eq!(length, LEN);
292        assert_eq!(output, input);
293    }
294
295    #[track_caller]
296    fn test_max_len(s: &str) -> (String, warning::Set<Warning>) {
297        let quoted_input = format!(r#""{s}""#);
298        let elem = json::parse(&quoted_input).unwrap();
299        let output = CiMaxLen::<'_, LEN>::from_json(&elem).unwrap();
300        let (output, warnings) = output.into_parts();
301        (output.to_string(), warnings)
302    }
303
304    #[track_caller]
305    fn test_expect_len(s: &str) -> (String, warning::Set<Warning>) {
306        let quoted_input = format!(r#""{s}""#);
307        let elem = json::parse(&quoted_input).unwrap();
308        let output = CiExactLen::<'_, LEN>::from_json(&elem).unwrap();
309        let (output, warnings) = output.into_parts();
310        (output.to_string(), warnings)
311    }
312}