use core::fmt;
use core::hash::{Hash, Hasher};
use core::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::text::{InvalidString, StringKind, check_printable_ascii};
use super::validate::{Validate, Validator, ViolationCode};
#[derive(Clone, Default)]
pub struct CiString<const N: usize>(String);
impl<const N: usize> CiString<N> {
pub const MAX_LEN: usize = N;
pub fn new(value: impl Into<String>) -> Result<Self, InvalidString> {
let value = value.into();
check_printable_ascii(&value, StringKind::Ci)?;
if value.len() > N {
return Err(InvalidString::too_long(value.len(), N, StringKind::Ci));
}
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.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn is_conformant(&self) -> bool {
self.0.len() <= N && check_printable_ascii(&self.0, StringKind::Ci).is_ok()
}
pub fn resize<const M: usize>(self) -> Result<CiString<M>, InvalidString> {
CiString::<M>::new(self.0)
}
#[must_use]
pub fn eq_ignore_case(&self, other: &str) -> bool {
self.0.eq_ignore_ascii_case(other)
}
pub const NOT_AVAILABLE: &'static str = "#NA";
#[must_use]
pub fn is_not_available(&self) -> bool {
self.0.eq_ignore_ascii_case(Self::NOT_AVAILABLE)
}
}
impl<const N: usize> Validate for CiString<N> {
fn validate_in(&self, v: &mut Validator) {
if let Err(e) = check_printable_ascii(&self.0, StringKind::Ci) {
v.report(ViolationCode::IllegalCharacter, e.to_string());
}
if self.0.len() > N {
v.report(ViolationCode::TooLong, format!("CiString({N}) holds {} characters", self.0.len()));
}
}
}
impl<const N: usize> fmt::Display for CiString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl<const N: usize> fmt::Debug for CiString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl<const N: usize> PartialEq for CiString<N> {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl<const N: usize> Eq for CiString<N> {}
impl<const N: usize> PartialOrd for CiString<N> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<const N: usize> Ord for CiString<N> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
let a = self.0.bytes().map(|b| b.to_ascii_lowercase());
let b = other.0.bytes().map(|b| b.to_ascii_lowercase());
a.cmp(b)
}
}
impl<const N: usize> Hash for CiString<N> {
fn hash<H: Hasher>(&self, state: &mut H) {
for byte in self.0.bytes() {
state.write_u8(byte.to_ascii_lowercase());
}
state.write_u8(0xff);
}
}
impl<const N: usize> AsRef<str> for CiString<N> {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<const N: usize> core::ops::Deref for CiString<N> {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl<const N: usize> FromStr for CiString<N> {
type Err = InvalidString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl<const N: usize> From<&str> for CiString<N> {
fn from(s: &str) -> Self {
Self::new_lenient(s)
}
}
impl<const N: usize> From<String> for CiString<N> {
fn from(s: String) -> Self {
Self::new_lenient(s)
}
}
impl<const N: usize> Serialize for CiString<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 CiString<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 CiString<N> {
fn schema_name() -> std::borrow::Cow<'static, str> {
format!("CiString{N}").into()
}
fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"maxLength": N,
"pattern": "^[\\u0020-\\u007E]*$",
"description": "OCPI CiString: case-insensitive, printable ASCII only",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn rejects_non_printable_and_non_ascii() {
assert!(CiString::<36>::new("ok").is_ok());
assert!(CiString::<36>::new("no\nnewline").is_err());
assert!(CiString::<36>::new("no\ttab").is_err());
assert!(CiString::<36>::new("caf\u{e9}").is_err(), "CiString is ASCII-only");
assert!(CiString::<3>::new("abcd").is_err());
}
#[test]
fn equality_and_hashing_ignore_case() {
let a = CiString::<36>::new("NL*TNM*001").unwrap();
let b = CiString::<36>::new("nl*tnm*001").unwrap();
assert_eq!(a, b);
let mut set = HashSet::new();
set.insert(a);
assert!(set.contains(&b), "case-folded hash must agree with case-folded Eq");
}
#[test]
fn deserialize_is_permissive_but_validate_complains() {
let long: CiString<3> = serde_json::from_str("\"abcdef\"").unwrap();
assert_eq!(long.as_str(), "abcdef", "peer data is never dropped");
let err = long.validate().unwrap_err();
assert_eq!(err.as_slice()[0].code, ViolationCode::TooLong);
assert!(!long.is_conformant());
}
#[test]
fn serialize_preserves_original_case() {
let a = CiString::<36>::new("MiXeD").unwrap();
assert_eq!(serde_json::to_string(&a).unwrap(), "\"MiXeD\"");
}
#[test]
fn na_sentinel_is_recognised() {
assert!(CiString::<36>::new("#NA").unwrap().is_not_available());
assert!(!CiString::<36>::new("NA").unwrap().is_not_available());
}
}