use serde::{Deserialize, Deserializer};
use serde_json::Value;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeCodeLog {
#[serde(default)]
pub cwd: String,
#[serde(default)]
pub session_id: String,
#[serde(default, rename = "type")]
pub log_type: String,
#[serde(default)]
pub timestamp: String,
#[serde(default)]
pub message: Option<ClaudeMessage>,
#[serde(default, deserialize_with = "deserialize_tool_use_result")]
pub tool_use_result: Option<ClaudeToolUseResult>,
#[serde(default)]
pub is_sidechain: bool,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ClaudeMessage {
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub usage: Option<Value>,
#[serde(default, deserialize_with = "deserialize_content_items")]
pub content: Vec<ClaudeContentItem>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClaudeContentItem {
ToolUse {
#[serde(default)]
id: String,
#[serde(default)]
name: String,
#[serde(default)]
input: Option<ClaudeToolInput>,
},
ToolResult {
#[serde(default)]
tool_use_id: String,
#[serde(default, deserialize_with = "deserialize_tool_result_content")]
content: String,
#[serde(default)]
is_error: bool,
},
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ClaudeToolInput {
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub file_path: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub old_string: Option<String>,
#[serde(default)]
pub new_string: Option<String>,
}
fn deserialize_tool_result_content<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
match value {
Value::String(s) => Ok(s),
Value::Array(arr) => {
let texts: Vec<&str> = arr
.iter()
.filter_map(|item| item.get("text").and_then(|t| t.as_str()))
.collect();
Ok(texts.join("\n"))
}
Value::Null => Ok(String::new()),
_ => Ok(value.to_string()),
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeToolUseResult {
#[serde(default, rename = "type")]
pub result_type: Option<String>,
#[serde(default)]
pub file: Option<ClaudeToolUseFile>,
#[serde(default)]
pub file_path: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub new_string: Option<String>,
#[serde(default)]
pub old_string: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeToolUseFile {
#[serde(default)]
pub file_path: Option<String>,
#[serde(default)]
pub content: Option<String>,
}
fn deserialize_tool_use_result<'de, D>(
deserializer: D,
) -> Result<Option<ClaudeToolUseResult>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::IgnoredAny;
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Object(Box<ClaudeToolUseResult>),
#[allow(dead_code)]
Ignored(IgnoredAny),
}
match Option::<Repr>::deserialize(deserializer)? {
Some(Repr::Object(obj)) => Ok(Some(*obj)),
_ => Ok(None),
}
}
fn deserialize_content_items<'de, D>(deserializer: D) -> Result<Vec<ClaudeContentItem>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::IgnoredAny;
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Array(Vec<ClaudeContentItem>),
#[allow(dead_code)]
Ignored(IgnoredAny),
}
match Option::<Repr>::deserialize(deserializer)? {
Some(Repr::Array(arr)) => Ok(arr),
_ => Ok(Vec::new()),
}
}