1use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use uuid::Uuid;
36
37fn serialize_optional_uuid<S>(uuid: &Option<Uuid>, serializer: S) -> Result<S::Ok, S::Error>
39where
40 S: Serializer,
41{
42 match uuid {
43 Some(id) => serializer.serialize_str(&id.to_string()),
44 None => serializer.serialize_none(),
45 }
46}
47
48fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
50where
51 D: Deserializer<'de>,
52{
53 let opt_str: Option<String> = Option::deserialize(deserializer)?;
54 match opt_str {
55 Some(s) => Uuid::parse_str(&s)
56 .map(Some)
57 .map_err(serde::de::Error::custom),
58 None => Ok(None),
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ClaudeInput {
66 User(UserMessage),
68
69 #[serde(untagged)]
71 Raw(Value),
72}
73
74#[derive(Debug, Clone)]
76pub struct ParseError {
77 pub raw_json: Value,
79 pub error_message: String,
81}
82
83impl fmt::Display for ParseError {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
86 }
87}
88
89impl std::error::Error for ParseError {}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(tag = "type", rename_all = "snake_case")]
94pub enum ClaudeOutput {
95 System(SystemMessage),
97
98 User(UserMessage),
100
101 Assistant(AssistantMessage),
103
104 Result(ResultMessage),
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct UserMessage {
111 pub message: MessageContent,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 #[serde(
114 serialize_with = "serialize_optional_uuid",
115 deserialize_with = "deserialize_optional_uuid"
116 )]
117 pub session_id: Option<Uuid>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct MessageContent {
123 pub role: String,
124 #[serde(deserialize_with = "deserialize_content_blocks")]
125 pub content: Vec<ContentBlock>,
126}
127
128fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
130where
131 D: Deserializer<'de>,
132{
133 let value: Value = Value::deserialize(deserializer)?;
134 match value {
135 Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
136 Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
137 _ => Err(serde::de::Error::custom(
138 "content must be a string or array",
139 )),
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SystemMessage {
146 pub subtype: String,
147 #[serde(flatten)]
148 pub data: Value, }
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct AssistantMessage {
154 pub message: AssistantMessageContent,
155 pub session_id: String,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub uuid: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub parent_tool_use_id: Option<String>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct AssistantMessageContent {
165 pub id: String,
166 pub role: String,
167 pub model: String,
168 pub content: Vec<ContentBlock>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub stop_reason: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub stop_sequence: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub usage: Option<serde_json::Value>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(tag = "type", rename_all = "snake_case")]
180pub enum ContentBlock {
181 Text(TextBlock),
182 Image(ImageBlock),
183 Thinking(ThinkingBlock),
184 ToolUse(ToolUseBlock),
185 ToolResult(ToolResultBlock),
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct TextBlock {
191 pub text: String,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ImageBlock {
197 pub source: ImageSource,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ImageSource {
203 #[serde(rename = "type")]
204 pub source_type: String, pub media_type: String, pub data: String, }
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct ThinkingBlock {
212 pub thinking: String,
213 pub signature: String,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ToolUseBlock {
219 pub id: String,
220 pub name: String,
221 pub input: Value,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ToolResultBlock {
227 pub tool_use_id: String,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub content: Option<ToolResultContent>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub is_error: Option<bool>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236#[serde(untagged)]
237pub enum ToolResultContent {
238 Text(String),
239 Structured(Vec<Value>),
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct ResultMessage {
245 pub subtype: ResultSubtype,
246 pub is_error: bool,
247 pub duration_ms: u64,
248 pub duration_api_ms: u64,
249 pub num_turns: i32,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub result: Option<String>,
253
254 pub session_id: String,
255 pub total_cost_usd: f64,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub usage: Option<UsageInfo>,
259
260 #[serde(default)]
261 pub permission_denials: Vec<Value>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub uuid: Option<String>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269#[serde(rename_all = "snake_case")]
270pub enum ResultSubtype {
271 Success,
272 ErrorMaxTurns,
273 ErrorDuringExecution,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(tag = "type", rename_all = "snake_case")]
279pub enum McpServerConfig {
280 Stdio(McpStdioServerConfig),
281 Sse(McpSseServerConfig),
282 Http(McpHttpServerConfig),
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct McpStdioServerConfig {
288 pub command: String,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub args: Option<Vec<String>>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub env: Option<std::collections::HashMap<String, String>>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct McpSseServerConfig {
298 pub url: String,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub headers: Option<std::collections::HashMap<String, String>>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct McpHttpServerConfig {
306 pub url: String,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub headers: Option<std::collections::HashMap<String, String>>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub enum PermissionMode {
315 Default,
316 AcceptEdits,
317 BypassPermissions,
318 Plan,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct UsageInfo {
324 pub input_tokens: u32,
325 pub cache_creation_input_tokens: u32,
326 pub cache_read_input_tokens: u32,
327 pub output_tokens: u32,
328 pub server_tool_use: ServerToolUse,
329 pub service_tier: String,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct ServerToolUse {
335 pub web_search_requests: u32,
336}
337
338impl ClaudeInput {
339 pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
341 ClaudeInput::User(UserMessage {
342 message: MessageContent {
343 role: "user".to_string(),
344 content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
345 },
346 session_id: Some(session_id),
347 })
348 }
349
350 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
352 ClaudeInput::User(UserMessage {
353 message: MessageContent {
354 role: "user".to_string(),
355 content: blocks,
356 },
357 session_id: Some(session_id),
358 })
359 }
360
361 pub fn user_message_with_image(
364 image_data: String,
365 media_type: String,
366 text: Option<String>,
367 session_id: Uuid,
368 ) -> Result<Self, String> {
369 let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
371
372 if !valid_types.contains(&media_type.as_str()) {
373 return Err(format!(
374 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
375 media_type
376 ));
377 }
378
379 let mut blocks = vec![ContentBlock::Image(ImageBlock {
380 source: ImageSource {
381 source_type: "base64".to_string(),
382 media_type,
383 data: image_data,
384 },
385 })];
386
387 if let Some(text_content) = text {
388 blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
389 }
390
391 Ok(Self::user_message_blocks(blocks, session_id))
392 }
393}
394
395impl ClaudeOutput {
396 pub fn message_type(&self) -> String {
398 match self {
399 ClaudeOutput::System(_) => "system".to_string(),
400 ClaudeOutput::User(_) => "user".to_string(),
401 ClaudeOutput::Assistant(_) => "assistant".to_string(),
402 ClaudeOutput::Result(_) => "result".to_string(),
403 }
404 }
405
406 pub fn is_error(&self) -> bool {
408 matches!(self, ClaudeOutput::Result(r) if r.is_error)
409 }
410
411 pub fn is_assistant_message(&self) -> bool {
413 matches!(self, ClaudeOutput::Assistant(_))
414 }
415
416 pub fn is_system_message(&self) -> bool {
418 matches!(self, ClaudeOutput::System(_))
419 }
420
421 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
426 match Self::parse_json(s) {
428 Ok(output) => Ok(output),
429 Err(first_error) => {
430 if let Some(json_start) = s.find('{') {
432 let trimmed = &s[json_start..];
433 match Self::parse_json(trimmed) {
434 Ok(output) => Ok(output),
435 Err(_) => {
436 Err(first_error)
438 }
439 }
440 } else {
441 Err(first_error)
442 }
443 }
444 }
445 }
446
447 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
449 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
451 raw_json: Value::String(s.to_string()),
452 error_message: format!("Invalid JSON: {}", e),
453 })?;
454
455 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
457 raw_json: value,
458 error_message: e.to_string(),
459 })
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_serialize_user_message() {
469 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
470 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
471 let json = serde_json::to_string(&input).unwrap();
472 assert!(json.contains("\"type\":\"user\""));
473 assert!(json.contains("\"role\":\"user\""));
474 assert!(json.contains("\"text\":\"Hello, Claude!\""));
475 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
476 }
477
478 #[test]
479 fn test_deserialize_assistant_message() {
480 let json = r#"{
481 "type": "assistant",
482 "message": {
483 "id": "msg_123",
484 "role": "assistant",
485 "model": "claude-3-sonnet",
486 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
487 },
488 "session_id": "123"
489 }"#;
490
491 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
492 assert!(output.is_assistant_message());
493 }
494
495 #[test]
496 fn test_deserialize_result_message() {
497 let json = r#"{
498 "type": "result",
499 "subtype": "success",
500 "is_error": false,
501 "duration_ms": 100,
502 "duration_api_ms": 200,
503 "num_turns": 1,
504 "result": "Done",
505 "session_id": "123",
506 "total_cost_usd": 0.01,
507 "permission_denials": []
508 }"#;
509
510 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
511 assert!(!output.is_error());
512 }
513}