#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum Dialect {
#[default]
Normative,
ZeroTolerant,
OneTolerant,
}
impl FromStr for Dialect {
type Err = String;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"n" | "N" => Ok(Self::Normative),
"0" => Ok(Self::ZeroTolerant),
"1" => Ok(Self::OneTolerant),
_ => Err(format!(
"Invalid dialect '{s}'. Valid values are: 'n' (normative), '0' (zero-tolerant), '1' (one-tolerant)"
)),
}
}
}
impl fmt::Display for Dialect {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Normative => write!(f, "n"),
Self::ZeroTolerant => write!(f, "0"),
Self::OneTolerant => write!(f, "1"),
}
}
}
#[inline]
#[must_use]
pub fn effective_min_count(dialect: Dialect, declared_min_count: u32) -> u32 {
match dialect {
Dialect::Normative => declared_min_count,
Dialect::ZeroTolerant => 0,
Dialect::OneTolerant => declared_min_count.max(1),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_dialect_default() {
assert_eq!(Dialect::default(), Dialect::Normative);
}
#[test]
fn test_dialect_from_str() {
assert_eq!(Dialect::from_str("n").unwrap(), Dialect::Normative);
assert_eq!(Dialect::from_str("N").unwrap(), Dialect::Normative);
assert_eq!(Dialect::from_str("0").unwrap(), Dialect::ZeroTolerant);
assert_eq!(Dialect::from_str("1").unwrap(), Dialect::OneTolerant);
assert!(Dialect::from_str("x").is_err());
assert!(Dialect::from_str("normative").is_err());
assert!(Dialect::from_str("").is_err());
}
#[test]
fn test_dialect_display() {
assert_eq!(Dialect::Normative.to_string(), "n");
assert_eq!(Dialect::ZeroTolerant.to_string(), "0");
assert_eq!(Dialect::OneTolerant.to_string(), "1");
}
#[test]
fn test_effective_min_count_normative() {
assert_eq!(effective_min_count(Dialect::Normative, 0), 0);
assert_eq!(effective_min_count(Dialect::Normative, 1), 1);
assert_eq!(effective_min_count(Dialect::Normative, 5), 5);
assert_eq!(effective_min_count(Dialect::Normative, 100), 100);
}
#[test]
fn test_effective_min_count_zero_tolerant() {
assert_eq!(effective_min_count(Dialect::ZeroTolerant, 0), 0);
assert_eq!(effective_min_count(Dialect::ZeroTolerant, 1), 0);
assert_eq!(effective_min_count(Dialect::ZeroTolerant, 5), 0);
assert_eq!(effective_min_count(Dialect::ZeroTolerant, 100), 0);
}
#[test]
fn test_effective_min_count_one_tolerant() {
assert_eq!(effective_min_count(Dialect::OneTolerant, 0), 1);
assert_eq!(effective_min_count(Dialect::OneTolerant, 1), 1);
assert_eq!(effective_min_count(Dialect::OneTolerant, 5), 5);
assert_eq!(effective_min_count(Dialect::OneTolerant, 100), 100);
}
#[test]
fn test_dialect_roundtrip() {
let dialects = [
Dialect::Normative,
Dialect::ZeroTolerant,
Dialect::OneTolerant,
];
for dialect in dialects {
let s = dialect.to_string();
let parsed = Dialect::from_str(&s).unwrap();
assert_eq!(parsed, dialect);
}
}
#[test]
fn test_dialect_debug_format() {
assert!(format!("{:?}", Dialect::Normative).contains("Normative"));
assert!(format!("{:?}", Dialect::ZeroTolerant).contains("ZeroTolerant"));
assert!(format!("{:?}", Dialect::OneTolerant).contains("OneTolerant"));
}
#[test]
fn test_dialect_clone_preserves_value() {
let d = Dialect::ZeroTolerant;
let cloned = d;
assert_eq!(d, cloned);
}
#[test]
fn test_dialect_eq_different_variants() {
assert_ne!(Dialect::Normative, Dialect::ZeroTolerant);
assert_ne!(Dialect::ZeroTolerant, Dialect::OneTolerant);
assert_ne!(Dialect::OneTolerant, Dialect::Normative);
}
#[test]
fn test_dialect_hash_consistency() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Dialect::Normative);
set.insert(Dialect::ZeroTolerant);
set.insert(Dialect::OneTolerant);
assert_eq!(set.len(), 3);
set.insert(Dialect::Normative);
assert_eq!(set.len(), 3);
}
#[test]
fn test_dialect_serde_roundtrip_all_variants() {
let variants = [
Dialect::Normative,
Dialect::ZeroTolerant,
Dialect::OneTolerant,
];
for dialect in variants {
let json = serde_json::to_string(&dialect).unwrap();
let deserialized: Dialect = serde_json::from_str(&json).unwrap();
assert_eq!(
dialect, deserialized,
"Serde roundtrip failed for {dialect}"
);
}
}
#[test]
fn test_dialect_from_str_with_whitespace() {
assert_eq!(Dialect::from_str(" n ").unwrap(), Dialect::Normative);
assert_eq!(Dialect::from_str(" 0 ").unwrap(), Dialect::ZeroTolerant);
assert_eq!(Dialect::from_str(" 1 ").unwrap(), Dialect::OneTolerant);
}
#[test]
fn test_dialect_from_str_error_message_content() {
let err = Dialect::from_str("invalid").unwrap_err();
assert!(err.contains("Invalid dialect"));
assert!(err.contains("'invalid'"));
assert!(err.contains("normative"));
assert!(err.contains("zero-tolerant"));
assert!(err.contains("one-tolerant"));
}
#[test]
fn test_dialect_from_str_numeric_invalid() {
assert!(Dialect::from_str("2").is_err());
assert!(Dialect::from_str("3").is_err());
assert!(Dialect::from_str("-1").is_err());
}
#[test]
fn test_effective_min_count_normative_u32_max() {
assert_eq!(effective_min_count(Dialect::Normative, u32::MAX), u32::MAX);
}
#[test]
fn test_effective_min_count_zero_tolerant_u32_max() {
assert_eq!(effective_min_count(Dialect::ZeroTolerant, u32::MAX), 0);
}
#[test]
fn test_effective_min_count_one_tolerant_u32_max() {
assert_eq!(
effective_min_count(Dialect::OneTolerant, u32::MAX),
u32::MAX
);
}
#[test]
fn test_effective_min_count_one_tolerant_zero_clamps_to_one() {
assert_eq!(effective_min_count(Dialect::OneTolerant, 0), 1);
}
#[test]
fn test_effective_min_count_normative_zero_stays_zero() {
assert_eq!(effective_min_count(Dialect::Normative, 0), 0);
}
#[test]
fn test_effective_min_count_zero_tolerant_one_becomes_zero() {
assert_eq!(effective_min_count(Dialect::ZeroTolerant, 1), 0);
}
}