shep_core/protocol/request/
envelope.rs1use serde::{Deserialize, Serialize};
4
5use super::{HelloAck, Request, Response};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct Envelope {
11 pub id: u64,
13 pub deadline_ms: Option<u64>,
15 pub body: Request,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Reply {
26 pub id: u64,
28 pub result: Result<Response, RpcError>,
30}
31
32pub type HelloReply = Result<HelloAck, RpcError>;
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct RpcError {
41 pub code: RpcErrorCode,
43 pub message: String,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub daemon_version: Option<String>,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63#[non_exhaustive]
64pub enum RpcErrorCode {
65 NotFound,
67 InvalidConfig,
69 SpawnFailed,
71 ProtocolMismatch,
73 Internal,
75 DeadlineExceeded,
77 Unsupported,
83 #[serde(other)]
91 Unrecognized,
92}
93
94impl RpcErrorCode {
95 pub const ALL: [Self; 7] = [
102 Self::NotFound,
103 Self::InvalidConfig,
104 Self::SpawnFailed,
105 Self::ProtocolMismatch,
106 Self::Internal,
107 Self::DeadlineExceeded,
108 Self::Unsupported,
109 ];
110
111 #[must_use]
114 pub const fn exit_code(self) -> u8 {
115 use crate::exit;
116 match self {
117 Self::NotFound => exit::NOT_FOUND,
118 Self::InvalidConfig => exit::INVALID_CONFIG,
119 Self::SpawnFailed => exit::SPAWN_FAILED,
120 Self::ProtocolMismatch => exit::PROTOCOL_MISMATCH,
121 Self::Internal | Self::Unrecognized => exit::INTERNAL,
122 Self::DeadlineExceeded => exit::DEADLINE_EXCEEDED,
123 Self::Unsupported => exit::UNSUPPORTED,
124 }
125 }
126
127 #[allow(dead_code)]
134 const fn assert_all_lists_every_variant(code: Self) -> Self {
135 match code {
136 Self::NotFound => Self::ALL[0],
137 Self::InvalidConfig => Self::ALL[1],
138 Self::SpawnFailed => Self::ALL[2],
139 Self::ProtocolMismatch => Self::ALL[3],
140 Self::Internal => Self::ALL[4],
141 Self::DeadlineExceeded => Self::ALL[5],
142 Self::Unsupported => Self::ALL[6],
143 Self::Unrecognized => Self::ALL[4],
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
157 fn an_unknown_error_code_decodes_as_unrecognized() {
158 assert_eq!(
159 serde_json::from_str::<RpcErrorCode>(r#""invented_next_year""#).unwrap(),
160 RpcErrorCode::Unrecognized
161 );
162 }
163
164 #[test]
165 fn every_known_error_code_still_round_trips() {
166 for code in [
167 RpcErrorCode::NotFound,
168 RpcErrorCode::InvalidConfig,
169 RpcErrorCode::SpawnFailed,
170 RpcErrorCode::ProtocolMismatch,
171 RpcErrorCode::Internal,
172 RpcErrorCode::DeadlineExceeded,
173 RpcErrorCode::Unsupported,
174 ] {
175 let json = serde_json::to_string(&code).unwrap();
176 assert_eq!(serde_json::from_str::<RpcErrorCode>(&json).unwrap(), code);
177 }
178 }
179
180 #[test]
183 fn a_non_string_error_code_is_still_an_error() {
184 assert!(serde_json::from_str::<RpcErrorCode>("42").is_err());
185 }
186
187 #[test]
190 fn an_unknown_request_kind_keeps_the_envelope_id() {
191 let envelope: Envelope = serde_json::from_str(
192 r#"{"id":42,"deadline_ms":null,"body":{"kind":"from_the_future","extra":{"a":1}}}"#,
193 )
194 .unwrap();
195 assert_eq!(envelope.id, 42);
196 assert_eq!(envelope.body, Request::Unrecognized);
197 }
198
199 #[test]
200 fn v1_reply_fixture_still_deserializes() {
201 let ok = r#"{"id":1,"result":{"Ok":{"kind":"pong"}}}"#;
203 let reply: Reply = serde_json::from_str(ok).unwrap();
204 assert!(matches!(reply.result, Ok(Response::Pong)));
205 let err = r#"{"id":2,"result":{"Err":{"code":"not_found","message":"no sheep"}}}"#;
206 let reply: Reply = serde_json::from_str(err).unwrap();
207 assert_eq!(reply.result.unwrap_err().code, RpcErrorCode::NotFound);
208 }
209
210 #[test]
211 fn an_rpc_error_without_a_daemon_version_serializes_exactly_as_before() {
212 let plain = RpcError {
215 code: RpcErrorCode::NotFound,
216 message: "no sheep".to_string(),
217 daemon_version: None,
218 };
219 assert_eq!(
220 serde_json::to_string(&plain).unwrap(),
221 r#"{"code":"not_found","message":"no sheep"}"#
222 );
223 }
224
225 #[test]
226 fn a_v1_rpc_error_fixture_deserializes_with_no_daemon_version() {
227 let fixture =
228 r#"{"code":"protocol_mismatch","message":"daemon speaks protocol 1, client sent 2"}"#;
229 let err: RpcError = serde_json::from_str(fixture).unwrap();
230 assert_eq!(err.code, RpcErrorCode::ProtocolMismatch);
231 assert_eq!(err.daemon_version, None);
232 }
233
234 #[test]
235 fn an_old_client_ignores_an_rpc_error_field_it_has_never_seen() {
236 #[derive(Deserialize)]
239 struct OldRpcError {
240 code: RpcErrorCode,
241 message: String,
242 }
243
244 let current = serde_json::to_string(&RpcError {
245 code: RpcErrorCode::ProtocolMismatch,
246 message: "daemon speaks protocol 1, client sent 2".to_string(),
247 daemon_version: Some("0.1.16".to_string()),
248 })
249 .unwrap();
250 let old: OldRpcError = serde_json::from_str(¤t).expect("must tolerate");
251 assert_eq!(old.code, RpcErrorCode::ProtocolMismatch);
252 assert_eq!(old.message, "daemon speaks protocol 1, client sent 2");
253 }
254
255 #[test]
256 fn deadline_exceeded_code_serializes_snake_case() {
257 assert_eq!(
258 serde_json::to_string(&RpcErrorCode::DeadlineExceeded).unwrap(),
259 "\"deadline_exceeded\""
260 );
261 assert_eq!(
262 serde_json::from_str::<RpcErrorCode>("\"deadline_exceeded\"").unwrap(),
263 RpcErrorCode::DeadlineExceeded
264 );
265 }
266}