use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolUseResult {
Text(TextResult),
Create(CreateResult),
Update(UpdateResult),
Delete(DeleteResult),
Read(ReadResult),
Error(ErrorResult),
Image(ImageResult),
#[serde(other)]
Unknown,
}
impl ToolUseResult {
pub fn file_path(&self) -> Option<&str> {
match self {
Self::Create(r) => Some(&r.file_path),
Self::Update(r) => Some(&r.file_path),
Self::Delete(r) => Some(&r.file_path),
Self::Read(r) => Some(&r.file_path),
Self::Image(r) => r.file_path.as_deref(),
_ => None,
}
}
pub fn is_create(&self) -> bool {
matches!(self, Self::Create(_))
}
pub fn is_update(&self) -> bool {
matches!(self, Self::Update(_))
}
pub fn is_error(&self) -> bool {
matches!(self, Self::Error(_))
}
pub fn is_image(&self) -> bool {
matches!(self, Self::Image(_))
}
pub fn text_content(&self) -> Option<&str> {
match self {
Self::Text(r) => Some(&r.content),
Self::Create(r) => Some(&r.content),
Self::Update(r) => Some(&r.content),
Self::Read(r) => Some(&r.content),
_ => None,
}
}
pub fn as_create(&self) -> Option<&CreateResult> {
match self {
Self::Create(r) => Some(r),
_ => None,
}
}
pub fn as_update(&self) -> Option<&UpdateResult> {
match self {
Self::Update(r) => Some(r),
_ => None,
}
}
}
pub fn deserialize_tool_use_result_lenient<'de, D>(
deserializer: D,
) -> Result<Option<ToolUseResult>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<JsonValue> = Option::deserialize(deserializer)?;
Ok(value.and_then(|raw| serde_json::from_value(raw).ok()))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextResult {
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateResult {
#[serde(rename = "filePath")]
pub file_path: String,
pub content: String,
#[serde(rename = "structuredPatch")]
pub structured_patch: Vec<PatchHunk>,
#[serde(rename = "originalFile")]
pub original_file: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateResult {
#[serde(rename = "filePath")]
pub file_path: String,
pub content: String,
#[serde(rename = "structuredPatch")]
pub structured_patch: Vec<PatchHunk>,
#[serde(rename = "originalFile")]
pub original_file: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteResult {
#[serde(rename = "filePath")]
pub file_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResult {
#[serde(rename = "filePath")]
pub file_path: String,
pub content: String,
#[serde(rename = "lineCount")]
pub line_count: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResult {
pub error: String,
#[serde(rename = "toolName")]
pub tool_name: Option<String>,
#[serde(flatten)]
pub extra: JsonValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageResult {
pub source: ImageSource,
#[serde(rename = "filePath")]
pub file_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageSource {
#[serde(rename = "type")]
pub source_type: String,
#[serde(rename = "media_type")]
pub media_type: String,
pub data: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatchHunk {
#[serde(rename = "oldStart")]
pub old_start: u64,
#[serde(rename = "oldLines")]
pub old_lines: u64,
#[serde(rename = "newStart")]
pub new_start: u64,
#[serde(rename = "newLines")]
pub new_lines: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_text_result() {
let json = r#"{
"type": "text",
"content": "cargo build succeeded"
}"#;
let result: ToolUseResult = serde_json::from_str(json).unwrap();
assert!(matches!(result, ToolUseResult::Text(_)));
if let ToolUseResult::Text(text) = result {
assert_eq!(text.content, "cargo build succeeded");
}
}
#[test]
fn test_parse_create_result() {
let json = r#"{
"type": "create",
"filePath": "/test/new_file.rs",
"content": "pub fn main() {}",
"structuredPatch": [],
"originalFile": null
}"#;
let result: ToolUseResult = serde_json::from_str(json).unwrap();
assert!(result.is_create());
assert_eq!(result.file_path(), Some("/test/new_file.rs"));
if let ToolUseResult::Create(create) = result {
assert_eq!(create.file_path, "/test/new_file.rs");
assert_eq!(create.content, "pub fn main() {}");
assert!(create.structured_patch.is_empty());
assert!(create.original_file.is_none());
}
}
#[test]
fn test_parse_update_result() {
let json = r#"{
"type": "update",
"filePath": "/test/file.rs",
"content": "pub fn main() { println!(\"Hello\"); }",
"structuredPatch": [
{
"oldStart": 1,
"oldLines": 1,
"newStart": 1,
"newLines": 1
}
],
"originalFile": "pub fn main() {}"
}"#;
let result: ToolUseResult = serde_json::from_str(json).unwrap();
assert!(result.is_update());
assert_eq!(result.file_path(), Some("/test/file.rs"));
if let ToolUseResult::Update(update) = result {
assert_eq!(update.file_path, "/test/file.rs");
assert_eq!(update.content, "pub fn main() { println!(\"Hello\"); }");
assert_eq!(update.structured_patch.len(), 1);
assert_eq!(update.original_file, Some("pub fn main() {}".to_string()));
let patch = &update.structured_patch[0];
assert_eq!(patch.old_start, 1);
assert_eq!(patch.old_lines, 1);
assert_eq!(patch.new_start, 1);
assert_eq!(patch.new_lines, 1);
}
}
#[test]
fn test_parse_delete_result() {
let json = r#"{
"type": "delete",
"filePath": "/test/deleted.rs"
}"#;
let result: ToolUseResult = serde_json::from_str(json).unwrap();
assert_eq!(result.file_path(), Some("/test/deleted.rs"));
}
#[test]
fn test_parse_error_result() {
let json = r#"{
"type": "error",
"error": "File not found",
"toolName": "Read"
}"#;
let result: ToolUseResult = serde_json::from_str(json).unwrap();
assert!(result.is_error());
if let ToolUseResult::Error(error) = result {
assert_eq!(error.error, "File not found");
assert_eq!(error.tool_name, Some("Read".to_string()));
}
}
#[test]
fn test_parse_image_result() {
let json = r#"{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "iVBORw0KGgo="
},
"filePath": "/tmp/screenshot.png"
}"#;
let result: ToolUseResult = serde_json::from_str(json).unwrap();
assert!(result.is_image());
assert_eq!(result.file_path(), Some("/tmp/screenshot.png"));
if let ToolUseResult::Image(image) = result {
assert_eq!(image.source.source_type, "base64");
assert_eq!(image.source.media_type, "image/png");
assert_eq!(image.source.data, "iVBORw0KGgo=");
assert_eq!(image.file_path, Some("/tmp/screenshot.png".to_string()));
}
}
#[test]
fn test_lenient_tool_use_result_accepts_matching_shape() {
#[derive(Deserialize)]
struct Wrapper {
#[serde(deserialize_with = "deserialize_tool_use_result_lenient", default)]
result: Option<ToolUseResult>,
}
let json = r#"{"result":{"type":"text","content":"cargo build succeeded"}}"#;
let wrapper: Wrapper = serde_json::from_str(json).unwrap();
assert!(matches!(wrapper.result, Some(ToolUseResult::Text(_))));
}
#[test]
fn test_lenient_tool_use_result_drops_mismatched_shape_without_failing() {
#[derive(Deserialize)]
struct Wrapper {
#[serde(deserialize_with = "deserialize_tool_use_result_lenient", default)]
result: Option<ToolUseResult>,
}
let json = r#"{"result":{"type":"text","file":{"filePath":"/tmp/x","content":"","numLines":1,"startLine":1,"totalLines":1}}}"#;
let wrapper: Wrapper = serde_json::from_str(json).unwrap();
assert!(wrapper.result.is_none());
}
#[test]
fn test_lenient_tool_use_result_accepts_missing_field() {
#[derive(Deserialize)]
struct Wrapper {
#[serde(deserialize_with = "deserialize_tool_use_result_lenient", default)]
result: Option<ToolUseResult>,
}
let wrapper: Wrapper = serde_json::from_str("{}").unwrap();
assert!(wrapper.result.is_none());
}
#[test]
fn test_text_content_extraction() {
let text_result = ToolUseResult::Text(TextResult {
content: "Output".to_string(),
});
assert_eq!(text_result.text_content(), Some("Output"));
let create_result = ToolUseResult::Create(CreateResult {
file_path: "/test.rs".to_string(),
content: "Code".to_string(),
structured_patch: vec![],
original_file: None,
});
assert_eq!(create_result.text_content(), Some("Code"));
let delete_result = ToolUseResult::Delete(DeleteResult {
file_path: "/test.rs".to_string(),
});
assert_eq!(delete_result.text_content(), None);
}
}