#[cfg(test)]
mod test;
use std::{fmt, ops::Deref};
use crate::{
json,
warning::{self, IntoCaveat},
Verdict,
};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
ContainsEscapeCodes,
ContainsNonPrintableASCII,
InvalidType { type_found: json::ValueKind },
InvalidLengthMax { length: usize },
InvalidLengthExact { length: usize },
PreferUppercase,
}
impl Warning {
fn invalid_type(elem: &json::Element<'_>) -> Self {
Self::InvalidType {
type_found: elem.value().kind(),
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ContainsEscapeCodes => f.write_str("The string contains escape codes."),
Self::ContainsNonPrintableASCII => {
f.write_str("The string contains non-printable bytes.")
}
Self::InvalidType { type_found } => {
write!(f, "The value should be a string but is `{type_found}`")
}
Self::InvalidLengthMax { length } => {
write!(
f,
"The string is longer than the max length `{length}` defined in the spec.",
)
}
Self::InvalidLengthExact { length } => {
write!(f, "The string should be length `{length}`.")
}
Self::PreferUppercase => {
write!(f, "Upper case is preffered")
}
}
}
}
impl crate::Warning for Warning {
fn id(&self) -> warning::Id {
match self {
Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
Self::ContainsNonPrintableASCII => {
warning::Id::from_static("contains_non_printable_ascii")
}
Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
Self::InvalidLengthMax { .. } => warning::Id::from_static("invalid_length_max"),
Self::InvalidLengthExact { .. } => warning::Id::from_static("invalid_length_exact"),
Self::PreferUppercase => warning::Id::from_static("prefer_upper_case"),
}
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);
impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'buf, const MAX_LEN: usize> json::FromJson<'buf> for CiMaxLen<'buf, MAX_LEN> {
type Warning = Warning;
fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
let (s, mut warnings) = Base::from_json(elem)?.into_parts();
if s.len() > MAX_LEN {
warnings.insert(Warning::InvalidLengthMax { length: MAX_LEN }, elem);
}
Ok(Self(s.0).into_caveat(warnings))
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);
impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'buf, const LEN: usize> json::FromJson<'buf> for CiExactLen<'buf, LEN> {
type Warning = Warning;
fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
let (s, mut warnings) = Base::from_json(elem)?.into_parts();
if s.len() != LEN {
warnings.insert(Warning::InvalidLengthExact { length: LEN }, elem);
}
Ok(Self(s.0).into_caveat(warnings))
}
}
#[derive(Copy, Clone, Debug)]
struct Base<'buf>(&'buf str);
impl Deref for Base<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl fmt::Display for Base<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'buf> json::FromJson<'buf> for Base<'buf> {
type Warning = Warning;
fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
let mut warnings = warning::Set::new();
let Some(id) = elem.to_raw_str() else {
return warnings.bail(Warning::invalid_type(elem), elem);
};
let s = id.has_escapes(elem).ignore_warnings();
let s = match s {
json::decode::PendingStr::NoEscapes(s) => {
if check_printable(s) {
warnings.insert(Warning::ContainsNonPrintableASCII, elem);
}
s
}
json::decode::PendingStr::HasEscapes(escape_str) => {
warnings.insert(Warning::ContainsEscapeCodes, elem);
let decoded = escape_str.decode_escapes(elem).ignore_warnings();
if check_printable(&decoded) {
warnings.insert(Warning::ContainsNonPrintableASCII, elem);
}
escape_str.into_raw()
}
};
Ok(Self(s).into_caveat(warnings))
}
}
fn check_printable(s: &str) -> bool {
s.chars()
.any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
}