Skip to main content

ocpi_kit/types/
string.rs

1//! `OcpiString` — the OCPI case-sensitive, printable-UTF-8 string type.
2
3use core::fmt;
4use core::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use super::text::{InvalidString, StringKind, check_printable_utf8};
9use super::validate::{Validate, Validator, ViolationCode};
10
11/// A case-sensitive OCPI string of at most `N` characters.
12///
13/// > *Case Sensitive String. Only printable UTF-8 allowed. (Non-printable characters like:
14/// > Carriage returns, Tabs, Line breaks, etc are not allowed)*
15///
16/// `N` is the maximum length from the spec's property table, so `OcpiString<255>` is the spec's
17/// `string(255)`.
18///
19/// # Characters, not bytes
20///
21/// The specification writes `string(N)` without saying whether `N` counts bytes or characters.
22/// This crate counts **Unicode scalar values** (`char`s), because the limits are clearly meant
23/// to bound what a human sees — `string(45)` for a street address is a display constraint, and
24/// counting bytes would silently halve the usable length of a Greek or Japanese address while
25/// leaving an English one untouched. [`OcpiString::len_bytes`] is available where the byte
26/// length matters, and [`OcpiString::is_conformant_in_bytes`] answers the stricter reading for
27/// peers known to enforce it.
28///
29/// # Strict and lenient construction
30///
31/// [`OcpiString::new`] and [`FromStr`] are strict; the infallible `From` conversions are
32/// lenient, like `Deserialize`. See [`CiString`](super::CiString#strict-and-lenient-construction).
33///
34/// Spec: 2.3.0 §types_string_type
35#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct OcpiString<const N: usize>(String);
37
38impl<const N: usize> OcpiString<N> {
39    /// The maximum length the spec allows for this string, in characters.
40    pub const MAX_LEN: usize = N;
41
42    /// Creates an `OcpiString`, enforcing the character set and the length limit.
43    ///
44    /// # Errors
45    ///
46    /// Returns [`InvalidString`] if `value` contains a control character or is longer than `N`
47    /// Unicode scalar values.
48    pub fn new(value: impl Into<String>) -> Result<Self, InvalidString> {
49        let value = value.into();
50        check_printable_utf8(&value, StringKind::Utf8)?;
51        let len = value.chars().count();
52        if len > N {
53            return Err(InvalidString::too_long(len, N, StringKind::Utf8));
54        }
55        Ok(Self(value))
56    }
57
58    /// Creates an `OcpiString` without enforcing anything.
59    ///
60    /// This is what `Deserialize` uses; see [`types::validate`](super::validate).
61    pub fn new_lenient(value: impl Into<String>) -> Self {
62        Self(value.into())
63    }
64
65    /// The string.
66    #[must_use]
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70
71    /// Consumes this string and yields the inner [`String`].
72    #[must_use]
73    pub fn into_string(self) -> String {
74        self.0
75    }
76
77    /// The length in Unicode scalar values, which is what `N` bounds.
78    #[must_use]
79    pub fn len(&self) -> usize {
80        self.0.chars().count()
81    }
82
83    /// The length in UTF-8 bytes.
84    #[must_use]
85    pub fn len_bytes(&self) -> usize {
86        self.0.len()
87    }
88
89    /// Whether the string is empty.
90    #[must_use]
91    pub fn is_empty(&self) -> bool {
92        self.0.is_empty()
93    }
94
95    /// Whether this value satisfies the spec constraints that [`OcpiString::new`] enforces.
96    #[must_use]
97    pub fn is_conformant(&self) -> bool {
98        self.len() <= N && check_printable_utf8(&self.0, StringKind::Utf8).is_ok()
99    }
100
101    /// Whether this value also fits `N` *bytes*, the stricter reading of `string(N)`.
102    ///
103    /// Use this when talking to a peer known to count bytes; ASCII-only values satisfy both
104    /// readings at once.
105    #[must_use]
106    pub fn is_conformant_in_bytes(&self) -> bool {
107        self.0.len() <= N && check_printable_utf8(&self.0, StringKind::Utf8).is_ok()
108    }
109
110    /// Re-types this string to a different maximum length.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`InvalidString`] if the value does not fit in `M` characters.
115    pub fn resize<const M: usize>(self) -> Result<OcpiString<M>, InvalidString> {
116        OcpiString::<M>::new(self.0)
117    }
118
119    /// The `#NA` sentinel the spec allows where a required string cannot be filled.
120    ///
121    /// Spec: 2.3.0 §transport_and_format_not_available
122    pub const NOT_AVAILABLE: &'static str = "#NA";
123
124    /// Whether this value is the `#NA` sentinel.
125    #[must_use]
126    pub fn is_not_available(&self) -> bool {
127        self.0 == Self::NOT_AVAILABLE
128    }
129}
130
131impl<const N: usize> Validate for OcpiString<N> {
132    fn validate_in(&self, v: &mut Validator) {
133        if let Err(e) = check_printable_utf8(&self.0, StringKind::Utf8) {
134            v.report(ViolationCode::IllegalCharacter, e.to_string());
135        }
136        let len = self.len();
137        if len > N {
138            v.report(ViolationCode::TooLong, format!("string({N}) holds {len} characters"));
139        }
140    }
141}
142
143impl<const N: usize> fmt::Display for OcpiString<N> {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        f.write_str(&self.0)
146    }
147}
148
149impl<const N: usize> fmt::Debug for OcpiString<N> {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        fmt::Debug::fmt(&self.0, f)
152    }
153}
154
155impl<const N: usize> AsRef<str> for OcpiString<N> {
156    fn as_ref(&self) -> &str {
157        &self.0
158    }
159}
160
161impl<const N: usize> core::ops::Deref for OcpiString<N> {
162    type Target = str;
163    fn deref(&self) -> &str {
164        &self.0
165    }
166}
167
168impl<const N: usize> FromStr for OcpiString<N> {
169    type Err = InvalidString;
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        Self::new(s)
172    }
173}
174
175// The infallible conversions are **lenient**, matching `Deserialize`; see [`CiString`].
176impl<const N: usize> From<&str> for OcpiString<N> {
177    fn from(s: &str) -> Self {
178        Self::new_lenient(s)
179    }
180}
181
182impl<const N: usize> From<String> for OcpiString<N> {
183    fn from(s: String) -> Self {
184        Self::new_lenient(s)
185    }
186}
187
188impl<const N: usize> Serialize for OcpiString<N> {
189    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
190        serializer.serialize_str(&self.0)
191    }
192}
193
194impl<'de, const N: usize> Deserialize<'de> for OcpiString<N> {
195    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
196        String::deserialize(deserializer).map(Self)
197    }
198}
199
200#[cfg(feature = "schema")]
201impl<const N: usize> schemars::JsonSchema for OcpiString<N> {
202    fn schema_name() -> std::borrow::Cow<'static, str> {
203        format!("String{N}").into()
204    }
205    fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
206        schemars::json_schema!({
207            "type": "string",
208            "maxLength": N,
209            "description": "OCPI string: case-sensitive, printable UTF-8 only",
210        })
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn counts_characters_not_bytes() {
220        // 5 characters, 10 bytes.
221        let s = OcpiString::<5>::new("日本語です").unwrap();
222        assert_eq!(s.len(), 5);
223        assert_eq!(s.len_bytes(), 15);
224        assert!(s.is_conformant());
225        assert!(!s.is_conformant_in_bytes(), "the byte reading is stricter");
226        assert!(OcpiString::<4>::new("日本語です").is_err());
227    }
228
229    #[test]
230    fn accepts_utf8_but_rejects_control_characters() {
231        assert!(OcpiString::<64>::new("Straße 12 — Küche 🚗").is_ok());
232        assert!(OcpiString::<64>::new("a\rb").is_err());
233    }
234
235    #[test]
236    fn deserialize_is_permissive() {
237        let s: OcpiString<2> = serde_json::from_str("\"much too long\"").unwrap();
238        assert_eq!(s.as_str(), "much too long");
239        assert_eq!(s.validate().unwrap_err().as_slice()[0].code, ViolationCode::TooLong);
240    }
241}