use core::fmt;
use core::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::text::{InvalidString, StringKind, check_printable_utf8};
use super::validate::{Validate, Validator, ViolationCode};
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OcpiString<const N: usize>(String);
impl<const N: usize> OcpiString<N> {
pub const MAX_LEN: usize = N;
pub fn new(value: impl Into<String>) -> Result<Self, InvalidString> {
let value = value.into();
check_printable_utf8(&value, StringKind::Utf8)?;
let len = value.chars().count();
if len > N {
return Err(InvalidString::too_long(len, N, StringKind::Utf8));
}
Ok(Self(value))
}
pub fn new_lenient(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
#[must_use]
pub fn len(&self) -> usize {
self.0.chars().count()
}
#[must_use]
pub fn len_bytes(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn is_conformant(&self) -> bool {
self.len() <= N && check_printable_utf8(&self.0, StringKind::Utf8).is_ok()
}
#[must_use]
pub fn is_conformant_in_bytes(&self) -> bool {
self.0.len() <= N && check_printable_utf8(&self.0, StringKind::Utf8).is_ok()
}
pub fn resize<const M: usize>(self) -> Result<OcpiString<M>, InvalidString> {
OcpiString::<M>::new(self.0)
}
pub const NOT_AVAILABLE: &'static str = "#NA";
#[must_use]
pub fn is_not_available(&self) -> bool {
self.0 == Self::NOT_AVAILABLE
}
}
impl<const N: usize> Validate for OcpiString<N> {
fn validate_in(&self, v: &mut Validator) {
if let Err(e) = check_printable_utf8(&self.0, StringKind::Utf8) {
v.report(ViolationCode::IllegalCharacter, e.to_string());
}
let len = self.len();
if len > N {
v.report(ViolationCode::TooLong, format!("string({N}) holds {len} characters"));
}
}
}
impl<const N: usize> fmt::Display for OcpiString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl<const N: usize> fmt::Debug for OcpiString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl<const N: usize> AsRef<str> for OcpiString<N> {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<const N: usize> core::ops::Deref for OcpiString<N> {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl<const N: usize> FromStr for OcpiString<N> {
type Err = InvalidString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl<const N: usize> From<&str> for OcpiString<N> {
fn from(s: &str) -> Self {
Self::new_lenient(s)
}
}
impl<const N: usize> From<String> for OcpiString<N> {
fn from(s: String) -> Self {
Self::new_lenient(s)
}
}
impl<const N: usize> Serialize for OcpiString<N> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de, const N: usize> Deserialize<'de> for OcpiString<N> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer).map(Self)
}
}
#[cfg(feature = "schema")]
impl<const N: usize> schemars::JsonSchema for OcpiString<N> {
fn schema_name() -> std::borrow::Cow<'static, str> {
format!("String{N}").into()
}
fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"maxLength": N,
"description": "OCPI string: case-sensitive, printable UTF-8 only",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_characters_not_bytes() {
let s = OcpiString::<5>::new("日本語です").unwrap();
assert_eq!(s.len(), 5);
assert_eq!(s.len_bytes(), 15);
assert!(s.is_conformant());
assert!(!s.is_conformant_in_bytes(), "the byte reading is stricter");
assert!(OcpiString::<4>::new("日本語です").is_err());
}
#[test]
fn accepts_utf8_but_rejects_control_characters() {
assert!(OcpiString::<64>::new("Straße 12 — Küche 🚗").is_ok());
assert!(OcpiString::<64>::new("a\rb").is_err());
}
#[test]
fn deserialize_is_permissive() {
let s: OcpiString<2> = serde_json::from_str("\"much too long\"").unwrap();
assert_eq!(s.as_str(), "much too long");
assert_eq!(s.validate().unwrap_err().as_slice()[0].code, ViolationCode::TooLong);
}
}