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
58impl CoercionFlag {
59 pub fn description(&self) -> String {
76 match self {
77 Self::StrippedMarkdown => "Stripped markdown code fences".to_string(),
78 Self::FixedTrailingComma => "Fixed trailing comma in JSON".to_string(),
79 Self::FixedQuotes => "Fixed mismatched quotes".to_string(),
80 Self::FuzzyFieldMatch { expected, found } => {
81 format!("Matched field '{}' as '{}'", found, expected)
82 }
83 Self::TypeCoercion { from, to } => {
84 format!("Coerced type from {} to {}", from, to)
85 }
86 Self::UsedDefaultValue { field } => {
87 format!("Used default value for field '{}'", field)
88 }
89 Self::TruncatedJson => "Truncated malformed JSON".to_string(),
90 Self::FixedUnquotedKeys => "Fixed unquoted object keys".to_string(),
91 Self::FixedControlCharacters => "Fixed control characters".to_string(),
92 Self::RemovedBom => "Removed byte order mark (BOM)".to_string(),
93 }
94 }
95
96 pub fn is_major(&self) -> bool {
98 matches!(
99 self,
100 Self::TypeCoercion { .. }
101 | Self::UsedDefaultValue { .. }
102 | Self::TruncatedJson
103 | Self::FuzzyFieldMatch { .. }
104 )
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct CoercionResult<T> {
113 pub value: T,
115 pub flags: Vec<CoercionFlag>,
117 pub confidence: f32,
119}
120
121impl<T> CoercionResult<T> {
122 pub fn new(value: T) -> Self {
134 Self {
135 value,
136 flags: Vec::new(),
137 confidence: 1.0,
138 }
139 }
140
141 pub fn with_confidence(value: T, confidence: f32) -> Self {
143 Self {
144 value,
145 flags: Vec::new(),
146 confidence: confidence.clamp(0.0, 1.0),
147 }
148 }
149
150 pub fn set_confidence(mut self, confidence: f32) -> Self {
152 self.confidence = confidence.clamp(0.0, 1.0);
153 self
154 }
155
156 pub fn with_flag(mut self, flag: CoercionFlag) -> Self {
158 self.flags.push(flag);
159 self
160 }
161
162 pub fn with_flags(mut self, flags: Vec<CoercionFlag>) -> Self {
164 self.flags.extend(flags);
165 self
166 }
167
168 pub fn was_coerced(&self) -> bool {
181 !self.flags.is_empty()
182 }
183
184 pub fn is_confident(&self, threshold: f32) -> bool {
195 self.confidence >= threshold
196 }
197
198 pub fn has_major_coercions(&self) -> bool {
200 self.flags.iter().any(|f| f.is_major())
201 }
202
203 pub fn map<U, F>(self, f: F) -> CoercionResult<U>
205 where
206 F: FnOnce(T) -> U,
207 {
208 CoercionResult {
209 value: f(self.value),
210 flags: self.flags,
211 confidence: self.confidence,
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_coercion_flag_description() {
222 let flag = CoercionFlag::StrippedMarkdown;
223 assert!(!flag.description().is_empty());
224
225 let flag = CoercionFlag::TypeCoercion {
226 from: "string".to_string(),
227 to: "number".to_string(),
228 };
229 assert!(flag.description().contains("string"));
230 assert!(flag.description().contains("number"));
231 }
232
233 #[test]
234 fn test_coercion_flag_is_major() {
235 assert!(!CoercionFlag::StrippedMarkdown.is_major());
236 assert!(!CoercionFlag::FixedTrailingComma.is_major());
237 assert!(!CoercionFlag::FixedQuotes.is_major());
238
239 assert!(CoercionFlag::TypeCoercion {
240 from: "string".to_string(),
241 to: "number".to_string(),
242 }
243 .is_major());
244
245 assert!(CoercionFlag::UsedDefaultValue {
246 field: "test".to_string()
247 }
248 .is_major());
249
250 assert!(CoercionFlag::TruncatedJson.is_major());
251 }
252
253 #[test]
254 fn test_coercion_result_new() {
255 let result = CoercionResult::new(42);
256 assert_eq!(result.value, 42);
257 assert_eq!(result.confidence, 1.0);
258 assert!(result.flags.is_empty());
259 assert!(!result.was_coerced());
260 }
261
262 #[test]
263 fn test_coercion_result_with_confidence() {
264 let result = CoercionResult::with_confidence(42, 0.8);
265 assert_eq!(result.value, 42);
266 assert_eq!(result.confidence, 0.8);
267 assert!(result.is_confident(0.7));
268 assert!(!result.is_confident(0.9));
269 }
270
271 #[test]
272 fn test_coercion_result_confidence_clamped() {
273 let result = CoercionResult::with_confidence(42, 1.5);
274 assert_eq!(result.confidence, 1.0);
275
276 let result = CoercionResult::with_confidence(42, -0.5);
277 assert_eq!(result.confidence, 0.0);
278 }
279
280 #[test]
281 fn test_coercion_result_with_flag() {
282 let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
283 assert!(result.was_coerced());
284 assert_eq!(result.flags.len(), 1);
285 }
286
287 #[test]
288 fn test_coercion_result_with_flags() {
289 let flags = vec![
290 CoercionFlag::StrippedMarkdown,
291 CoercionFlag::FixedTrailingComma,
292 ];
293 let result = CoercionResult::new(42).with_flags(flags);
294 assert_eq!(result.flags.len(), 2);
295 }
296
297 #[test]
298 fn test_coercion_result_has_major_coercions() {
299 let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
300 assert!(!result.has_major_coercions());
301
302 let result = CoercionResult::new(42).with_flag(CoercionFlag::TruncatedJson);
303 assert!(result.has_major_coercions());
304 }
305
306 #[test]
307 fn test_coercion_result_map() {
308 let result = CoercionResult::new(42)
309 .with_flag(CoercionFlag::StrippedMarkdown)
310 .set_confidence(0.8);
311
312 let mapped = result.map(|x: i32| x.to_string());
313 assert_eq!(mapped.value, "42");
314 assert_eq!(mapped.flags.len(), 1);
315 assert_eq!(mapped.confidence, 0.8);
316 }
317
318 #[test]
319 fn test_coercion_flag_serialization() {
320 let flag = CoercionFlag::StrippedMarkdown;
321 let json = serde_json::to_string(&flag).unwrap();
322 let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
323 assert_eq!(flag, parsed);
324
325 let flag = CoercionFlag::TypeCoercion {
326 from: "string".to_string(),
327 to: "number".to_string(),
328 };
329 let json = serde_json::to_string(&flag).unwrap();
330 let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
331 assert_eq!(flag, parsed);
332 }
333
334 #[test]
335 fn test_coercion_result_serialization() {
336 let result = CoercionResult::new(42)
337 .with_flag(CoercionFlag::StrippedMarkdown)
338 .set_confidence(0.8);
339
340 let json = serde_json::to_string(&result).unwrap();
341 let parsed: CoercionResult<i32> = serde_json::from_str(&json).unwrap();
342 assert_eq!(result.value, parsed.value);
343 assert_eq!(result.flags, parsed.flags);
344 assert_eq!(result.confidence, parsed.confidence);
345 }
346}