use serde::{Deserialize, Serialize};
use super::{HelloAck, Request, Response};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Envelope {
pub id: u64,
pub deadline_ms: Option<u64>,
pub body: Request,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reply {
pub id: u64,
pub result: Result<Response, RpcError>,
}
pub type HelloReply = Result<HelloAck, RpcError>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RpcError {
pub code: RpcErrorCode,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daemon_version: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RpcErrorCode {
NotFound,
InvalidConfig,
SpawnFailed,
ProtocolMismatch,
Internal,
DeadlineExceeded,
Unsupported,
#[serde(other)]
Unrecognized,
}
impl RpcErrorCode {
pub const ALL: [Self; 7] = [
Self::NotFound,
Self::InvalidConfig,
Self::SpawnFailed,
Self::ProtocolMismatch,
Self::Internal,
Self::DeadlineExceeded,
Self::Unsupported,
];
#[must_use]
pub const fn exit_code(self) -> u8 {
use crate::exit;
match self {
Self::NotFound => exit::NOT_FOUND,
Self::InvalidConfig => exit::INVALID_CONFIG,
Self::SpawnFailed => exit::SPAWN_FAILED,
Self::ProtocolMismatch => exit::PROTOCOL_MISMATCH,
Self::Internal | Self::Unrecognized => exit::INTERNAL,
Self::DeadlineExceeded => exit::DEADLINE_EXCEEDED,
Self::Unsupported => exit::UNSUPPORTED,
}
}
#[allow(dead_code)]
const fn assert_all_lists_every_variant(code: Self) -> Self {
match code {
Self::NotFound => Self::ALL[0],
Self::InvalidConfig => Self::ALL[1],
Self::SpawnFailed => Self::ALL[2],
Self::ProtocolMismatch => Self::ALL[3],
Self::Internal => Self::ALL[4],
Self::DeadlineExceeded => Self::ALL[5],
Self::Unsupported => Self::ALL[6],
Self::Unrecognized => Self::ALL[4],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unknown_error_code_decodes_as_unrecognized() {
assert_eq!(
serde_json::from_str::<RpcErrorCode>(r#""invented_next_year""#).unwrap(),
RpcErrorCode::Unrecognized
);
}
#[test]
fn every_known_error_code_still_round_trips() {
for code in [
RpcErrorCode::NotFound,
RpcErrorCode::InvalidConfig,
RpcErrorCode::SpawnFailed,
RpcErrorCode::ProtocolMismatch,
RpcErrorCode::Internal,
RpcErrorCode::DeadlineExceeded,
RpcErrorCode::Unsupported,
] {
let json = serde_json::to_string(&code).unwrap();
assert_eq!(serde_json::from_str::<RpcErrorCode>(&json).unwrap(), code);
}
}
#[test]
fn a_non_string_error_code_is_still_an_error() {
assert!(serde_json::from_str::<RpcErrorCode>("42").is_err());
}
#[test]
fn an_unknown_request_kind_keeps_the_envelope_id() {
let envelope: Envelope = serde_json::from_str(
r#"{"id":42,"deadline_ms":null,"body":{"kind":"from_the_future","extra":{"a":1}}}"#,
)
.unwrap();
assert_eq!(envelope.id, 42);
assert_eq!(envelope.body, Request::Unrecognized);
}
#[test]
fn v1_reply_fixture_still_deserializes() {
let ok = r#"{"id":1,"result":{"Ok":{"kind":"pong"}}}"#;
let reply: Reply = serde_json::from_str(ok).unwrap();
assert!(matches!(reply.result, Ok(Response::Pong)));
let err = r#"{"id":2,"result":{"Err":{"code":"not_found","message":"no sheep"}}}"#;
let reply: Reply = serde_json::from_str(err).unwrap();
assert_eq!(reply.result.unwrap_err().code, RpcErrorCode::NotFound);
}
#[test]
fn an_rpc_error_without_a_daemon_version_serializes_exactly_as_before() {
let plain = RpcError {
code: RpcErrorCode::NotFound,
message: "no sheep".to_string(),
daemon_version: None,
};
assert_eq!(
serde_json::to_string(&plain).unwrap(),
r#"{"code":"not_found","message":"no sheep"}"#
);
}
#[test]
fn a_v1_rpc_error_fixture_deserializes_with_no_daemon_version() {
let fixture =
r#"{"code":"protocol_mismatch","message":"daemon speaks protocol 1, client sent 2"}"#;
let err: RpcError = serde_json::from_str(fixture).unwrap();
assert_eq!(err.code, RpcErrorCode::ProtocolMismatch);
assert_eq!(err.daemon_version, None);
}
#[test]
fn an_old_client_ignores_an_rpc_error_field_it_has_never_seen() {
#[derive(Deserialize)]
struct OldRpcError {
code: RpcErrorCode,
message: String,
}
let current = serde_json::to_string(&RpcError {
code: RpcErrorCode::ProtocolMismatch,
message: "daemon speaks protocol 1, client sent 2".to_string(),
daemon_version: Some("0.1.16".to_string()),
})
.unwrap();
let old: OldRpcError = serde_json::from_str(¤t).expect("must tolerate");
assert_eq!(old.code, RpcErrorCode::ProtocolMismatch);
assert_eq!(old.message, "daemon speaks protocol 1, client sent 2");
}
#[test]
fn deadline_exceeded_code_serializes_snake_case() {
assert_eq!(
serde_json::to_string(&RpcErrorCode::DeadlineExceeded).unwrap(),
"\"deadline_exceeded\""
);
assert_eq!(
serde_json::from_str::<RpcErrorCode>("\"deadline_exceeded\"").unwrap(),
RpcErrorCode::DeadlineExceeded
);
}
}