use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CoercionFlag {
StrippedMarkdown,
FixedTrailingComma,
FixedQuotes,
FuzzyFieldMatch {
expected: String,
found: String,
},
TypeCoercion {
from: String,
to: String,
},
UsedDefaultValue {
field: String,
},
TruncatedJson,
FixedUnquotedKeys,
FixedControlCharacters,
RemovedBom,
}
impl CoercionFlag {
pub fn description(&self) -> String {
match self {
Self::StrippedMarkdown => "Stripped markdown code fences".to_string(),
Self::FixedTrailingComma => "Fixed trailing comma in JSON".to_string(),
Self::FixedQuotes => "Fixed mismatched quotes".to_string(),
Self::FuzzyFieldMatch { expected, found } => {
format!("Matched field '{}' as '{}'", found, expected)
}
Self::TypeCoercion { from, to } => {
format!("Coerced type from {} to {}", from, to)
}
Self::UsedDefaultValue { field } => {
format!("Used default value for field '{}'", field)
}
Self::TruncatedJson => "Truncated malformed JSON".to_string(),
Self::FixedUnquotedKeys => "Fixed unquoted object keys".to_string(),
Self::FixedControlCharacters => "Fixed control characters".to_string(),
Self::RemovedBom => "Removed byte order mark (BOM)".to_string(),
}
}
pub fn is_major(&self) -> bool {
matches!(
self,
Self::TypeCoercion { .. }
| Self::UsedDefaultValue { .. }
| Self::TruncatedJson
| Self::FuzzyFieldMatch { .. }
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoercionResult<T> {
pub value: T,
pub flags: Vec<CoercionFlag>,
pub confidence: f32,
}
impl<T> CoercionResult<T> {
pub fn new(value: T) -> Self {
Self {
value,
flags: Vec::new(),
confidence: 1.0,
}
}
pub fn with_confidence(value: T, confidence: f32) -> Self {
Self {
value,
flags: Vec::new(),
confidence: confidence.clamp(0.0, 1.0),
}
}
pub fn set_confidence(mut self, confidence: f32) -> Self {
self.confidence = confidence.clamp(0.0, 1.0);
self
}
pub fn with_flag(mut self, flag: CoercionFlag) -> Self {
self.flags.push(flag);
self
}
pub fn with_flags(mut self, flags: Vec<CoercionFlag>) -> Self {
self.flags.extend(flags);
self
}
pub fn was_coerced(&self) -> bool {
!self.flags.is_empty()
}
pub fn is_confident(&self, threshold: f32) -> bool {
self.confidence >= threshold
}
pub fn has_major_coercions(&self) -> bool {
self.flags.iter().any(|f| f.is_major())
}
pub fn map<U, F>(self, f: F) -> CoercionResult<U>
where
F: FnOnce(T) -> U,
{
CoercionResult {
value: f(self.value),
flags: self.flags,
confidence: self.confidence,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coercion_flag_description() {
let flag = CoercionFlag::StrippedMarkdown;
assert!(!flag.description().is_empty());
let flag = CoercionFlag::TypeCoercion {
from: "string".to_string(),
to: "number".to_string(),
};
assert!(flag.description().contains("string"));
assert!(flag.description().contains("number"));
}
#[test]
fn test_coercion_flag_is_major() {
assert!(!CoercionFlag::StrippedMarkdown.is_major());
assert!(!CoercionFlag::FixedTrailingComma.is_major());
assert!(!CoercionFlag::FixedQuotes.is_major());
assert!(CoercionFlag::TypeCoercion {
from: "string".to_string(),
to: "number".to_string(),
}
.is_major());
assert!(CoercionFlag::UsedDefaultValue {
field: "test".to_string()
}
.is_major());
assert!(CoercionFlag::TruncatedJson.is_major());
}
#[test]
fn test_coercion_result_new() {
let result = CoercionResult::new(42);
assert_eq!(result.value, 42);
assert_eq!(result.confidence, 1.0);
assert!(result.flags.is_empty());
assert!(!result.was_coerced());
}
#[test]
fn test_coercion_result_with_confidence() {
let result = CoercionResult::with_confidence(42, 0.8);
assert_eq!(result.value, 42);
assert_eq!(result.confidence, 0.8);
assert!(result.is_confident(0.7));
assert!(!result.is_confident(0.9));
}
#[test]
fn test_coercion_result_confidence_clamped() {
let result = CoercionResult::with_confidence(42, 1.5);
assert_eq!(result.confidence, 1.0);
let result = CoercionResult::with_confidence(42, -0.5);
assert_eq!(result.confidence, 0.0);
}
#[test]
fn test_coercion_result_with_flag() {
let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
assert!(result.was_coerced());
assert_eq!(result.flags.len(), 1);
}
#[test]
fn test_coercion_result_with_flags() {
let flags = vec![
CoercionFlag::StrippedMarkdown,
CoercionFlag::FixedTrailingComma,
];
let result = CoercionResult::new(42).with_flags(flags);
assert_eq!(result.flags.len(), 2);
}
#[test]
fn test_coercion_result_has_major_coercions() {
let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
assert!(!result.has_major_coercions());
let result = CoercionResult::new(42).with_flag(CoercionFlag::TruncatedJson);
assert!(result.has_major_coercions());
}
#[test]
fn test_coercion_result_map() {
let result = CoercionResult::new(42)
.with_flag(CoercionFlag::StrippedMarkdown)
.set_confidence(0.8);
let mapped = result.map(|x: i32| x.to_string());
assert_eq!(mapped.value, "42");
assert_eq!(mapped.flags.len(), 1);
assert_eq!(mapped.confidence, 0.8);
}
#[test]
fn test_coercion_flag_serialization() {
let flag = CoercionFlag::StrippedMarkdown;
let json = serde_json::to_string(&flag).unwrap();
let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
assert_eq!(flag, parsed);
let flag = CoercionFlag::TypeCoercion {
from: "string".to_string(),
to: "number".to_string(),
};
let json = serde_json::to_string(&flag).unwrap();
let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
assert_eq!(flag, parsed);
}
#[test]
fn test_coercion_result_serialization() {
let result = CoercionResult::new(42)
.with_flag(CoercionFlag::StrippedMarkdown)
.set_confidence(0.8);
let json = serde_json::to_string(&result).unwrap();
let parsed: CoercionResult<i32> = serde_json::from_str(&json).unwrap();
assert_eq!(result.value, parsed.value);
assert_eq!(result.flags, parsed.flags);
assert_eq!(result.confidence, parsed.confidence);
}
}