pub mod constraints;
pub mod deserializer;
pub mod error;
pub mod parser;
pub mod scoring;
pub mod value;
#[doc(hidden)]
pub fn __ensure_primitives_linked() {
deserializer::primitives::__ensure_linked();
}
use std::time::{Duration, Instant};
use deserializer::{CoercingDeserializer, CoercionContext, LlmDeserialize};
use error::{ParseError, Result};
use parser::FlexibleParser;
use serde::de::DeserializeOwned;
#[derive(Debug, Clone)]
pub struct ParseMetadata {
pub strategy_used: String,
pub duration: Duration,
pub candidates_evaluated: usize,
pub total_candidates: usize,
pub winning_score: u32,
}
use value::FlexValue;
pub fn parse<T: DeserializeOwned>(input: &str) -> Result<T> {
let (result, _candidates) = parse_with_candidates(input)?;
Ok(result)
}
pub fn parse_with_candidates<T: DeserializeOwned>(input: &str) -> Result<(T, Vec<FlexValue>)> {
let parser = FlexibleParser::new();
let candidates = parser.parse(input)?;
if candidates.is_empty() {
return Err(ParseError::NoCandidates);
}
let mut errors = Vec::new();
let ranked = scoring::rank_candidates(candidates);
for i in 0..ranked.len() {
let candidate = &ranked[i];
let mut deserializer = CoercingDeserializer::new(candidate.clone());
match T::deserialize(&mut deserializer) {
Ok(value) => {
return Ok((value, ranked));
}
Err(e) => {
let source_name = match &candidate.source {
value::Source::Direct => "direct".to_string(),
value::Source::Markdown { lang } => {
format!("markdown({})", lang.as_deref().unwrap_or(""))
}
value::Source::Fixed { .. } => "fixed".to_string(),
value::Source::MultiJson { index } => format!("multi_json[{}]", index),
value::Source::MultiJsonArray => "multi_json_array".to_string(),
value::Source::Heuristic { pattern } => format!("heuristic({})", pattern),
value::Source::Yaml => "yaml".to_string(),
};
let preview: String = candidate.value.to_string().chars().take(100).collect();
let score = scoring::score_candidate(candidate);
errors.push(error::CandidateError {
source: source_name,
score,
preview,
error: e,
});
}
}
}
Err(ParseError::AllCandidatesFailed(error::AllCandidatesError {
attempts: errors,
}))
}
pub fn parse_with_metadata<T: DeserializeOwned>(input: &str) -> Result<(T, ParseMetadata)> {
let start = Instant::now();
let parser = FlexibleParser::new();
let candidates = parser.parse(input)?;
if candidates.is_empty() {
return Err(ParseError::NoCandidates);
}
let total_candidates = candidates.len();
let ranked = scoring::rank_candidates(candidates);
let mut errors = Vec::new();
for (idx, candidate) in ranked.iter().enumerate() {
let candidates_evaluated = idx + 1;
let mut deserializer = CoercingDeserializer::new(candidate.clone());
match T::deserialize(&mut deserializer) {
Ok(value) => {
let source_name = match &candidate.source {
value::Source::Direct => "direct".to_string(),
value::Source::Markdown { lang } => {
format!("markdown({})", lang.as_deref().unwrap_or(""))
}
value::Source::Fixed { .. } => "fixed".to_string(),
value::Source::MultiJson { index } => format!("multi_json[{}]", index),
value::Source::MultiJsonArray => "multi_json_array".to_string(),
value::Source::Heuristic { pattern } => format!("heuristic({})", pattern),
value::Source::Yaml => "yaml".to_string(),
};
let metadata = ParseMetadata {
strategy_used: source_name,
duration: start.elapsed(),
candidates_evaluated,
total_candidates,
winning_score: scoring::score_candidate(candidate),
};
return Ok((value, metadata));
}
Err(e) => {
errors.push(e);
}
}
}
Err(ParseError::AllCandidatesFailed(error::AllCandidatesError {
attempts: ranked
.iter()
.zip(errors)
.map(|(candidate, error)| {
let source_name = match &candidate.source {
value::Source::Direct => "direct".to_string(),
value::Source::Markdown { lang } => {
format!("markdown({})", lang.as_deref().unwrap_or(""))
}
value::Source::Fixed { .. } => "fixed".to_string(),
value::Source::MultiJson { index } => format!("multi_json[{}]", index),
value::Source::MultiJsonArray => "multi_json_array".to_string(),
value::Source::Heuristic { pattern } => format!("heuristic({})", pattern),
value::Source::Yaml => "yaml".to_string(),
};
error::CandidateError {
source: source_name,
score: scoring::score_candidate(candidate),
preview: candidate.value.to_string().chars().take(100).collect(),
error,
}
})
.collect(),
}))
}
pub fn parse_with_parser<T: DeserializeOwned>(input: &str, parser: &FlexibleParser) -> Result<T> {
let candidates = parser.parse(input)?;
if candidates.is_empty() {
return Err(ParseError::NoCandidates);
}
let ranked = scoring::rank_candidates(candidates);
for candidate in ranked {
let mut deserializer = CoercingDeserializer::new(candidate);
if let Ok(value) = T::deserialize(&mut deserializer) {
return Ok(value);
}
}
Err(ParseError::NoCandidates)
}
pub fn parse_llm<T: LlmDeserialize>(input: &str) -> Result<T> {
let (result, _candidates) = parse_llm_with_candidates(input)?;
Ok(result)
}
pub fn parse_llm_with_candidates<T: LlmDeserialize>(input: &str) -> Result<(T, Vec<FlexValue>)> {
let parser = FlexibleParser::new();
let candidates = parser.parse(input)?;
if candidates.is_empty() {
return Err(ParseError::NoCandidates);
}
let ranked = scoring::rank_candidates(candidates);
for (idx, candidate) in ranked.iter().enumerate() {
let mut ctx = CoercionContext::new();
if let Some(value) = T::try_deserialize(candidate, &mut ctx) {
let mut updated_ranked = ranked.clone();
for transformation in ctx.transformations() {
updated_ranked[idx].add_transformation(transformation.clone());
}
return Ok((value, updated_ranked));
}
}
for (idx, candidate) in ranked.iter().enumerate() {
let mut ctx = CoercionContext::new();
match T::deserialize(candidate, &mut ctx) {
Ok(value) => {
let mut updated_ranked = ranked.clone();
for transformation in ctx.transformations() {
updated_ranked[idx].add_transformation(transformation.clone());
}
return Ok((value, updated_ranked));
}
Err(_) => {
continue;
}
}
}
Err(ParseError::NoCandidates)
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use super::*;
#[derive(Deserialize, Debug, PartialEq)]
struct User {
name: String,
age: u32,
}
#[test]
fn test_parse_clean_json() {
let input = r#"{"name": "Alice", "age": 30}"#;
let user: User = parse(input).unwrap();
assert_eq!(user.name, "Alice");
assert_eq!(user.age, 30);
}
#[test]
fn test_parse_with_type_coercion() {
let input = r#"{"name": "Bob", "age": "25"}"#;
let user: User = parse(input).unwrap();
assert_eq!(user.age, 25);
}
#[test]
fn test_parse_markdown() {
let input = r#"
Here's the user:
```json
{"name": "Charlie", "age": 35}
```
"#;
let user: User = parse(input).unwrap();
assert_eq!(user.name, "Charlie");
}
#[test]
fn test_parse_with_trailing_comma() {
let input = r#"{"name": "Dave", "age": 40,}"#;
let user: User = parse(input).unwrap();
assert_eq!(user.name, "Dave");
}
#[test]
fn test_parse_with_unquoted_keys() {
let input = r#"{name: "Eve", age: 45}"#;
let user: User = parse(input).unwrap();
assert_eq!(user.name, "Eve");
}
#[test]
fn test_parse_with_single_quotes() {
let input = r#"{'name': 'Frank', 'age': 50}"#;
let user: User = parse(input).unwrap();
assert_eq!(user.name, "Frank");
}
#[test]
fn test_parse_with_candidates() {
let input = r#"{"name": "Grace", "age": "55"}"#;
let (user, candidates): (User, _) = parse_with_candidates(input).unwrap();
assert_eq!(user.name, "Grace");
assert!(!candidates.is_empty());
}
#[test]
fn test_parse_invalid_input() {
let input = "This is not JSON at all";
let result: Result<User> = parse(input);
assert!(result.is_err());
}
#[test]
fn test_parse_array() {
let input = r#"[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]"#;
let users: Vec<User> = parse(input).unwrap();
assert_eq!(users.len(), 2);
}
#[test]
fn test_parse_nested_struct() {
#[derive(Deserialize, Debug, PartialEq)]
struct Address {
city: String,
}
#[derive(Deserialize, Debug, PartialEq)]
struct Person {
name: String,
address: Address,
}
let input = r#"{"name": "Alice", "address": {"city": "NYC"}}"#;
let person: Person = parse(input).unwrap();
assert_eq!(person.address.city, "NYC");
}
#[test]
fn test_parse_with_custom_parser() {
let parser = FlexibleParser::new();
let input = r#"{"name": "Alice", "age": 30}"#;
let user: User = parse_with_parser(input, &parser).unwrap();
assert_eq!(user.name, "Alice");
}
}