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
159    // Database commands
160    DbStats,
161
162    // Export command
163    Export {
164        format: String,
165        since: Option<String>,
166    },
167
168    // Rebuild command
169    Rebuild,
170
171    // Sync command
172    Sync {
173        remote: String,
174        pull: bool,
175        push: bool,
176    },
177
178    // Snapshot commands
179    SnapshotCreate,
180    SnapshotList,
181    SnapshotGc {
182        keep: u32,
183    },
184
185    // Daemon commands
186    DaemonStatus,
187    DaemonStop,
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_request_creation() {
196        let req = IpcRequest::new(
197            "test-123".to_string(),
198            "/path/to/repo".to_string(),
199            "abcd1234".to_string(),
200            ".git/grite/actors/abcd1234".to_string(),
201            IpcCommand::IssueList {
202                state: Some("open".to_string()),
203                label: None,
204            },
205        );
206
207        assert_eq!(req.ipc_schema_version, IPC_SCHEMA_VERSION);
208        assert_eq!(req.request_id, "test-123");
209    }
210
211    #[test]
212    fn test_response_success() {
213        let resp = IpcResponse::success(
214            "test-123".to_string(),
215            Some(r#"{"issues": []}"#.to_string()),
216        );
217
218        assert!(resp.ok);
219        assert!(resp.error.is_none());
220        assert!(resp.data.is_some());
221    }
222
223    #[test]
224    fn test_response_error() {
225        let resp = IpcResponse::error(
226            "test-123".to_string(),
227            "not_found".to_string(),
228            "Issue not found".to_string(),
229        );
230
231        assert!(!resp.ok);
232        assert!(resp.data.is_none());
233        assert!(resp.error.is_some());
234        assert_eq!(resp.error.as_ref().unwrap().code, "not_found");
235    }
236
237    #[test]
238    fn test_rkyv_roundtrip() {
239        let req = IpcRequest::new(
240            "test-456".to_string(),
241            "/repo".to_string(),
242            "actor123".to_string(),
243            ".git/grite/actors/actor123".to_string(),
244            IpcCommand::IssueCreate {
245                title: "Test Issue".to_string(),
246                body: "Description".to_string(),
247                labels: vec!["bug".to_string()],
248            },
249        );
250
251        // Serialize
252        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&req).unwrap();
253
254        // Deserialize
255        let archived = rkyv::access::<ArchivedIpcRequest, rkyv::rancor::Error>(&bytes).unwrap();
256        assert_eq!(archived.request_id, "test-456");
257        assert_eq!(archived.ipc_schema_version, IPC_SCHEMA_VERSION);
258    }
259}