1use crate::distance::{find_closest, Algorithm};
7use crate::error::FuzzyError;
8use crate::schema::{ObjectSchema, TaggedEnumSchema};
9use serde_json::{Map, Value};
10
11#[derive(Debug, Clone)]
13pub struct FuzzyOptions {
14 pub min_similarity: f64,
19
20 pub algorithm: Algorithm,
24}
25
26impl Default for FuzzyOptions {
27 fn default() -> Self {
28 Self {
29 min_similarity: 0.7,
30 algorithm: Algorithm::JaroWinkler,
31 }
32 }
33}
34
35impl FuzzyOptions {
36 pub fn with_min_similarity(mut self, min_similarity: f64) -> Self {
38 self.min_similarity = min_similarity;
39 self
40 }
41
42 pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self {
44 self.algorithm = algorithm;
45 self
46 }
47}
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct Correction {
52 pub original: String,
54 pub corrected: String,
56 pub similarity: f64,
58 pub field_path: String,
60}
61
62impl Correction {
63 pub fn new(original: String, corrected: String, similarity: f64, field_path: String) -> Self {
65 Self {
66 original,
67 corrected,
68 similarity,
69 field_path,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct RepairResult {
77 pub repaired: Value,
79 pub corrections: Vec<Correction>,
81}
82
83impl RepairResult {
84 pub fn has_corrections(&self) -> bool {
86 !self.corrections.is_empty()
87 }
88
89 pub fn correction_count(&self) -> usize {
91 self.corrections.len()
92 }
93}
94
95pub fn repair_object_fields(
103 obj: &mut Map<String, Value>,
104 schema: &ObjectSchema,
105 path: &str,
106 options: &FuzzyOptions,
107) -> Vec<Correction> {
108 repair_fields_with_list(obj, schema.valid_fields, path, options)
109}
110
111pub fn repair_fields_with_list(
127 obj: &mut Map<String, Value>,
128 valid_fields: &[&str],
129 path: &str,
130 options: &FuzzyOptions,
131) -> Vec<Correction> {
132 let mut corrections = Vec::new();
133
134 let keys_to_check: Vec<String> = obj
136 .keys()
137 .filter(|k| !valid_fields.contains(&k.as_str()))
138 .cloned()
139 .collect();
140
141 for key in keys_to_check {
143 if let Some(m) = find_closest(
144 &key,
145 valid_fields.iter().copied(),
146 options.min_similarity,
147 options.algorithm,
148 ) {
149 if !obj.contains_key(&m.candidate) {
151 if let Some(val) = obj.remove(&key) {
152 corrections.push(Correction::new(
153 key.clone(),
154 m.candidate.clone(),
155 m.similarity,
156 format!("{}.{}", path, key),
157 ));
158 obj.insert(m.candidate, val);
159 }
160 }
161 }
162 }
163
164 corrections
165}
166
167pub fn repair_tagged_enum<F>(
184 obj: &mut Map<String, Value>,
185 schema: &TaggedEnumSchema<F>,
186 path: &str,
187 options: &FuzzyOptions,
188) -> Vec<Correction>
189where
190 F: Fn(&str) -> Option<&'static [&'static str]>,
191{
192 let mut corrections = Vec::new();
193
194 let tag_value = if let Some(tag_val) = obj.get(schema.tag_field).and_then(|v| v.as_str()) {
196 if !schema.is_valid_tag(tag_val) {
197 if let Some(m) = find_closest(
199 tag_val,
200 schema.valid_tags.iter().copied(),
201 options.min_similarity,
202 options.algorithm,
203 ) {
204 corrections.push(Correction::new(
205 tag_val.to_string(),
206 m.candidate.clone(),
207 m.similarity,
208 format!("{}.{}", path, schema.tag_field),
209 ));
210 obj.insert(
211 schema.tag_field.to_string(),
212 Value::String(m.candidate.clone()),
213 );
214 m.candidate
215 } else {
216 tag_val.to_string()
217 }
218 } else {
219 tag_val.to_string()
220 }
221 } else {
222 return corrections; };
224
225 if let Some(valid_fields) = schema.get_fields(&tag_value) {
227 let keys_to_check: Vec<String> = obj
229 .keys()
230 .filter(|k| *k != schema.tag_field && !valid_fields.contains(&k.as_str()))
231 .cloned()
232 .collect();
233
234 for key in keys_to_check {
235 if let Some(m) = find_closest(
236 &key,
237 valid_fields.iter().copied(),
238 options.min_similarity,
239 options.algorithm,
240 ) {
241 if !obj.contains_key(&m.candidate) {
242 if let Some(val) = obj.remove(&key) {
243 corrections.push(Correction::new(
244 key.clone(),
245 m.candidate.clone(),
246 m.similarity,
247 format!("{}.{}", path, key),
248 ));
249 obj.insert(m.candidate, val);
250 }
251 }
252 }
253 }
254 }
255
256 for (field_name, valid_values) in &schema.enum_arrays {
258 if let Some(Value::Array(arr)) = obj.get_mut(*field_name) {
259 let field_path = format!("{}.{}", path, field_name);
260 let arr_corrections = repair_enum_array(arr, valid_values, &field_path, options);
261 corrections.extend(arr_corrections);
262 }
263 }
264
265 for (field_name, valid_fields) in &schema.nested_objects {
267 if let Some(Value::Object(nested_obj)) = obj.get_mut(*field_name) {
268 let nested_path = format!("{}.{}", path, field_name);
269 let nested_corrections =
270 repair_fields_with_list(nested_obj, valid_fields, &nested_path, options);
271 corrections.extend(nested_corrections);
272 }
273 }
274
275 corrections
276}
277
278pub fn repair_enum_array(
282 arr: &mut [Value],
283 valid_values: &[&str],
284 path: &str,
285 options: &FuzzyOptions,
286) -> Vec<Correction> {
287 let mut corrections = Vec::new();
288
289 for (i, item) in arr.iter_mut().enumerate() {
290 if let Value::String(s) = item {
291 if !valid_values.contains(&s.as_str()) {
292 if let Some(m) = find_closest(
293 s,
294 valid_values.iter().copied(),
295 options.min_similarity,
296 options.algorithm,
297 ) {
298 corrections.push(Correction::new(
299 s.clone(),
300 m.candidate.clone(),
301 m.similarity,
302 format!("{}[{}]", path, i),
303 ));
304 *item = Value::String(m.candidate);
305 }
306 }
307 }
308 }
309
310 corrections
311}
312
313pub fn repair_tagged_enum_json<F>(
315 json: &str,
316 schema: &TaggedEnumSchema<F>,
317 options: &FuzzyOptions,
318) -> Result<RepairResult, FuzzyError>
319where
320 F: Fn(&str) -> Option<&'static [&'static str]>,
321{
322 let mut value: Value = serde_json::from_str(json)?;
323
324 let corrections = if let Some(obj) = value.as_object_mut() {
325 repair_tagged_enum(obj, schema, "$", options)
326 } else {
327 return Err(FuzzyError::NotObject);
328 };
329
330 Ok(RepairResult {
331 repaired: value,
332 corrections,
333 })
334}
335
336pub fn repair_tagged_enum_array<F>(
338 arr: &mut [Value],
339 schema: &TaggedEnumSchema<F>,
340 path: &str,
341 options: &FuzzyOptions,
342) -> Vec<Correction>
343where
344 F: Fn(&str) -> Option<&'static [&'static str]>,
345{
346 let mut all_corrections = Vec::new();
347
348 for (i, item) in arr.iter_mut().enumerate() {
349 if let Some(obj) = item.as_object_mut() {
350 let item_path = format!("{}[{}]", path, i);
351 let corrections = repair_tagged_enum(obj, schema, &item_path, options);
352 all_corrections.extend(corrections);
353 }
354 }
355
356 all_corrections
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 type FieldResolver = fn(&str) -> Option<&'static [&'static str]>;
367
368 fn test_schema() -> TaggedEnumSchema<FieldResolver> {
369 TaggedEnumSchema::new(
370 "type",
371 &["AddDerive", "RemoveDerive", "RenameIdent"],
372 |tag| match tag {
373 "AddDerive" | "RemoveDerive" => Some(&["target", "derives"]),
374 "RenameIdent" => Some(&["from", "to", "kind"]),
375 _ => None,
376 },
377 )
378 }
379
380 #[test]
381 fn test_repair_tagged_enum_type_typo() {
382 let schema = test_schema();
383 let json = r#"{"type": "AddDeriv", "target": "User", "derives": ["Debug"]}"#;
384 let options = FuzzyOptions::default();
385
386 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
387
388 assert_eq!(result.repaired["type"], "AddDerive");
389 assert_eq!(result.corrections.len(), 1);
390 assert_eq!(result.corrections[0].original, "AddDeriv");
391 assert_eq!(result.corrections[0].corrected, "AddDerive");
392 }
393
394 #[test]
395 fn test_repair_tagged_enum_field_typo() {
396 let schema = test_schema();
397 let json = r#"{"type": "AddDerive", "taget": "User", "derives": ["Debug"]}"#;
398 let options = FuzzyOptions::default();
399
400 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
401
402 assert!(result.repaired.get("target").is_some());
403 assert!(result.repaired.get("taget").is_none());
404 assert_eq!(result.corrections.len(), 1);
405 }
406
407 #[test]
408 fn test_repair_tagged_enum_multiple_typos() {
409 let schema = test_schema();
410 let json = r#"{"type": "RenamIdent", "form": "old", "too": "new"}"#;
411 let options = FuzzyOptions::default();
412
413 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
414
415 assert_eq!(result.repaired["type"], "RenameIdent");
416 assert!(result.repaired.get("from").is_some());
417 assert!(result.repaired.get("to").is_some());
418 assert_eq!(result.corrections.len(), 3);
419 }
420
421 #[test]
422 fn test_repair_object_fields() {
423 let schema = ObjectSchema::new(&["name", "module", "derives"]);
424 let mut obj: Map<String, Value> =
425 serde_json::from_str(r#"{"nam": "Test", "modul": "foo"}"#).unwrap();
426 let options = FuzzyOptions::default();
427
428 let corrections = repair_object_fields(&mut obj, &schema, "$", &options);
429
430 assert!(obj.contains_key("name"));
431 assert!(obj.contains_key("module"));
432 assert_eq!(corrections.len(), 2);
433 }
434
435 #[test]
436 fn test_no_correction_needed() {
437 let schema = test_schema();
438 let json = r#"{"type": "AddDerive", "target": "User", "derives": ["Debug"]}"#;
439 let options = FuzzyOptions::default();
440
441 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
442
443 assert!(!result.has_corrections());
444 }
445
446 #[test]
447 fn test_high_similarity_threshold() {
448 let schema = test_schema();
449 let json = r#"{"type": "AddDeriv", "target": "User", "derives": ["Debug"]}"#;
450 let options = FuzzyOptions::default().with_min_similarity(0.99);
451
452 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
453
454 assert_eq!(result.repaired["type"], "AddDeriv");
456 assert!(!result.has_corrections());
457 }
458
459 #[test]
460 fn test_repair_array() {
461 let schema = test_schema();
462 let mut arr: Vec<Value> = serde_json::from_str(
463 r#"[
464 {"type": "AddDeriv", "taget": "User", "derives": ["Debug"]},
465 {"type": "RenamIdent", "form": "old", "too": "new"}
466 ]"#,
467 )
468 .unwrap();
469 let options = FuzzyOptions::default();
470
471 let corrections = repair_tagged_enum_array(&mut arr, &schema, "$.intents", &options);
472
473 assert_eq!(arr[0]["type"], "AddDerive");
474 assert!(arr[0].get("target").is_some());
475 assert_eq!(arr[1]["type"], "RenameIdent");
476 assert!(arr[1].get("from").is_some());
477 assert!(corrections.len() >= 4);
478 }
479
480 #[test]
481 fn test_repair_enum_array_values() {
482 let schema =
483 TaggedEnumSchema::new("type", &["AddDerive"], |_| Some(&["target", "derives"][..]))
484 .with_enum_array("derives", &["Debug", "Clone", "Serialize", "Default"]);
485
486 let json =
487 r#"{"type": "AddDerive", "target": "User", "derives": ["Debg", "Clne", "Serializ"]}"#;
488 let options = FuzzyOptions::default();
489
490 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
491
492 assert_eq!(result.repaired["derives"][0], "Debug");
493 assert_eq!(result.repaired["derives"][1], "Clone");
494 assert_eq!(result.repaired["derives"][2], "Serialize");
495 assert_eq!(result.corrections.len(), 3);
496 }
497
498 #[test]
499 fn test_repair_nested_object_fields() {
500 let schema =
501 TaggedEnumSchema::new("type", &["Configure"], |_| Some(&["name", "config"][..]))
502 .with_nested_object("config", &["timeout", "retries", "enabled"]);
503
504 let json =
505 r#"{"type": "Configure", "name": "test", "config": {"timout": 30, "retres": 3}}"#;
506 let options = FuzzyOptions::default();
507
508 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
509
510 assert!(result.repaired["config"].get("timeout").is_some());
511 assert!(result.repaired["config"].get("retries").is_some());
512 assert_eq!(result.repaired["config"]["timeout"], 30);
513 assert_eq!(result.repaired["config"]["retries"], 3);
514 assert_eq!(result.corrections.len(), 2);
515 }
516
517 #[test]
518 fn test_repair_combined_all_features() {
519 let schema = TaggedEnumSchema::new("type", &["AddDerive"], |_| {
520 Some(&["target", "derives", "config"][..])
521 })
522 .with_enum_array("derives", &["Debug", "Clone", "Serialize"])
523 .with_nested_object("config", &["timeout", "retries"]);
524
525 let json = r#"{
526 "type": "AddDeriv",
527 "taget": "User",
528 "derives": ["Debg", "Clne"],
529 "config": {"timout": 30}
530 }"#;
531 let options = FuzzyOptions::default();
532
533 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
534
535 assert_eq!(result.repaired["type"], "AddDerive");
537 assert!(result.repaired.get("target").is_some());
539 assert_eq!(result.repaired["target"], "User");
540 assert_eq!(result.repaired["derives"][0], "Debug");
542 assert_eq!(result.repaired["derives"][1], "Clone");
543 assert!(result.repaired["config"].get("timeout").is_some());
545 assert_eq!(result.repaired["config"]["timeout"], 30);
546 assert_eq!(result.corrections.len(), 5);
548 }
549
550 #[test]
551 fn test_collision_first_win_two_typos_same_candidate() {
552 let json = r#"{"type": "AddDerive", "taget": "User", "targt": "Post"}"#;
558 let schema = test_schema();
559 let options = FuzzyOptions::default();
560
561 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
562
563 assert_eq!(result.repaired["target"], "User");
565 assert_eq!(result.repaired["targt"], "Post");
566 assert!(result.repaired.get("taget").is_none());
567 assert_eq!(result.corrections.len(), 1);
568 assert_eq!(result.corrections[0].original, "taget");
569 assert_eq!(result.corrections[0].corrected, "target");
570 }
571
572 #[test]
573 fn test_collision_first_win_existing_key_preserved() {
574 let json = r#"{"type": "AddDerive", "target": "User", "taget": "Post"}"#;
577 let schema = test_schema();
578 let options = FuzzyOptions::default();
579
580 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
581
582 assert_eq!(result.repaired["target"], "User");
583 assert_eq!(result.repaired["taget"], "Post");
584 assert!(!result.has_corrections());
585 }
586
587 #[test]
588 fn test_repair_enum_array_no_correction_needed() {
589 let schema =
590 TaggedEnumSchema::new("type", &["AddDerive"], |_| Some(&["target", "derives"][..]))
591 .with_enum_array("derives", &["Debug", "Clone"]);
592
593 let json = r#"{"type": "AddDerive", "target": "User", "derives": ["Debug", "Clone"]}"#;
594 let options = FuzzyOptions::default();
595
596 let result = repair_tagged_enum_json(json, &schema, &options).unwrap();
597
598 assert!(!result.has_corrections());
599 }
600}