Skip to main content

ocpi_tariffs/
string.rs

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