Skip to main content

synwire_core/output_parsers/
structured.rs

1//! Structured output parser that deserialises JSON to a typed struct.
2
3use std::marker::PhantomData;
4
5use serde::de::DeserializeOwned;
6
7use crate::error::{ParseError, SynwireError};
8use crate::output_parsers::OutputParser;
9
10/// Parser that deserialises JSON text to a typed struct.
11///
12/// Use this when you have a specific Rust type annotated with `#[derive(Deserialize)]`
13/// that you want to parse model output into.
14///
15/// # Examples
16///
17/// ```
18/// use serde::Deserialize;
19/// use synwire_core::output_parsers::{OutputParser, StructuredOutputParser};
20///
21/// #[derive(Deserialize, Debug, PartialEq)]
22/// struct Person {
23///     name: String,
24///     age: u32,
25/// }
26///
27/// let parser = StructuredOutputParser::<Person>::new();
28/// let result = parser.parse(r#"{"name": "Alice", "age": 30}"#).unwrap();
29/// assert_eq!(result.name, "Alice");
30/// assert_eq!(result.age, 30);
31/// ```
32pub struct StructuredOutputParser<T: DeserializeOwned> {
33    _marker: PhantomData<T>,
34}
35
36impl<T: DeserializeOwned> StructuredOutputParser<T> {
37    /// Create a new structured output parser.
38    pub const fn new() -> Self {
39        Self {
40            _marker: PhantomData,
41        }
42    }
43}
44
45impl<T: DeserializeOwned + Send + Sync> StructuredOutputParser<T> {
46    /// Parse with a validation error message for retry.
47    ///
48    /// On success, returns the parsed value. On failure, returns both the
49    /// original error and a formatted context string suitable for inclusion
50    /// in a retry prompt.
51    ///
52    /// # Errors
53    ///
54    /// Returns a tuple of (`SynwireError`, retry context `String`) when
55    /// parsing fails.
56    pub fn parse_with_retry_context(&self, text: &str) -> Result<T, (SynwireError, String)> {
57        match self.parse(text) {
58            Ok(v) => Ok(v),
59            Err(e) => {
60                let context = format!(
61                    "Previous attempt failed with error: {e}\nPlease fix the output and try again."
62                );
63                Err((e, context))
64            }
65        }
66    }
67}
68
69impl<T: DeserializeOwned> Default for StructuredOutputParser<T> {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl<T: DeserializeOwned + Send + Sync> OutputParser for StructuredOutputParser<T> {
76    type Output = T;
77
78    fn parse(&self, text: &str) -> Result<T, SynwireError> {
79        serde_json::from_str(text).map_err(|e| {
80            SynwireError::from(ParseError::ParseFailed {
81                message: format!("Failed to parse structured output: {e}"),
82            })
83        })
84    }
85
86    fn get_format_instructions(&self) -> String {
87        "Respond with valid JSON matching the expected schema.".to_string()
88    }
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used)]
93mod tests {
94    use serde::Deserialize;
95
96    use super::*;
97
98    #[derive(Debug, Deserialize, PartialEq)]
99    struct TestPerson {
100        name: String,
101        age: u32,
102    }
103
104    #[test]
105    fn test_structured_parser() {
106        let parser = StructuredOutputParser::<TestPerson>::new();
107        let result = parser.parse(r#"{"name": "Alice", "age": 30}"#).unwrap();
108        assert_eq!(
109            result,
110            TestPerson {
111                name: "Alice".to_string(),
112                age: 30,
113            }
114        );
115    }
116
117    #[test]
118    fn test_structured_parser_invalid() {
119        let parser = StructuredOutputParser::<TestPerson>::new();
120        let result = parser.parse(r#"{"name": "Alice"}"#);
121        assert!(result.is_err());
122    }
123
124    #[test]
125    fn test_structured_parser_format_instructions() {
126        let parser = StructuredOutputParser::<TestPerson>::new();
127        assert_eq!(
128            parser.get_format_instructions(),
129            "Respond with valid JSON matching the expected schema."
130        );
131    }
132
133    #[test]
134    fn test_parse_with_retry_context_success() {
135        let parser = StructuredOutputParser::<TestPerson>::new();
136        let result = parser
137            .parse_with_retry_context(r#"{"name": "Alice", "age": 30}"#)
138            .unwrap();
139        assert_eq!(result.name, "Alice");
140        assert_eq!(result.age, 30);
141    }
142
143    #[test]
144    fn test_parse_with_retry_context_failure() {
145        let parser = StructuredOutputParser::<TestPerson>::new();
146        let result = parser.parse_with_retry_context(r#"{"name": "Alice"}"#);
147        assert!(result.is_err());
148        let (err, context) = result.unwrap_err();
149        assert!(err.to_string().contains("Failed to parse"));
150        assert!(context.contains("Previous attempt failed"));
151        assert!(context.contains("Please fix the output"));
152    }
153
154    #[test]
155    fn test_structured_parser_default() {
156        let parser = StructuredOutputParser::<TestPerson>::default();
157        let result = parser.parse(r#"{"name": "Bob", "age": 25}"#).unwrap();
158        assert_eq!(result.name, "Bob");
159    }
160}