1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9pub const PROTOCOL_VERSION: &str = "0.2.0";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum ClaudeRequest {
16 WriteFile(WriteFileRequest),
18 EditFile(EditFileRequest),
20 Ping,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct WriteFileRequest {
27 pub id: String,
29 pub path: PathBuf,
31 pub content: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub encoding: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub create_dirs: Option<bool>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct EditFileRequest {
44 pub id: String,
46 pub path: PathBuf,
48 pub old_string: String,
50 pub new_string: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub replace_all: Option<bool>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(tag = "type", rename_all = "snake_case")]
60pub enum ClaudeResponse {
61 Approved(ApprovedResponse),
63 Rejected(RejectedResponse),
65 Warning(WarningResponse),
67 Pong,
69 ProtocolError(ProtocolErrorResponse),
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ApprovedResponse {
76 pub request_id: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub modified_content: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct RejectedResponse {
86 pub request_id: String,
88 pub message: String,
90 pub issues: Vec<IssueDetail>,
92 pub can_autofix: bool,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct WarningResponse {
99 pub request_id: String,
101 pub message: String,
103 pub warnings: Vec<IssueDetail>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub modified_content: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ProtocolErrorResponse {
113 pub message: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub code: Option<String>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct IssueDetail {
123 pub code: String,
125 pub level: IssueSeverity,
127 pub message: String,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub line: Option<usize>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub column: Option<usize>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub suggestion: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub context: Option<String>,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "lowercase")]
146pub enum IssueSeverity {
147 Note,
149 Warning,
151 Error,
153}
154
155impl From<oparry_core::IssueLevel> for IssueSeverity {
156 fn from(level: oparry_core::IssueLevel) -> Self {
157 match level {
158 oparry_core::IssueLevel::Note => IssueSeverity::Note,
159 oparry_core::IssueLevel::Warning => IssueSeverity::Warning,
160 oparry_core::IssueLevel::Error => IssueSeverity::Error,
161 }
162 }
163}
164
165impl From<oparry_core::Issue> for IssueDetail {
166 fn from(issue: oparry_core::Issue) -> Self {
167 Self {
168 code: issue.code,
169 level: issue.level.into(),
170 message: issue.message,
171 line: issue.line,
172 column: issue.column,
173 suggestion: issue.suggestion,
174 context: issue.context,
175 }
176 }
177}
178
179impl ClaudeRequest {
180 pub fn from_json(json: &str) -> crate::Result<Self> {
182 serde_json::from_str(json).map_err(|e| {
183 oparry_core::Error::Wrapper(format!("Failed to parse request JSON: {}", e))
184 })
185 }
186
187 pub fn to_json(&self) -> crate::Result<String> {
189 serde_json::to_string(self).map_err(|e| {
190 oparry_core::Error::Wrapper(format!("Failed to serialize request: {}", e))
191 })
192 }
193
194 pub fn id(&self) -> Option<&str> {
196 match self {
197 ClaudeRequest::WriteFile(w) => Some(&w.id),
198 ClaudeRequest::EditFile(e) => Some(&e.id),
199 ClaudeRequest::Ping => None,
200 }
201 }
202
203 pub fn path(&self) -> Option<&PathBuf> {
205 match self {
206 ClaudeRequest::WriteFile(w) => Some(&w.path),
207 ClaudeRequest::EditFile(e) => Some(&e.path),
208 ClaudeRequest::Ping => None,
209 }
210 }
211}
212
213impl ClaudeResponse {
214 pub fn to_json(&self) -> crate::Result<String> {
216 serde_json::to_string(self).map_err(|e| {
217 oparry_core::Error::Wrapper(format!("Failed to serialize response: {}", e))
218 })
219 }
220
221 pub fn approved(request_id: impl Into<String>) -> Self {
223 ClaudeResponse::Approved(ApprovedResponse {
224 request_id: request_id.into(),
225 modified_content: None,
226 })
227 }
228
229 pub fn approved_with_fix(request_id: impl Into<String>, content: impl Into<String>) -> Self {
231 ClaudeResponse::Approved(ApprovedResponse {
232 request_id: request_id.into(),
233 modified_content: Some(content.into()),
234 })
235 }
236
237 pub fn rejected(
239 request_id: impl Into<String>,
240 message: impl Into<String>,
241 issues: Vec<IssueDetail>,
242 ) -> Self {
243 ClaudeResponse::Rejected(RejectedResponse {
244 request_id: request_id.into(),
245 message: message.into(),
246 issues,
247 can_autofix: true, })
249 }
250
251 pub fn warning(
253 request_id: impl Into<String>,
254 message: impl Into<String>,
255 warnings: Vec<IssueDetail>,
256 ) -> Self {
257 ClaudeResponse::Warning(WarningResponse {
258 request_id: request_id.into(),
259 message: message.into(),
260 warnings,
261 modified_content: None,
262 })
263 }
264
265 pub fn protocol_error(message: impl Into<String>) -> Self {
267 ClaudeResponse::ProtocolError(ProtocolErrorResponse {
268 message: message.into(),
269 code: None,
270 })
271 }
272
273 pub fn is_allowed(&self) -> bool {
275 matches!(self, ClaudeResponse::Approved(_) | ClaudeResponse::Warning(_))
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_write_file_serialization() {
285 let request = ClaudeRequest::WriteFile(WriteFileRequest {
286 id: "test-123".to_string(),
287 path: PathBuf::from("test.ts"),
288 content: "console.log('hello');".to_string(),
289 encoding: Some("utf-8".to_string()),
290 create_dirs: Some(true),
291 });
292
293 let json = request.to_json().unwrap();
294 let parsed = ClaudeRequest::from_json(&json).unwrap();
295
296 assert_eq!(parsed.id(), Some("test-123"));
297 assert_eq!(parsed.path(), Some(&PathBuf::from("test.ts")));
298 }
299
300 #[test]
301 fn test_response_creation() {
302 let response = ClaudeResponse::approved("test-123");
303 assert!(response.is_allowed());
304
305 let response = ClaudeResponse::rejected(
306 "test-456",
307 "Validation failed",
308 vec![],
309 );
310 assert!(!response.is_allowed());
311 }
312
313 #[test]
314 fn test_issue_conversion() {
315 let core_issue = oparry_core::Issue::error("test-error", "Test error message")
316 .with_line(10)
317 .with_column(5)
318 .with_suggestion("Fix it");
319
320 let detail: IssueDetail = core_issue.into();
321 assert_eq!(detail.code, "test-error");
322 assert_eq!(detail.level, IssueSeverity::Error);
323 assert_eq!(detail.line, Some(10));
324 assert_eq!(detail.suggestion, Some("Fix it".to_string()));
325 }
326}