ocpi_kit/types/
cistring.rs1use core::fmt;
4use core::hash::{Hash, Hasher};
5use core::str::FromStr;
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::text::{InvalidString, StringKind, check_printable_ascii};
10use super::validate::{Validate, Validator, ViolationCode};
11
12#[derive(Clone, Default)]
61pub struct CiString<const N: usize>(String);
62
63impl<const N: usize> CiString<N> {
64 pub const MAX_LEN: usize = N;
66
67 pub fn new(value: impl Into<String>) -> Result<Self, InvalidString> {
74 let value = value.into();
75 check_printable_ascii(&value, StringKind::Ci)?;
76 if value.len() > N {
77 return Err(InvalidString::too_long(value.len(), N, StringKind::Ci));
78 }
79 Ok(Self(value))
80 }
81
82 pub fn new_lenient(value: impl Into<String>) -> Self {
88 Self(value.into())
89 }
90
91 #[must_use]
93 pub fn as_str(&self) -> &str {
94 &self.0
95 }
96
97 #[must_use]
99 pub fn into_string(self) -> String {
100 self.0
101 }
102
103 #[must_use]
105 pub fn len(&self) -> usize {
106 self.0.len()
107 }
108
109 #[must_use]
111 pub fn is_empty(&self) -> bool {
112 self.0.is_empty()
113 }
114
115 #[must_use]
117 pub fn is_conformant(&self) -> bool {
118 self.0.len() <= N && check_printable_ascii(&self.0, StringKind::Ci).is_ok()
119 }
120
121 pub fn resize<const M: usize>(self) -> Result<CiString<M>, InvalidString> {
130 CiString::<M>::new(self.0)
131 }
132
133 #[must_use]
135 pub fn eq_ignore_case(&self, other: &str) -> bool {
136 self.0.eq_ignore_ascii_case(other)
137 }
138
139 pub const NOT_AVAILABLE: &'static str = "#NA";
143
144 #[must_use]
148 pub fn is_not_available(&self) -> bool {
149 self.0.eq_ignore_ascii_case(Self::NOT_AVAILABLE)
150 }
151}
152
153impl<const N: usize> Validate for CiString<N> {
154 fn validate_in(&self, v: &mut Validator) {
155 if let Err(e) = check_printable_ascii(&self.0, StringKind::Ci) {
156 v.report(ViolationCode::IllegalCharacter, e.to_string());
157 }
158 if self.0.len() > N {
159 v.report(ViolationCode::TooLong, format!("CiString({N}) holds {} characters", self.0.len()));
160 }
161 }
162}
163
164impl<const N: usize> fmt::Display for CiString<N> {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(&self.0)
167 }
168}
169
170impl<const N: usize> fmt::Debug for CiString<N> {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 fmt::Debug::fmt(&self.0, f)
173 }
174}
175
176impl<const N: usize> PartialEq for CiString<N> {
177 fn eq(&self, other: &Self) -> bool {
178 self.0.eq_ignore_ascii_case(&other.0)
179 }
180}
181
182impl<const N: usize> Eq for CiString<N> {}
183
184impl<const N: usize> PartialOrd for CiString<N> {
185 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
186 Some(self.cmp(other))
187 }
188}
189
190impl<const N: usize> Ord for CiString<N> {
191 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
192 let a = self.0.bytes().map(|b| b.to_ascii_lowercase());
193 let b = other.0.bytes().map(|b| b.to_ascii_lowercase());
194 a.cmp(b)
195 }
196}
197
198impl<const N: usize> Hash for CiString<N> {
199 fn hash<H: Hasher>(&self, state: &mut H) {
200 for byte in self.0.bytes() {
202 state.write_u8(byte.to_ascii_lowercase());
203 }
204 state.write_u8(0xff);
205 }
206}
207
208impl<const N: usize> AsRef<str> for CiString<N> {
209 fn as_ref(&self) -> &str {
210 &self.0
211 }
212}
213
214impl<const N: usize> core::ops::Deref for CiString<N> {
215 type Target = str;
216 fn deref(&self) -> &str {
217 &self.0
218 }
219}
220
221impl<const N: usize> FromStr for CiString<N> {
222 type Err = InvalidString;
223 fn from_str(s: &str) -> Result<Self, Self::Err> {
224 Self::new(s)
225 }
226}
227
228impl<const N: usize> From<&str> for CiString<N> {
230 fn from(s: &str) -> Self {
231 Self::new_lenient(s)
232 }
233}
234
235impl<const N: usize> From<String> for CiString<N> {
236 fn from(s: String) -> Self {
237 Self::new_lenient(s)
238 }
239}
240
241impl<const N: usize> Serialize for CiString<N> {
242 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
243 serializer.serialize_str(&self.0)
244 }
245}
246
247impl<'de, const N: usize> Deserialize<'de> for CiString<N> {
248 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
249 String::deserialize(deserializer).map(Self)
250 }
251}
252
253#[cfg(feature = "schema")]
254impl<const N: usize> schemars::JsonSchema for CiString<N> {
255 fn schema_name() -> std::borrow::Cow<'static, str> {
256 format!("CiString{N}").into()
257 }
258 fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
259 schemars::json_schema!({
260 "type": "string",
261 "maxLength": N,
262 "pattern": "^[\\u0020-\\u007E]*$",
263 "description": "OCPI CiString: case-insensitive, printable ASCII only",
264 })
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use std::collections::HashSet;
272
273 #[test]
274 fn rejects_non_printable_and_non_ascii() {
275 assert!(CiString::<36>::new("ok").is_ok());
276 assert!(CiString::<36>::new("no\nnewline").is_err());
277 assert!(CiString::<36>::new("no\ttab").is_err());
278 assert!(CiString::<36>::new("caf\u{e9}").is_err(), "CiString is ASCII-only");
279 assert!(CiString::<3>::new("abcd").is_err());
280 }
281
282 #[test]
283 fn equality_and_hashing_ignore_case() {
284 let a = CiString::<36>::new("NL*TNM*001").unwrap();
285 let b = CiString::<36>::new("nl*tnm*001").unwrap();
286 assert_eq!(a, b);
287 let mut set = HashSet::new();
288 set.insert(a);
289 assert!(set.contains(&b), "case-folded hash must agree with case-folded Eq");
290 }
291
292 #[test]
293 fn deserialize_is_permissive_but_validate_complains() {
294 let long: CiString<3> = serde_json::from_str("\"abcdef\"").unwrap();
295 assert_eq!(long.as_str(), "abcdef", "peer data is never dropped");
296 let err = long.validate().unwrap_err();
297 assert_eq!(err.as_slice()[0].code, ViolationCode::TooLong);
298 assert!(!long.is_conformant());
299 }
300
301 #[test]
302 fn serialize_preserves_original_case() {
303 let a = CiString::<36>::new("MiXeD").unwrap();
304 assert_eq!(serde_json::to_string(&a).unwrap(), "\"MiXeD\"");
305 }
306
307 #[test]
308 fn na_sentinel_is_recognised() {
309 assert!(CiString::<36>::new("#NA").unwrap().is_not_available());
310 assert!(!CiString::<36>::new("NA").unwrap().is_not_available());
311 }
312}