use serde::Deserialize;
use serde::de::DeserializeOwned;
use crate::error::ClaudeError;
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ClaudeResponse {
pub result: String,
pub is_error: bool,
pub duration_ms: u64,
pub num_turns: u32,
pub session_id: String,
pub total_cost_usd: f64,
pub stop_reason: String,
pub usage: Usage,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_read_input_tokens: u64,
pub cache_creation_input_tokens: u64,
}
impl ClaudeResponse {
pub fn parse_result<T: DeserializeOwned>(&self) -> Result<T, ClaudeError> {
serde_json::from_str(&self.result).map_err(|e| ClaudeError::StructuredOutputError {
raw_result: self.result.clone(),
source: e,
})
}
}
pub(crate) fn strip_ansi(input: &str) -> &str {
let start = input.find('{');
let end = input.rfind('}');
match (start, end) {
(Some(s), Some(e)) if s <= e => &input[s..=e],
_ => input,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_success_fixture() {
let json = include_str!("../tests/fixtures/success.json");
let resp: ClaudeResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.result, "Hello!");
assert!(!resp.is_error);
assert_eq!(resp.num_turns, 1);
assert_eq!(resp.usage.input_tokens, 10);
assert_eq!(resp.usage.output_tokens, 421);
}
#[test]
fn deserialize_error_fixture() {
let json = include_str!("../tests/fixtures/error_response.json");
let resp: ClaudeResponse = serde_json::from_str(json).unwrap();
assert!(resp.is_error);
assert_eq!(resp.result, "Error: invalid request");
assert_eq!(resp.total_cost_usd, 0.0);
}
#[test]
fn strip_ansi_with_escape_sequences() {
let input = "\x1b[?1004l{\"result\":\"hello\"}\x1b[?1004l";
assert_eq!(strip_ansi(input), "{\"result\":\"hello\"}");
}
#[test]
fn strip_ansi_without_escape_sequences() {
let input = "{\"result\":\"hello\"}";
assert_eq!(strip_ansi(input), "{\"result\":\"hello\"}");
}
#[test]
fn strip_ansi_no_json() {
let input = "no json here";
assert_eq!(strip_ansi(input), "no json here");
}
#[derive(Debug, Deserialize, PartialEq)]
struct Answer {
value: i32,
}
#[test]
fn parse_result_success() {
let json = include_str!("../tests/fixtures/structured_success.json");
let resp: ClaudeResponse = serde_json::from_str(json).unwrap();
let answer: Answer = resp.parse_result().unwrap();
assert_eq!(answer, Answer { value: 42 });
}
#[test]
fn parse_result_invalid_json() {
let resp = ClaudeResponse {
result: "not valid json".into(),
is_error: false,
duration_ms: 0,
num_turns: 0,
session_id: String::new(),
total_cost_usd: 0.0,
stop_reason: String::new(),
usage: Usage {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
};
let err = resp.parse_result::<Answer>().unwrap_err();
match err {
crate::error::ClaudeError::StructuredOutputError { raw_result, .. } => {
assert_eq!(raw_result, "not valid json");
}
_ => panic!("expected StructuredOutputError, got {err:?}"),
}
}
#[test]
fn parse_result_type_mismatch() {
let resp = ClaudeResponse {
result: r#"{"wrong_field": "hello"}"#.into(),
is_error: false,
duration_ms: 0,
num_turns: 0,
session_id: String::new(),
total_cost_usd: 0.0,
stop_reason: String::new(),
usage: Usage {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
};
let err = resp.parse_result::<Answer>().unwrap_err();
assert!(matches!(
err,
crate::error::ClaudeError::StructuredOutputError { .. }
));
}
}