use std::collections::HashMap;
use crate::coerce::CoercionLevel;
use crate::diagnostic::Diagnostic;
mod sealed {
pub trait Sealed {}
}
pub type Overflow = HashMap<String, serde_json::Value>;
pub trait Mode: sealed::Sealed + std::fmt::Debug + Clone + Copy {
type Residual: std::fmt::Debug + Clone;
fn default_coercion() -> CoercionLevel;
fn reject_unknown_fields() -> bool;
fn require_all_fields() -> bool;
fn fail_fast() -> bool;
}
#[derive(Debug, Clone, Copy)]
pub struct Lenient;
impl sealed::Sealed for Lenient {}
impl Mode for Lenient {
type Residual = ();
fn default_coercion() -> CoercionLevel {
CoercionLevel::BestEffort
}
fn reject_unknown_fields() -> bool {
false
}
fn require_all_fields() -> bool {
false
}
fn fail_fast() -> bool {
false
}
}
#[derive(Debug, Clone, Copy)]
pub struct Absorbing;
impl sealed::Sealed for Absorbing {}
impl Mode for Absorbing {
type Residual = Overflow;
fn default_coercion() -> CoercionLevel {
CoercionLevel::SafeWidening
}
fn reject_unknown_fields() -> bool {
false
}
fn require_all_fields() -> bool {
true
}
fn fail_fast() -> bool {
false
}
}
#[derive(Debug, Clone, Copy)]
pub struct Strict;
impl sealed::Sealed for Strict {}
impl Mode for Strict {
type Residual = std::convert::Infallible;
fn default_coercion() -> CoercionLevel {
CoercionLevel::Exact
}
fn reject_unknown_fields() -> bool {
true
}
fn require_all_fields() -> bool {
true
}
fn fail_fast() -> bool {
true
}
}
#[derive(Debug, Clone)]
pub struct LaminateResult<T, M: Mode> {
pub value: T,
pub residual: M::Residual,
pub diagnostics: Vec<Diagnostic>,
}
impl<T> LaminateResult<T, Lenient> {
pub fn lenient(value: T, diagnostics: Vec<Diagnostic>) -> Self {
LaminateResult {
value,
residual: (),
diagnostics,
}
}
}
impl<T> LaminateResult<T, Absorbing> {
pub fn absorbing(value: T, overflow: Overflow, diagnostics: Vec<Diagnostic>) -> Self {
LaminateResult {
value,
residual: overflow,
diagnostics,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DynamicMode {
Lenient,
Absorbing,
Strict,
}
impl DynamicMode {
pub fn default_coercion(&self) -> CoercionLevel {
match self {
DynamicMode::Lenient => Lenient::default_coercion(),
DynamicMode::Absorbing => Absorbing::default_coercion(),
DynamicMode::Strict => Strict::default_coercion(),
}
}
pub fn reject_unknown_fields(&self) -> bool {
match self {
DynamicMode::Lenient => Lenient::reject_unknown_fields(),
DynamicMode::Absorbing => Absorbing::reject_unknown_fields(),
DynamicMode::Strict => Strict::reject_unknown_fields(),
}
}
pub fn require_all_fields(&self) -> bool {
match self {
DynamicMode::Lenient => Lenient::require_all_fields(),
DynamicMode::Absorbing => Absorbing::require_all_fields(),
DynamicMode::Strict => Strict::require_all_fields(),
}
}
pub fn fail_fast(&self) -> bool {
match self {
DynamicMode::Lenient => Lenient::fail_fast(),
DynamicMode::Absorbing => Absorbing::fail_fast(),
DynamicMode::Strict => Strict::fail_fast(),
}
}
}
impl std::fmt::Display for DynamicMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DynamicMode::Lenient => write!(f, "lenient"),
DynamicMode::Absorbing => write!(f, "absorbing"),
DynamicMode::Strict => write!(f, "strict"),
}
}
}
impl std::str::FromStr for DynamicMode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"lenient" => Ok(DynamicMode::Lenient),
"absorbing" => Ok(DynamicMode::Absorbing),
"strict" => Ok(DynamicMode::Strict),
other => Err(format!(
"unknown mode: {other} (expected lenient, absorbing, or strict)"
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lenient_defaults() {
assert_eq!(Lenient::default_coercion(), CoercionLevel::BestEffort);
assert!(!Lenient::reject_unknown_fields());
assert!(!Lenient::require_all_fields());
assert!(!Lenient::fail_fast());
}
#[test]
fn absorbing_defaults() {
assert_eq!(Absorbing::default_coercion(), CoercionLevel::SafeWidening);
assert!(!Absorbing::reject_unknown_fields());
assert!(Absorbing::require_all_fields());
assert!(!Absorbing::fail_fast());
}
#[test]
fn strict_defaults() {
assert_eq!(Strict::default_coercion(), CoercionLevel::Exact);
assert!(Strict::reject_unknown_fields());
assert!(Strict::require_all_fields());
assert!(Strict::fail_fast());
}
#[test]
fn lenient_result() {
let result = LaminateResult::<String, Lenient>::lenient("hello".into(), vec![]);
assert_eq!(result.value, "hello");
assert_eq!(result.residual, ());
assert!(result.diagnostics.is_empty());
}
#[test]
fn absorbing_result() {
let mut overflow = HashMap::new();
overflow.insert("extra".into(), serde_json::json!("value"));
let result =
LaminateResult::<String, Absorbing>::absorbing("hello".into(), overflow, vec![]);
assert_eq!(result.value, "hello");
assert_eq!(result.residual.len(), 1);
assert!(result.residual.contains_key("extra"));
}
#[test]
fn dynamic_mode_from_str() {
assert_eq!(
"lenient".parse::<DynamicMode>().unwrap(),
DynamicMode::Lenient
);
assert_eq!(
"Absorbing".parse::<DynamicMode>().unwrap(),
DynamicMode::Absorbing
);
assert_eq!(
"STRICT".parse::<DynamicMode>().unwrap(),
DynamicMode::Strict
);
assert!("unknown".parse::<DynamicMode>().is_err());
}
#[test]
fn dynamic_mode_mirrors_static() {
let dm = DynamicMode::Lenient;
assert_eq!(dm.default_coercion(), Lenient::default_coercion());
assert_eq!(dm.reject_unknown_fields(), Lenient::reject_unknown_fields());
assert_eq!(dm.require_all_fields(), Lenient::require_all_fields());
assert_eq!(dm.fail_fast(), Lenient::fail_fast());
}
#[test]
fn dynamic_mode_display() {
assert_eq!(DynamicMode::Lenient.to_string(), "lenient");
assert_eq!(DynamicMode::Absorbing.to_string(), "absorbing");
assert_eq!(DynamicMode::Strict.to_string(), "strict");
}
}