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//! The repair pipeline has three independent stages:
12//!
13//! 1. **Extract** ([`extract_json`]) — locate the JSON payload inside raw
14//!    LLM output (Markdown code fences, surrounding prose, multiple blocks)
15//! 2. **Sanitize** ([`sanitize_json`]) — fix syntax errors (trailing
16//!    commas, missing/mismatched closing delimiters, unclosed strings)
17//! 3. **Repair** ([`repair_tagged_enum_json`]) — fix typos and coerce
18//!    value types, guided by a caller-provided schema
19//!
20//! # Features
21//!
22//! - **JSON extraction**: Pull JSON out of code fences and prose
23//! - **JSON sanitization**: Fix syntax errors (trailing commas, missing braces)
24//! - **Tagged enum repair**: Fix type discriminator typos (e.g., `"AddDeriv"` → `"AddDerive"`)
25//! - **Field name repair**: Fix field name typos (e.g., `"taget"` → `"target"`)
26//! - **Enum value repair**: Fix string values constrained to closed sets
27//!   (e.g., `["Debg"]` → `["Debug"]`)
28//! - **Recursive schemas**: Nested objects, arrays of objects, and nested
29//!   tagged enums (e.g. arrays of DSL intents) are repaired to any depth
30//!   ([`FieldKind`])
31//! - **Type coercion**: Fix string-encoded scalars (e.g., `"42"` → `42`)
32//! - **Dynamic schemas**: Schemas own their data and can be built at runtime
33//! - **JSON Schema import**: Derive repair schemas from JSON Schema
34//!   documents ([`TaggedEnumSchema::from_json_schema`]) or, with the
35//!   `schemars` feature, directly from `#[derive(JsonSchema)]` types
36//! - **Transparency**: Every applied change is a [`Correction`]; every
37//!   collision-skipped rename is a [`SkippedCorrection`]
38//! - **Multiple algorithm support**: Jaro-Winkler, Levenshtein, Damerau-Levenshtein
39//! - **Configurable similarity threshold**
40//!
41//! # Example
42//!
43//! ````
44//! use fuzzy_parser::{
45//!     extract_json, sanitize_json, repair_tagged_enum_json, TaggedEnumSchema, FuzzyOptions,
46//! };
47//!
48//! // Define schema with enum arrays and nested objects
49//! let schema = TaggedEnumSchema::new(
50//!     "type",  // tag field
51//!     &["AddDerive", "RemoveDerive"],  // valid types
52//!     |tag| match tag {
53//!         "AddDerive" | "RemoveDerive" => Some(&["target", "derives", "config"][..]),
54//!         _ => None,
55//!     },
56//! )
57//! .with_enum_array("derives", &["Debug", "Clone", "Serialize", "Default"])
58//! .with_nested_object("config", &["timeout", "retries"]);
59//!
60//! // Raw LLM output: prose + code fence + syntax errors + typos
61//! let raw = r#"Here is the change you asked for:
62//!
63//! ```json
64//! {"type": "AddDeriv", "taget": "User", "derives": ["Debg",], "config": {"timout": 30,}}
65//! ```
66//! "#;
67//!
68//! // Step 0: Extract (strip prose and code fences)
69//! let payload = extract_json(raw).unwrap();
70//!
71//! // Step 1: Sanitize (fix syntax errors)
72//! let sanitized = sanitize_json(payload);
73//!
74//! // Step 2: Repair (fix typos)
75//! let result = repair_tagged_enum_json(&sanitized, &schema, &FuzzyOptions::default()).unwrap();
76//!
77//! assert_eq!(result.repaired["type"], "AddDerive");
78//! assert!(result.repaired.get("target").is_some());
79//! assert_eq!(result.repaired["derives"][0], "Debug");
80//! assert!(result.repaired["config"].get("timeout").is_some());
81//! ````
82
83#![warn(missing_docs)]
84
85pub mod distance;
86pub mod error;
87pub mod extract;
88pub mod import;
89pub mod repair;
90pub mod sanitize;
91pub mod schema;
92
93// Re-export main types
94pub use distance::{Algorithm, Match};
95pub use error::FuzzyError;
96pub use extract::{extract_json, extract_json_blocks, strip_code_fences};
97pub use import::{ImportWarning, SchemaImport};
98pub use repair::{
99    repair_enum_array, repair_fields_with_list, repair_object_fields, repair_tagged_enum,
100    repair_tagged_enum_array, repair_tagged_enum_json, Correction, FuzzyOptions, RepairLog,
101    RepairResult, SkipReason, SkippedCorrection,
102};
103pub use sanitize::sanitize_json;
104pub use schema::{FieldDef, FieldKind, ObjectSchema, TaggedEnumSchema};
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_full_workflow() {
112        // Define a simple schema
113        let schema =
114            TaggedEnumSchema::new("type", &["AddDerive", "RenameIdent"], |tag| match tag {
115                "AddDerive" => Some(&["target", "derives"][..]),
116                "RenameIdent" => Some(&["from", "to", "kind"][..]),
117                _ => None,
118            });
119
120        // Simulate LLM output with typos
121        let llm_output = r#"{
122            "type": "AddDeriv",
123            "taget": "DatabaseConfig",
124            "derives": ["Debug", "Clone"]
125        }"#;
126
127        let result =
128            repair_tagged_enum_json(llm_output, &schema, &FuzzyOptions::default()).unwrap();
129
130        // Verify corrections
131        assert_eq!(result.repaired["type"], "AddDerive");
132        assert_eq!(result.repaired["target"], "DatabaseConfig");
133        assert_eq!(result.corrections.len(), 2);
134    }
135
136    #[test]
137    fn test_sanitize_then_repair_workflow() {
138        // Define schema with enum arrays
139        let schema =
140            TaggedEnumSchema::new("type", &["AddDerive"], |_| Some(&["target", "derives"][..]))
141                .with_enum_array("derives", ["Debug", "Clone", "Serialize"]);
142
143        // LLM output with BOTH syntax errors AND typos
144        let malformed_llm_output = r#"{
145            "type": "AddDeriv",
146            "taget": "User",
147            "derives": ["Debg", "Clne",],
148        }"#;
149
150        // Step 1: Sanitize
151        let sanitized = sanitize_json(malformed_llm_output);
152
153        // Verify sanitization worked
154        assert!(!sanitized.contains(",]"));
155        assert!(!sanitized.contains(",}"));
156
157        // Step 2: Repair
158        let result =
159            repair_tagged_enum_json(&sanitized, &schema, &FuzzyOptions::default()).unwrap();
160
161        // Verify all corrections
162        assert_eq!(result.repaired["type"], "AddDerive");
163        assert!(result.repaired.get("target").is_some());
164        assert_eq!(result.repaired["derives"][0], "Debug");
165        assert_eq!(result.repaired["derives"][1], "Clone");
166    }
167
168    #[test]
169    fn test_sanitize_then_repair_missing_brace() {
170        let schema = TaggedEnumSchema::new("type", &["Action"], |_| Some(&["name"][..]));
171
172        // LLM truncated output
173        let truncated = r#"{"type": "Action", "name": "test"#;
174
175        let sanitized = sanitize_json(truncated);
176        let result =
177            repair_tagged_enum_json(&sanitized, &schema, &FuzzyOptions::default()).unwrap();
178
179        assert_eq!(result.repaired["type"], "Action");
180        assert_eq!(result.repaired["name"], "test");
181    }
182
183    #[test]
184    fn test_extract_sanitize_repair_full_pipeline() {
185        // Raw LLM output: prose + fence + trailing comma + typos + truncation
186        let raw = "Sure, here it is:\n\n```json\n{\"type\": \"AddDeriv\", \"taget\": \"User\", \"derives\": [\"Debg\",]\n```";
187
188        let schema =
189            TaggedEnumSchema::new("type", &["AddDerive"], |_| Some(&["target", "derives"][..]))
190                .with_enum_array("derives", ["Debug", "Clone"]);
191
192        let payload = extract_json(raw).unwrap();
193        let sanitized = sanitize_json(payload);
194        let result =
195            repair_tagged_enum_json(&sanitized, &schema, &FuzzyOptions::default()).unwrap();
196
197        assert_eq!(result.repaired["type"], "AddDerive");
198        assert_eq!(result.repaired["target"], "User");
199        assert_eq!(result.repaired["derives"][0], "Debug");
200    }
201}