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 as _},
11    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    /// Create a `Warning` for invalid type.
41    fn invalid_type(elem: &json::Element<'_>) -> Self {
42        Self::InvalidType {
43            type_found: elem.value().kind(),
44        }
45    }
46}
47
48impl fmt::Display for Warning {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::ContainsEscapeCodes => f.write_str("The string contains escape codes."),
52            Self::ContainsNonPrintableASCII => {
53                f.write_str("The string contains non-printable bytes.")
54            }
55            Self::InvalidType { type_found } => {
56                write!(f, "The value should be a string but is `{type_found}`")
57            }
58            Self::InvalidLengthMax { length } => {
59                write!(
60                    f,
61                    "The string is longer than the max length `{length}` defined in the spec.",
62                )
63            }
64            Self::InvalidLengthExact { length } => {
65                write!(f, "The string should be length `{length}`.")
66            }
67            Self::PreferUppercase => {
68                write!(f, "Upper case is preferred")
69            }
70        }
71    }
72}
73
74impl crate::Warning for Warning {
75    fn id(&self) -> warning::Id {
76        match self {
77            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
78            Self::ContainsNonPrintableASCII => {
79                warning::Id::from_static("contains_non_printable_ascii")
80            }
81            Self::InvalidType { type_found } => {
82                warning::Id::from_string(format!("invalid_type({type_found})"))
83            }
84            Self::InvalidLengthMax { .. } => warning::Id::from_static("invalid_length_max"),
85            Self::InvalidLengthExact { .. } => warning::Id::from_static("invalid_length_exact"),
86            Self::PreferUppercase => warning::Id::from_static("prefer_upper_case"),
87        }
88    }
89}
90
91/// String that can have `[0..=MAX_LEN]` bytes.
92///
93/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
94/// Case insensitivity is not enforced.
95///
96/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>.
97/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>.
98#[derive(Copy, Clone, Debug)]
99pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);
100
101impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
102    type Target = str;
103
104    fn deref(&self) -> &Self::Target {
105        self.0
106    }
107}
108
109impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.0)
112    }
113}
114
115impl<'buf, const MAX_LEN: usize> json::FromJson<'buf> for CiMaxLen<'buf, MAX_LEN> {
116    type Warning = Warning;
117
118    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
119        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
120
121        if s.len() > MAX_LEN {
122            warnings.insert(Warning::InvalidLengthMax { length: MAX_LEN }, elem);
123        }
124
125        Ok(Self(s.0).into_caveat(warnings))
126    }
127}
128
129/// String that can have `LEN` bytes exactly.
130///
131/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
132/// Case insensitivity is not enforced.
133///
134/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>.
135/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>.
136#[derive(Copy, Clone, Debug)]
137pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);
138
139impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
140    type Target = str;
141
142    fn deref(&self) -> &Self::Target {
143        self.0
144    }
145}
146
147impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}", self.0)
150    }
151}
152
153impl<'buf, const LEN: usize> json::FromJson<'buf> for CiExactLen<'buf, LEN> {
154    type Warning = Warning;
155
156    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
157        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
158
159        if s.len() != LEN {
160            warnings.insert(Warning::InvalidLengthExact { length: LEN }, elem);
161        }
162
163        Ok(Self(s.0).into_caveat(warnings))
164    }
165}
166
167/// Case Insensitive String. Only printable ASCII allowed. (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed).
168///
169/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>.
170/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>.
171#[derive(Copy, Clone, Debug)]
172struct Base<'buf>(&'buf str);
173
174impl Deref for Base<'_> {
175    type Target = str;
176
177    fn deref(&self) -> &Self::Target {
178        self.0
179    }
180}
181
182impl fmt::Display for Base<'_> {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        write!(f, "{}", self.0)
185    }
186}
187
188impl<'buf> json::FromJson<'buf> for Base<'buf> {
189    type Warning = Warning;
190
191    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
192        let mut warnings = warning::Set::new();
193        let Some(id) = elem.to_raw_str() else {
194            return warnings.bail(Warning::invalid_type(elem), elem);
195        };
196
197        // We don't care about the details of any warnings the escapes in the Id may have.
198        // The Id should simply not have any escapes.
199        let s = id.has_escapes(elem).ignore_warnings();
200        let s = match s {
201            json::decode::PendingStr::NoEscapes(s) => {
202                if check_printable(s) {
203                    warnings.insert(Warning::ContainsNonPrintableASCII, elem);
204                }
205                s
206            }
207            json::decode::PendingStr::HasEscapes(escape_str) => {
208                warnings.insert(Warning::ContainsEscapeCodes, elem);
209                // We decode the escapes to check if any of the escapes result in non-printable ASCII.
210                let decoded = escape_str.decode_escapes(elem).ignore_warnings();
211
212                if check_printable(&decoded) {
213                    warnings.insert(Warning::ContainsNonPrintableASCII, elem);
214                }
215
216                escape_str.into_raw()
217            }
218        };
219
220        Ok(Self(s).into_caveat(warnings))
221    }
222}
223
224/// Return true if the given `str` has any Non-printable characters like:
225/// Carriage returns, Tabs, Line breaks, etc.
226fn check_printable(s: &str) -> bool {
227    s.chars()
228        .any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
229}