1use serde::Deserialize;
2use serde::de::DeserializeOwned;
3
4use crate::error::ClaudeError;
5
6#[derive(Debug, Clone, Deserialize)]
8#[non_exhaustive]
9pub struct ClaudeResponse {
10 pub result: String,
12 pub is_error: bool,
14 pub duration_ms: u64,
16 pub num_turns: u32,
18 pub session_id: String,
20 pub total_cost_usd: f64,
22 pub stop_reason: String,
24 pub usage: Usage,
26}
27
28#[derive(Debug, Clone, Deserialize)]
30#[non_exhaustive]
31pub struct Usage {
32 pub input_tokens: u64,
34 pub output_tokens: u64,
36 pub cache_read_input_tokens: u64,
38 pub cache_creation_input_tokens: u64,
40}
41
42impl ClaudeResponse {
43 pub fn parse_result<T: DeserializeOwned>(&self) -> Result<T, ClaudeError> {
49 serde_json::from_str(&self.result).map_err(|e| ClaudeError::StructuredOutputError {
50 raw_result: self.result.clone(),
51 source: e,
52 })
53 }
54}
55
56pub(crate) fn strip_ansi(input: &str) -> &str {
58 let start = input.find('{');
61 let end = input.rfind('}');
62 match (start, end) {
63 (Some(s), Some(e)) if s <= e => &input[s..=e],
64 _ => input,
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn deserialize_success_fixture() {
74 let json = include_str!("../tests/fixtures/success.json");
75 let resp: ClaudeResponse = serde_json::from_str(json).unwrap();
76 assert_eq!(resp.result, "Hello!");
77 assert!(!resp.is_error);
78 assert_eq!(resp.num_turns, 1);
79 assert_eq!(resp.usage.input_tokens, 10);
80 assert_eq!(resp.usage.output_tokens, 421);
81 }
82
83 #[test]
84 fn deserialize_error_fixture() {
85 let json = include_str!("../tests/fixtures/error_response.json");
86 let resp: ClaudeResponse = serde_json::from_str(json).unwrap();
87 assert!(resp.is_error);
88 assert_eq!(resp.result, "Error: invalid request");
89 assert_eq!(resp.total_cost_usd, 0.0);
90 }
91
92 #[test]
93 fn strip_ansi_with_escape_sequences() {
94 let input = "\x1b[?1004l{\"result\":\"hello\"}\x1b[?1004l";
95 assert_eq!(strip_ansi(input), "{\"result\":\"hello\"}");
96 }
97
98 #[test]
99 fn strip_ansi_without_escape_sequences() {
100 let input = "{\"result\":\"hello\"}";
101 assert_eq!(strip_ansi(input), "{\"result\":\"hello\"}");
102 }
103
104 #[test]
105 fn strip_ansi_no_json() {
106 let input = "no json here";
107 assert_eq!(strip_ansi(input), "no json here");
108 }
109
110 #[derive(Debug, Deserialize, PartialEq)]
111 struct Answer {
112 value: i32,
113 }
114
115 #[test]
116 fn parse_result_success() {
117 let json = include_str!("../tests/fixtures/structured_success.json");
118 let resp: ClaudeResponse = serde_json::from_str(json).unwrap();
119 let answer: Answer = resp.parse_result().unwrap();
120 assert_eq!(answer, Answer { value: 42 });
121 }
122
123 #[test]
124 fn parse_result_invalid_json() {
125 let resp = ClaudeResponse {
126 result: "not valid json".into(),
127 is_error: false,
128 duration_ms: 0,
129 num_turns: 0,
130 session_id: String::new(),
131 total_cost_usd: 0.0,
132 stop_reason: String::new(),
133 usage: Usage {
134 input_tokens: 0,
135 output_tokens: 0,
136 cache_read_input_tokens: 0,
137 cache_creation_input_tokens: 0,
138 },
139 };
140 let err = resp.parse_result::<Answer>().unwrap_err();
141 match err {
142 crate::error::ClaudeError::StructuredOutputError { raw_result, .. } => {
143 assert_eq!(raw_result, "not valid json");
144 }
145 _ => panic!("expected StructuredOutputError, got {err:?}"),
146 }
147 }
148
149 #[test]
150 fn parse_result_type_mismatch() {
151 let resp = ClaudeResponse {
152 result: r#"{"wrong_field": "hello"}"#.into(),
153 is_error: false,
154 duration_ms: 0,
155 num_turns: 0,
156 session_id: String::new(),
157 total_cost_usd: 0.0,
158 stop_reason: String::new(),
159 usage: Usage {
160 input_tokens: 0,
161 output_tokens: 0,
162 cache_read_input_tokens: 0,
163 cache_creation_input_tokens: 0,
164 },
165 };
166 let err = resp.parse_result::<Answer>().unwrap_err();
167 assert!(matches!(
168 err,
169 crate::error::ClaudeError::StructuredOutputError { .. }
170 ));
171 }
172}