1use 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#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct OcpiString<const N: usize>(String);
37
38impl<const N: usize> OcpiString<N> {
39 pub const MAX_LEN: usize = N;
41
42 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 pub fn new_lenient(value: impl Into<String>) -> Self {
62 Self(value.into())
63 }
64
65 #[must_use]
67 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70
71 #[must_use]
73 pub fn into_string(self) -> String {
74 self.0
75 }
76
77 #[must_use]
79 pub fn len(&self) -> usize {
80 self.0.chars().count()
81 }
82
83 #[must_use]
85 pub fn len_bytes(&self) -> usize {
86 self.0.len()
87 }
88
89 #[must_use]
91 pub fn is_empty(&self) -> bool {
92 self.0.is_empty()
93 }
94
95 #[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 #[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 pub fn resize<const M: usize>(self) -> Result<OcpiString<M>, InvalidString> {
116 OcpiString::<M>::new(self.0)
117 }
118
119 pub const NOT_AVAILABLE: &'static str = "#NA";
123
124 #[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
175impl<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 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}