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 pub content: Vec<ContentBlock>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct SystemMessage {
130 pub subtype: String,
131 #[serde(flatten)]
132 pub data: Value, }
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct AssistantMessage {
138 pub message: AssistantMessageContent,
139 pub session_id: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub uuid: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub parent_tool_use_id: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct AssistantMessageContent {
149 pub id: String,
150 pub role: String,
151 pub model: String,
152 pub content: Vec<ContentBlock>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub stop_reason: Option<String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub stop_sequence: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub usage: Option<serde_json::Value>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(tag = "type", rename_all = "snake_case")]
164pub enum ContentBlock {
165 Text(TextBlock),
166 Image(ImageBlock),
167 Thinking(ThinkingBlock),
168 ToolUse(ToolUseBlock),
169 ToolResult(ToolResultBlock),
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TextBlock {
175 pub text: String,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ImageBlock {
181 pub source: ImageSource,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ImageSource {
187 #[serde(rename = "type")]
188 pub source_type: String, pub media_type: String, pub data: String, }
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ThinkingBlock {
196 pub thinking: String,
197 pub signature: String,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ToolUseBlock {
203 pub id: String,
204 pub name: String,
205 pub input: Value,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ToolResultBlock {
211 pub tool_use_id: String,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub content: Option<ToolResultContent>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub is_error: Option<bool>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220#[serde(untagged)]
221pub enum ToolResultContent {
222 Text(String),
223 Structured(Vec<Value>),
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ResultMessage {
229 pub subtype: ResultSubtype,
230 pub is_error: bool,
231 pub duration_ms: u64,
232 pub duration_api_ms: u64,
233 pub num_turns: i32,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub result: Option<String>,
237
238 pub session_id: String,
239 pub total_cost_usd: f64,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub usage: Option<UsageInfo>,
243
244 #[serde(default)]
245 pub permission_denials: Vec<Value>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub uuid: Option<String>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum ResultSubtype {
255 Success,
256 ErrorMaxTurns,
257 ErrorDuringExecution,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(tag = "type", rename_all = "snake_case")]
263pub enum McpServerConfig {
264 Stdio(McpStdioServerConfig),
265 Sse(McpSseServerConfig),
266 Http(McpHttpServerConfig),
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct McpStdioServerConfig {
272 pub command: String,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub args: Option<Vec<String>>,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub env: Option<std::collections::HashMap<String, String>>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct McpSseServerConfig {
282 pub url: String,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub headers: Option<std::collections::HashMap<String, String>>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct McpHttpServerConfig {
290 pub url: String,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub headers: Option<std::collections::HashMap<String, String>>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub enum PermissionMode {
299 Default,
300 AcceptEdits,
301 BypassPermissions,
302 Plan,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct UsageInfo {
308 pub input_tokens: u32,
309 pub cache_creation_input_tokens: u32,
310 pub cache_read_input_tokens: u32,
311 pub output_tokens: u32,
312 pub server_tool_use: ServerToolUse,
313 pub service_tier: String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ServerToolUse {
319 pub web_search_requests: u32,
320}
321
322impl ClaudeInput {
323 pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
325 ClaudeInput::User(UserMessage {
326 message: MessageContent {
327 role: "user".to_string(),
328 content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
329 },
330 session_id: Some(session_id),
331 })
332 }
333
334 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
336 ClaudeInput::User(UserMessage {
337 message: MessageContent {
338 role: "user".to_string(),
339 content: blocks,
340 },
341 session_id: Some(session_id),
342 })
343 }
344
345 pub fn user_message_with_image(
348 image_data: String,
349 media_type: String,
350 text: Option<String>,
351 session_id: Uuid,
352 ) -> Result<Self, String> {
353 let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
355
356 if !valid_types.contains(&media_type.as_str()) {
357 return Err(format!(
358 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
359 media_type
360 ));
361 }
362
363 let mut blocks = vec![ContentBlock::Image(ImageBlock {
364 source: ImageSource {
365 source_type: "base64".to_string(),
366 media_type,
367 data: image_data,
368 },
369 })];
370
371 if let Some(text_content) = text {
372 blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
373 }
374
375 Ok(Self::user_message_blocks(blocks, session_id))
376 }
377}
378
379impl ClaudeOutput {
380 pub fn message_type(&self) -> String {
382 match self {
383 ClaudeOutput::System(_) => "system".to_string(),
384 ClaudeOutput::User(_) => "user".to_string(),
385 ClaudeOutput::Assistant(_) => "assistant".to_string(),
386 ClaudeOutput::Result(_) => "result".to_string(),
387 }
388 }
389
390 pub fn is_error(&self) -> bool {
392 matches!(self, ClaudeOutput::Result(r) if r.is_error)
393 }
394
395 pub fn is_assistant_message(&self) -> bool {
397 matches!(self, ClaudeOutput::Assistant(_))
398 }
399
400 pub fn is_system_message(&self) -> bool {
402 matches!(self, ClaudeOutput::System(_))
403 }
404
405 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
410 match Self::parse_json(s) {
412 Ok(output) => Ok(output),
413 Err(first_error) => {
414 if let Some(json_start) = s.find('{') {
416 let trimmed = &s[json_start..];
417 match Self::parse_json(trimmed) {
418 Ok(output) => Ok(output),
419 Err(_) => {
420 Err(first_error)
422 }
423 }
424 } else {
425 Err(first_error)
426 }
427 }
428 }
429 }
430
431 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
433 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
435 raw_json: Value::String(s.to_string()),
436 error_message: format!("Invalid JSON: {}", e),
437 })?;
438
439 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
441 raw_json: value,
442 error_message: e.to_string(),
443 })
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn test_serialize_user_message() {
453 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
454 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
455 let json = serde_json::to_string(&input).unwrap();
456 assert!(json.contains("\"type\":\"user\""));
457 assert!(json.contains("\"role\":\"user\""));
458 assert!(json.contains("\"text\":\"Hello, Claude!\""));
459 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
460 }
461
462 #[test]
463 fn test_deserialize_assistant_message() {
464 let json = r#"{
465 "type": "assistant",
466 "message": {
467 "id": "msg_123",
468 "role": "assistant",
469 "model": "claude-3-sonnet",
470 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
471 },
472 "session_id": "123"
473 }"#;
474
475 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
476 assert!(output.is_assistant_message());
477 }
478
479 #[test]
480 fn test_deserialize_result_message() {
481 let json = r#"{
482 "type": "result",
483 "subtype": "success",
484 "is_error": false,
485 "duration_ms": 100,
486 "duration_api_ms": 200,
487 "num_turns": 1,
488 "result": "Done",
489 "session_id": "123",
490 "total_cost_usd": 0.01,
491 "permission_denials": []
492 }"#;
493
494 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
495 assert!(!output.is_error());
496 }
497}