Skip to main content

libgrite_ipc/
messages.rs

1//! IPC request and response message types
2//!
3//! These types define the wire format for daemon communication.
4//! Wire format is rkyv-serialized, but JSON is used for lock files.
5
6use rkyv::{Archive, Deserialize, Serialize};
7use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
8
9use crate::IPC_SCHEMA_VERSION;
10
11/// IPC request envelope
12#[derive(Archive, Serialize, Deserialize, Debug, Clone)]
13#[rkyv(derive(Debug))]
14pub struct IpcRequest {
15    /// Schema version for compatibility checking
16    pub ipc_schema_version: u32,
17    /// Unique request ID for correlation
18    pub request_id: String,
19    /// Repository root path
20    pub repo_root: String,
21    /// Actor ID (hex-encoded 16 bytes)
22    pub actor_id: String,
23    /// Data directory path
24    pub data_dir: String,
25    /// The command to execute
26    pub command: IpcCommand,
27}
28
29impl IpcRequest {
30    /// Create a new request with the current schema version
31    pub fn new(
32        request_id: String,
33        repo_root: String,
34        actor_id: String,
35        data_dir: String,
36        command: IpcCommand,
37    ) -> Self {
38        Self {
39            ipc_schema_version: IPC_SCHEMA_VERSION,
40            request_id,
41            repo_root,
42            actor_id,
43            data_dir,
44            command,
45        }
46    }
47}
48
49/// IPC response envelope
50#[derive(Archive, Serialize, Deserialize, Debug, Clone)]
51#[rkyv(derive(Debug))]
52pub struct IpcResponse {
53    /// Schema version (must match request)
54    pub ipc_schema_version: u32,
55    /// Request ID for correlation
56    pub request_id: String,
57    /// Whether the request succeeded
58    pub ok: bool,
59    /// Response data (JSON-encoded for flexibility)
60    pub data: Option<String>,
61    /// Error details if ok=false
62    pub error: Option<IpcErrorPayload>,
63}
64
65impl IpcResponse {
66    /// Create a successful response
67    pub fn success(request_id: String, data: Option<String>) -> Self {
68        Self {
69            ipc_schema_version: IPC_SCHEMA_VERSION,
70            request_id,
71            ok: true,
72            data,
73            error: None,
74        }
75    }
76
77    /// Create an error response
78    pub fn error(request_id: String, code: String, message: String) -> Self {
79        Self {
80            ipc_schema_version: IPC_SCHEMA_VERSION,
81            request_id,
82            ok: false,
83            data: None,
84            error: Some(IpcErrorPayload {
85                code,
86                message,
87                details: None,
88            }),
89        }
90    }
91}
92
93/// Error payload in responses
94#[derive(Archive, Serialize, Deserialize, Debug, Clone, SerdeSerialize, SerdeDeserialize)]
95#[rkyv(derive(Debug))]
96pub struct IpcErrorPayload {
97    /// Error code (matches docs/cli-json.md)
98    pub code: String,
99    /// Human-readable error message
100    pub message: String,
101    /// Optional additional details (JSON-encoded)
102    pub details: Option<String>,
103}
104
105/// Commands that can be sent to the daemon
106///
107/// These mirror the CLI commands. Payloads are equivalent to CLI flags.
108#[derive(Archive, Serialize, Deserialize, Debug, Clone)]
109#[rkyv(derive(Debug))]
110pub enum IpcCommand {
111    // Issue commands
112    IssueCreate {
113        title: String,
114        body: String,
115        labels: Vec<String>,
116    },
117    IssueList {
118        state: Option<String>,
119        label: Option<String>,
120    },
121    IssueShow {
122        issue_id: String,
123    },
124    IssueUpdate {
125        issue_id: String,
126        title: Option<String>,
127        body: Option<String>,
128    },
129    IssueComment {
130        issue_id: String,
131        body: String,
132    },
133    IssueLabel {
134        issue_id: String,
135        add: Vec<String>,
136        remove: Vec<String>,
137    },
138    IssueAssign {
139        issue_id: String,
140        add: Vec<String>,
141        remove: Vec<String>,
142    },
143    IssueClose {
144        issue_id: String,
145    },
146    IssueReopen {
147        issue_id: String,
148    },
149    IssueLink {
150        issue_id: String,
151        url: String,
152        note: Option<String>,
153    },
154    IssueAttach {
155        issue_id: String,
156        file_path: String,
157    },
158    IssueDepAdd {
159        issue_id: String,
160        target_id: String,
161        dep_type: String,
162    },
163    IssueDepRemove {
164        issue_id: String,
165        target_id: String,
166        dep_type: String,
167    },
168    IssueDepList {
169        issue_id: String,
170        reverse: bool,
171    },
172    IssueDepTopo {
173        state: Option<String>,
174        label: Option<String>,
175    },
176
177    // Database commands
178    DbStats,
179
180    // Export command
181    Export {
182        format: String,
183        since: Option<String>,
184    },
185
186    // Rebuild command
187    Rebuild,
188
189    // Sync command
190    Sync {
191        remote: String,
192        pull: bool,
193        push: bool,
194    },
195
196    // Snapshot commands
197    SnapshotCreate,
198    SnapshotList,
199    SnapshotGc {
200        keep: u32,
201    },
202
203    // Daemon commands
204    DaemonStatus,
205    DaemonStop,
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_request_creation() {
214        let req = IpcRequest::new(
215            "test-123".to_string(),
216            "/path/to/repo".to_string(),
217            "abcd1234".to_string(),
218            ".git/grite/actors/abcd1234".to_string(),
219            IpcCommand::IssueList {
220                state: Some("open".to_string()),
221                label: None,
222            },
223        );
224
225        assert_eq!(req.ipc_schema_version, IPC_SCHEMA_VERSION);
226        assert_eq!(req.request_id, "test-123");
227    }
228
229    #[test]
230    fn test_response_success() {
231        let resp = IpcResponse::success(
232            "test-123".to_string(),
233            Some(r#"{"issues": []}"#.to_string()),
234        );
235
236        assert!(resp.ok);
237        assert!(resp.error.is_none());
238        assert!(resp.data.is_some());
239    }
240
241    #[test]
242    fn test_response_error() {
243        let resp = IpcResponse::error(
244            "test-123".to_string(),
245            "not_found".to_string(),
246            "Issue not found".to_string(),
247        );
248
249        assert!(!resp.ok);
250        assert!(resp.data.is_none());
251        assert!(resp.error.is_some());
252        assert_eq!(resp.error.as_ref().unwrap().code, "not_found");
253    }
254
255    #[test]
256    fn test_rkyv_roundtrip() {
257        let req = IpcRequest::new(
258            "test-456".to_string(),
259            "/repo".to_string(),
260            "actor123".to_string(),
261            ".git/grite/actors/actor123".to_string(),
262            IpcCommand::IssueCreate {
263                title: "Test Issue".to_string(),
264                body: "Description".to_string(),
265                labels: vec!["bug".to_string()],
266            },
267        );
268
269        // Serialize
270        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&req).unwrap();
271
272        // Deserialize
273        let archived = rkyv::access::<ArchivedIpcRequest, rkyv::rancor::Error>(&bytes).unwrap();
274        assert_eq!(archived.request_id, "test-456");
275        assert_eq!(archived.ipc_schema_version, IPC_SCHEMA_VERSION);
276    }
277}