Skip to main content

fuzzy_parser/
lib.rs

1//! Fuzzy JSON repair for LLM-generated DSL
2//!
3//! This crate provides generic fuzzy matching and automatic correction for
4//! JSON that may contain typos (common when generated by LLMs).
5//!
6//! # Design
7//!
8//! This crate provides **generic repair APIs** - the schema definitions
9//! live in the calling crate (e.g., your application defines the schema).
10//!
11//! # Features
12//!
13//! - **JSON sanitization**: Fix syntax errors (trailing commas, missing braces)
14//! - **Tagged enum repair**: Fix type discriminator typos (e.g., `"AddDeriv"` → `"AddDerive"`)
15//! - **Field name repair**: Fix field name typos (e.g., `"taget"` → `"target"`)
16//! - **Enum array repair**: Fix values in enum arrays (e.g., `["Debg"]` → `["Debug"]`)
17//! - **Nested object repair**: Fix field names in nested objects
18//! - **Multiple algorithm support**: Jaro-Winkler, Levenshtein, Damerau-Levenshtein
19//! - **Configurable similarity threshold**
20//!
21//! # Example
22//!
23//! ```
24//! use fuzzy_parser::{
25//!     sanitize_json, repair_tagged_enum_json, TaggedEnumSchema, FuzzyOptions,
26//! };
27//!
28//! // Define schema with enum arrays and nested objects
29//! let schema = TaggedEnumSchema::new(
30//!     "type",  // tag field
31//!     &["AddDerive", "RemoveDerive"],  // valid types
32//!     |tag| match tag {
33//!         "AddDerive" | "RemoveDerive" => Some(&["target", "derives", "config"][..]),
34//!         _ => None,
35//!     },
36//! )
37//! .with_enum_array("derives", &["Debug", "Clone", "Serialize", "Default"])
38//! .with_nested_object("config", &["timeout", "retries"]);
39//!
40//! // LLM output with syntax errors AND typos
41//! let malformed = r#"{"type": "AddDeriv", "taget": "User", "derives": ["Debg",], "config": {"timout": 30,}}"#;
42//!
43//! // Step 1: Sanitize (fix syntax errors)
44//! let sanitized = sanitize_json(malformed);
45//!
46//! // Step 2: Repair (fix typos)
47//! let result = repair_tagged_enum_json(&sanitized, &schema, &FuzzyOptions::default()).unwrap();
48//!
49//! assert_eq!(result.repaired["type"], "AddDerive");
50//! assert!(result.repaired.get("target").is_some());
51//! assert_eq!(result.repaired["derives"][0], "Debug");
52//! assert!(result.repaired["config"].get("timeout").is_some());
53//! ```
54
55#![warn(missing_docs)]
56
57pub mod distance;
58pub mod error;
59pub mod repair;
60pub mod sanitize;
61pub mod schema;
62
63// Re-export main types
64pub use distance::{Algorithm, Match};
65pub use error::FuzzyError;
66pub use repair::{
67    repair_enum_array, repair_fields_with_list, repair_object_fields, repair_tagged_enum,
68    repair_tagged_enum_array, repair_tagged_enum_json, Correction, FuzzyOptions, RepairResult,
69};
70pub use sanitize::sanitize_json;
71pub use schema::{ObjectSchema, TaggedEnumSchema};
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_full_workflow() {
79        // Define a simple schema
80        let schema =
81            TaggedEnumSchema::new("type", &["AddDerive", "RenameIdent"], |tag| match tag {
82                "AddDerive" => Some(&["target", "derives"][..]),
83                "RenameIdent" => Some(&["from", "to", "kind"][..]),
84                _ => None,
85            });
86
87        // Simulate LLM output with typos
88        let llm_output = r#"{
89            "type": "AddDeriv",
90            "taget": "DatabaseConfig",
91            "derives": ["Debug", "Clone"]
92        }"#;
93
94        let result =
95            repair_tagged_enum_json(llm_output, &schema, &FuzzyOptions::default()).unwrap();
96
97        // Verify corrections
98        assert_eq!(result.repaired["type"], "AddDerive");
99        assert_eq!(result.repaired["target"], "DatabaseConfig");
100        assert_eq!(result.corrections.len(), 2);
101    }
102
103    #[test]
104    fn test_sanitize_then_repair_workflow() {
105        // Define schema with enum arrays
106        let schema =
107            TaggedEnumSchema::new("type", &["AddDerive"], |_| Some(&["target", "derives"][..]))
108                .with_enum_array("derives", &["Debug", "Clone", "Serialize"]);
109
110        // LLM output with BOTH syntax errors AND typos
111        let malformed_llm_output = r#"{
112            "type": "AddDeriv",
113            "taget": "User",
114            "derives": ["Debg", "Clne",],
115        }"#;
116
117        // Step 1: Sanitize
118        let sanitized = sanitize_json(malformed_llm_output);
119
120        // Verify sanitization worked
121        assert!(!sanitized.contains(",]"));
122        assert!(!sanitized.contains(",}"));
123
124        // Step 2: Repair
125        let result =
126            repair_tagged_enum_json(&sanitized, &schema, &FuzzyOptions::default()).unwrap();
127
128        // Verify all corrections
129        assert_eq!(result.repaired["type"], "AddDerive");
130        assert!(result.repaired.get("target").is_some());
131        assert_eq!(result.repaired["derives"][0], "Debug");
132        assert_eq!(result.repaired["derives"][1], "Clone");
133    }
134
135    #[test]
136    fn test_sanitize_then_repair_missing_brace() {
137        let schema = TaggedEnumSchema::new("type", &["Action"], |_| Some(&["name"][..]));
138
139        // LLM truncated output
140        let truncated = r#"{"type": "Action", "name": "test"#;
141
142        let sanitized = sanitize_json(truncated);
143        let result =
144            repair_tagged_enum_json(&sanitized, &schema, &FuzzyOptions::default()).unwrap();
145
146        assert_eq!(result.repaired["type"], "Action");
147        assert_eq!(result.repaired["name"], "test");
148    }
149}