1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum CoercionFlag {
14 StrippedMarkdown,
16
17 FixedTrailingComma,
19
20 FixedQuotes,
22
23 FuzzyFieldMatch {
25 expected: String,
27 found: String,
29 },
30
31 TypeCoercion {
33 from: String,
35 to: String,
37 },
38
39 UsedDefaultValue {
41 field: String,
43 },
44
45 TruncatedJson,
47
48 FixedUnquotedKeys,
50
51 FixedControlCharacters,
53
54 RemovedBom,
56
57 RequiredFieldDefaultedToNull {
60 field: String,
62 },
63}
64
65impl CoercionFlag {
66 pub fn description(&self) -> String {
83 match self {
84 Self::StrippedMarkdown => "Stripped markdown code fences".to_string(),
85 Self::FixedTrailingComma => "Fixed trailing comma in JSON".to_string(),
86 Self::FixedQuotes => "Fixed mismatched quotes".to_string(),
87 Self::FuzzyFieldMatch { expected, found } => {
88 format!("Matched field '{}' as '{}'", found, expected)
89 }
90 Self::TypeCoercion { from, to } => {
91 format!("Coerced type from {} to {}", from, to)
92 }
93 Self::UsedDefaultValue { field } => {
94 format!("Used default value for field '{}'", field)
95 }
96 Self::TruncatedJson => "Truncated malformed JSON".to_string(),
97 Self::FixedUnquotedKeys => "Fixed unquoted object keys".to_string(),
98 Self::FixedControlCharacters => "Fixed control characters".to_string(),
99 Self::RemovedBom => "Removed byte order mark (BOM)".to_string(),
100 Self::RequiredFieldDefaultedToNull { field } => {
101 format!(
102 "Required field '{}' defaulted to null due to truncated JSON",
103 field
104 )
105 }
106 }
107 }
108
109 pub fn is_major(&self) -> bool {
111 matches!(
112 self,
113 Self::TypeCoercion { .. }
114 | Self::UsedDefaultValue { .. }
115 | Self::TruncatedJson
116 | Self::FuzzyFieldMatch { .. }
117 | Self::RequiredFieldDefaultedToNull { .. }
118 )
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct CoercionResult<T> {
127 pub value: T,
129 pub flags: Vec<CoercionFlag>,
131 pub confidence: f32,
133}
134
135impl<T> CoercionResult<T> {
136 pub fn new(value: T) -> Self {
148 Self {
149 value,
150 flags: Vec::new(),
151 confidence: 1.0,
152 }
153 }
154
155 pub fn with_confidence(value: T, confidence: f32) -> Self {
157 Self {
158 value,
159 flags: Vec::new(),
160 confidence: confidence.clamp(0.0, 1.0),
161 }
162 }
163
164 pub fn set_confidence(mut self, confidence: f32) -> Self {
166 self.confidence = confidence.clamp(0.0, 1.0);
167 self
168 }
169
170 pub fn with_flag(mut self, flag: CoercionFlag) -> Self {
172 self.flags.push(flag);
173 self
174 }
175
176 pub fn with_flags(mut self, flags: Vec<CoercionFlag>) -> Self {
178 self.flags.extend(flags);
179 self
180 }
181
182 pub fn was_coerced(&self) -> bool {
195 !self.flags.is_empty()
196 }
197
198 pub fn is_confident(&self, threshold: f32) -> bool {
209 self.confidence >= threshold
210 }
211
212 pub fn has_major_coercions(&self) -> bool {
214 self.flags.iter().any(|f| f.is_major())
215 }
216
217 pub fn map<U, F>(self, f: F) -> CoercionResult<U>
219 where
220 F: FnOnce(T) -> U,
221 {
222 CoercionResult {
223 value: f(self.value),
224 flags: self.flags,
225 confidence: self.confidence,
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_coercion_flag_description() {
236 let flag = CoercionFlag::StrippedMarkdown;
237 assert!(!flag.description().is_empty());
238
239 let flag = CoercionFlag::TypeCoercion {
240 from: "string".to_string(),
241 to: "number".to_string(),
242 };
243 assert!(flag.description().contains("string"));
244 assert!(flag.description().contains("number"));
245 }
246
247 #[test]
248 fn test_coercion_flag_is_major() {
249 assert!(!CoercionFlag::StrippedMarkdown.is_major());
250 assert!(!CoercionFlag::FixedTrailingComma.is_major());
251 assert!(!CoercionFlag::FixedQuotes.is_major());
252
253 assert!(CoercionFlag::TypeCoercion {
254 from: "string".to_string(),
255 to: "number".to_string(),
256 }
257 .is_major());
258
259 assert!(CoercionFlag::UsedDefaultValue {
260 field: "test".to_string()
261 }
262 .is_major());
263
264 assert!(CoercionFlag::TruncatedJson.is_major());
265 }
266
267 #[test]
268 fn test_coercion_result_new() {
269 let result = CoercionResult::new(42);
270 assert_eq!(result.value, 42);
271 assert_eq!(result.confidence, 1.0);
272 assert!(result.flags.is_empty());
273 assert!(!result.was_coerced());
274 }
275
276 #[test]
277 fn test_coercion_result_with_confidence() {
278 let result = CoercionResult::with_confidence(42, 0.8);
279 assert_eq!(result.value, 42);
280 assert_eq!(result.confidence, 0.8);
281 assert!(result.is_confident(0.7));
282 assert!(!result.is_confident(0.9));
283 }
284
285 #[test]
286 fn test_coercion_result_confidence_clamped() {
287 let result = CoercionResult::with_confidence(42, 1.5);
288 assert_eq!(result.confidence, 1.0);
289
290 let result = CoercionResult::with_confidence(42, -0.5);
291 assert_eq!(result.confidence, 0.0);
292 }
293
294 #[test]
295 fn test_coercion_result_with_flag() {
296 let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
297 assert!(result.was_coerced());
298 assert_eq!(result.flags.len(), 1);
299 }
300
301 #[test]
302 fn test_coercion_result_with_flags() {
303 let flags = vec![
304 CoercionFlag::StrippedMarkdown,
305 CoercionFlag::FixedTrailingComma,
306 ];
307 let result = CoercionResult::new(42).with_flags(flags);
308 assert_eq!(result.flags.len(), 2);
309 }
310
311 #[test]
312 fn test_coercion_result_has_major_coercions() {
313 let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
314 assert!(!result.has_major_coercions());
315
316 let result = CoercionResult::new(42).with_flag(CoercionFlag::TruncatedJson);
317 assert!(result.has_major_coercions());
318 }
319
320 #[test]
321 fn test_coercion_result_map() {
322 let result = CoercionResult::new(42)
323 .with_flag(CoercionFlag::StrippedMarkdown)
324 .set_confidence(0.8);
325
326 let mapped = result.map(|x: i32| x.to_string());
327 assert_eq!(mapped.value, "42");
328 assert_eq!(mapped.flags.len(), 1);
329 assert_eq!(mapped.confidence, 0.8);
330 }
331
332 #[test]
333 fn test_coercion_flag_serialization() {
334 let flag = CoercionFlag::StrippedMarkdown;
335 let json = serde_json::to_string(&flag).unwrap();
336 let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
337 assert_eq!(flag, parsed);
338
339 let flag = CoercionFlag::TypeCoercion {
340 from: "string".to_string(),
341 to: "number".to_string(),
342 };
343 let json = serde_json::to_string(&flag).unwrap();
344 let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
345 assert_eq!(flag, parsed);
346 }
347
348 #[test]
349 fn test_coercion_result_serialization() {
350 let result = CoercionResult::new(42)
351 .with_flag(CoercionFlag::StrippedMarkdown)
352 .set_confidence(0.8);
353
354 let json = serde_json::to_string(&result).unwrap();
355 let parsed: CoercionResult<i32> = serde_json::from_str(&json).unwrap();
356 assert_eq!(result.value, parsed.value);
357 assert_eq!(result.flags, parsed.flags);
358 assert_eq!(result.confidence, parsed.confidence);
359 }
360}