use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{
into_caveat, json,
warning::{self, GatherWarnings as _, IntoCaveat},
Caveat, OutOfRange, Verdict,
};
pub trait SpecFieldNames {
const SPEC_FIELD_NAMES: &[&'static str];
}
pub mod cistring {
use std::{borrow::Cow, fmt};
use crate::warning;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
ContainsEscapeCodes,
ContainsNonPrintableASCII,
InvalidType,
ExceedsMaxLength,
}
impl fmt::Display for WarningKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WarningKind::ContainsEscapeCodes => {
f.write_str("The string contains escape codes.")
}
WarningKind::ContainsNonPrintableASCII => {
f.write_str("The string contains non-printable bytes.")
}
WarningKind::ExceedsMaxLength => {
f.write_str("The string is longer than the max length defined in the spec.")
}
WarningKind::InvalidType => f.write_str("The value should be a string."),
}
}
}
impl warning::Kind for WarningKind {
fn id(&self) -> Cow<'static, str> {
match self {
WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
WarningKind::ContainsNonPrintableASCII => "contains_non_printable_ascii".into(),
WarningKind::ExceedsMaxLength => "exceeds_max_len".into(),
WarningKind::InvalidType => "invalid_type".into(),
}
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct CiString<'buf, const BYTE_LEN: usize>(&'buf str);
impl<const BYTE_LEN: usize> fmt::Display for CiString<'_, BYTE_LEN> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<const BYTE_LEN: usize> IntoCaveat for CiString<'_, BYTE_LEN> {
fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
Caveat::new(self, warnings)
}
}
impl<'buf, 'elem: 'buf, const BYTE_LEN: usize> json::FromJson<'elem, 'buf>
for CiString<'buf, BYTE_LEN>
{
type WarningKind = cistring::WarningKind;
fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::WarningKind> {
let mut warnings = warning::Set::new();
let mut check_len_and_printable = |s: &str| {
if s.len() > BYTE_LEN {
warnings.with_elem(cistring::WarningKind::ExceedsMaxLength, elem);
}
if s.chars()
.any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
{
warnings.with_elem(cistring::WarningKind::ContainsNonPrintableASCII, elem);
}
};
let Some(id) = elem.as_raw_str() else {
warnings.with_elem(cistring::WarningKind::InvalidType, elem);
return Err(warnings);
};
let id = id.has_escapes(elem).ignore_warnings();
let id = match id {
json::decode::PendingStr::NoEscapes(s) => {
check_len_and_printable(s);
s
}
json::decode::PendingStr::HasEscapes(escape_str) => {
let decoded = escape_str.decode_escapes(elem).ignore_warnings();
check_len_and_printable(&decoded);
escape_str.into_raw()
}
};
Ok(CiString(id).into_caveat(warnings))
}
}
pub mod day_of_week {
use std::{borrow::Cow, fmt};
use crate::{json, warning};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
ContainsEscapeCodes,
Decode(json::decode::WarningKind),
InvalidCase,
InvalidDay,
InvalidType,
}
impl fmt::Display for WarningKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WarningKind::ContainsEscapeCodes => write!(
f,
"The value contains escape codes but it does not need them."
),
WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
WarningKind::InvalidCase => write!(f, "The day should be uppercase."),
WarningKind::InvalidDay => {
write!(f, "The value is not a valid day.")
}
WarningKind::InvalidType => write!(f, "The value should be a string."),
}
}
}
impl warning::Kind for WarningKind {
fn id(&self) -> Cow<'static, str> {
match self {
WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
WarningKind::Decode(kind) => format!("decode.{}", kind.id()).into(),
WarningKind::InvalidCase => "invalid_case".into(),
WarningKind::InvalidDay => "invalid_day".into(),
WarningKind::InvalidType => "invalid_type".into(),
}
}
}
impl From<json::decode::WarningKind> for WarningKind {
fn from(warn_kind: json::decode::WarningKind) -> Self {
Self::Decode(warn_kind)
}
}
}
#[derive(Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DayOfWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
into_caveat!(DayOfWeek);
impl From<chrono::Weekday> for DayOfWeek {
fn from(value: chrono::Weekday) -> Self {
match value {
chrono::Weekday::Mon => DayOfWeek::Monday,
chrono::Weekday::Tue => DayOfWeek::Tuesday,
chrono::Weekday::Wed => DayOfWeek::Wednesday,
chrono::Weekday::Thu => DayOfWeek::Thursday,
chrono::Weekday::Fri => DayOfWeek::Friday,
chrono::Weekday::Sat => DayOfWeek::Saturday,
chrono::Weekday::Sun => DayOfWeek::Sunday,
}
}
}
impl From<DayOfWeek> for usize {
fn from(value: DayOfWeek) -> Self {
match value {
DayOfWeek::Monday => 0,
DayOfWeek::Tuesday => 1,
DayOfWeek::Wednesday => 2,
DayOfWeek::Thursday => 3,
DayOfWeek::Friday => 4,
DayOfWeek::Saturday => 5,
DayOfWeek::Sunday => 6,
}
}
}
impl TryFrom<usize> for DayOfWeek {
type Error = OutOfRange;
fn try_from(value: usize) -> Result<Self, Self::Error> {
let day = match value {
0 => DayOfWeek::Monday,
1 => DayOfWeek::Tuesday,
2 => DayOfWeek::Wednesday,
3 => DayOfWeek::Thursday,
4 => DayOfWeek::Friday,
5 => DayOfWeek::Saturday,
6 => DayOfWeek::Sunday,
_ => return Err(OutOfRange::new()),
};
Ok(day)
}
}
impl json::FromJson<'_, '_> for DayOfWeek {
type WarningKind = day_of_week::WarningKind;
fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
const NUM_DAYS: usize = 7;
const DAYS: [&str; NUM_DAYS] = [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY",
];
let mut warnings = warning::Set::new();
let value = elem.as_value();
let Some(s) = value.as_raw_str() else {
warnings.with_elem(day_of_week::WarningKind::InvalidType, elem);
return Err(warnings);
};
let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
let s = match pending_str {
json::decode::PendingStr::NoEscapes(s) => s,
json::decode::PendingStr::HasEscapes(_) => {
warnings.with_elem(day_of_week::WarningKind::ContainsEscapeCodes, elem);
return Err(warnings);
}
};
if !s.chars().all(char::is_uppercase) {
warnings.with_elem(day_of_week::WarningKind::InvalidCase, elem);
}
let Some(index) = DAYS.iter().position(|day| day.eq_ignore_ascii_case(s)) else {
warnings.with_elem(day_of_week::WarningKind::InvalidDay, elem);
return Err(warnings);
};
let Ok(day) = DayOfWeek::try_from(index) else {
warnings.with_elem(day_of_week::WarningKind::InvalidDay, elem);
return Err(warnings);
};
Ok(day.into_caveat(warnings))
}
}
impl From<DayOfWeek> for chrono::Weekday {
fn from(day: DayOfWeek) -> Self {
match day {
DayOfWeek::Monday => Self::Mon,
DayOfWeek::Tuesday => Self::Tue,
DayOfWeek::Wednesday => Self::Wed,
DayOfWeek::Thursday => Self::Thu,
DayOfWeek::Friday => Self::Fri,
DayOfWeek::Saturday => Self::Sat,
DayOfWeek::Sunday => Self::Sun,
}
}
}
#[cfg(test)]
mod test_day_of_week {
use assert_matches::assert_matches;
use crate::{
json::{self, FromJson as _},
test,
};
use super::{day_of_week::WarningKind, DayOfWeek};
#[test]
fn should_create_from_json() {
const JSON: &str = r#""MONDAY""#;
test::setup();
let elem = json::parse(JSON).unwrap();
let day = DayOfWeek::from_json(&elem).unwrap().unwrap();
assert_matches!(day, DayOfWeek::Monday);
}
#[test]
fn should_fail_on_type_from_json() {
const JSON: &str = "[]";
test::setup();
let elem = json::parse(JSON).unwrap();
let warnings = DayOfWeek::from_json(&elem).unwrap_err().into_kind_vec();
assert_matches!(*warnings, [WarningKind::InvalidType]);
}
#[test]
fn should_fail_on_value_from_json() {
const JSON: &str = r#""MOONDAY""#;
test::setup();
let elem = json::parse(JSON).unwrap();
let warnings = DayOfWeek::from_json(&elem).unwrap_err().into_kind_vec();
assert_matches!(*warnings, [WarningKind::InvalidDay]);
}
#[test]
fn should_warn_about_case_from_json() {
const JSON: &str = r#""sunday""#;
test::setup();
let elem = json::parse(JSON).unwrap();
let (day, warnings) = DayOfWeek::from_json(&elem).unwrap().into_parts();
let warnings = warnings.into_kind_vec();
assert_matches!(day, DayOfWeek::Sunday);
assert_matches!(*warnings, [WarningKind::InvalidCase]);
}
}