use serde::{Deserialize, Serialize};
use crate::bus::abi::truncate_utf8;
const MAX_QUERY_FAILURE_BYTES: usize = 64 * 1024;
const MAX_QUERY_MESSAGE_BYTES: usize = 60 * 1024;
#[derive(
phoxal_macros::DescribeWire, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum QueryCode {
NotFound,
InvalidArgument,
Internal,
Unavailable,
Unimplemented,
DeadlineExceeded,
}
#[derive(phoxal_macros::DescribeWire, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryFailure {
pub code: QueryCode,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details_encoding: Option<String>,
}
impl QueryFailure {
pub fn new(code: QueryCode, message: impl Into<String>) -> Self {
QueryFailure {
code,
message: message.into(),
details: None,
details_encoding: None,
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(QueryCode::NotFound, message)
}
pub fn invalid_argument(message: impl Into<String>) -> Self {
Self::new(QueryCode::InvalidArgument, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(QueryCode::Internal, message)
}
pub fn unavailable(message: impl Into<String>) -> Self {
Self::new(QueryCode::Unavailable, message)
}
pub fn unimplemented(message: impl Into<String>) -> Self {
Self::new(QueryCode::Unimplemented, message)
}
pub fn deadline_exceeded(message: impl Into<String>) -> Self {
Self::new(QueryCode::DeadlineExceeded, message)
}
pub fn encode(&self) -> std::result::Result<Vec<u8>, rmp_serde::encode::Error> {
let encoded = rmp_serde::to_vec_named(self)?;
if encoded.len() <= MAX_QUERY_FAILURE_BYTES {
return Ok(encoded);
}
let mut bounded = self.clone();
bounded.details = None;
bounded.details_encoding = None;
bounded.message = truncate_utf8(&bounded.message, MAX_QUERY_MESSAGE_BYTES);
let encoded = rmp_serde::to_vec_named(&bounded)?;
debug_assert!(encoded.len() <= MAX_QUERY_FAILURE_BYTES);
Ok(encoded)
}
pub fn decode(bytes: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
if bytes.len() > MAX_QUERY_FAILURE_BYTES {
return Err(rmp_serde::decode::Error::Syntax(format!(
"QueryFailure exceeds the {MAX_QUERY_FAILURE_BYTES}-byte limit"
)));
}
rmp_serde::from_slice(bytes)
}
}
#[derive(Debug, thiserror::Error)]
pub enum QueryError {
#[error("no responder is available for this query topic")]
Unavailable,
#[error("query timed out: {0:?}")]
Timeout(QueryFailure),
#[error("query server error: {0:?}")]
Server(QueryFailure),
#[error("failed to decode query response: {0}")]
Decode(String),
#[error("query protocol error: {0}")]
Protocol(String),
#[error("multiple responders answered an exclusive query topic")]
TooManyResponders,
}
pub type QueryResult<T> = std::result::Result<T, QueryFailure>;
#[cfg(test)]
mod tests {
use super::*;
fn encoded(failure: &QueryFailure) -> Vec<u8> {
failure.encode().expect("a test failure encodes")
}
#[test]
fn the_bootstrap_error_leg_is_pinned_to_its_literal_fields() {
let failure = QueryFailure::not_found("no such entity");
assert_eq!(
serde_json::to_value(&failure).expect("a failure serializes"),
serde_json::json!({"code": "not_found", "message": "no such entity"}),
"an absent detail is absent on the wire, not a null"
);
let mut detailed = QueryFailure::internal("with detail");
detailed.details = Some(vec![1]);
detailed.details_encoding = Some("application/phoxal-test".to_string());
assert_eq!(
serde_json::to_value(&detailed)
.expect("a detailed failure serializes")
.as_object()
.expect("a failure is a map")
.keys()
.cloned()
.collect::<Vec<_>>(),
["code", "details", "details_encoding", "message"]
);
for (code, spelling) in [
(QueryCode::NotFound, "not_found"),
(QueryCode::InvalidArgument, "invalid_argument"),
(QueryCode::Internal, "internal"),
(QueryCode::Unavailable, "unavailable"),
(QueryCode::Unimplemented, "unimplemented"),
(QueryCode::DeadlineExceeded, "deadline_exceeded"),
] {
assert_eq!(
serde_json::to_value(code).expect("a code serializes"),
serde_json::Value::String(spelling.to_owned())
);
}
}
#[test]
fn every_query_code_round_trips() {
let codes = [
QueryCode::NotFound,
QueryCode::InvalidArgument,
QueryCode::Internal,
QueryCode::Unavailable,
QueryCode::Unimplemented,
QueryCode::DeadlineExceeded,
];
for code in codes {
let failure = QueryFailure::new(code, format!("{code:?}"));
assert_eq!(QueryFailure::decode(&encoded(&failure)).unwrap(), failure);
}
}
#[test]
fn query_failure_details_round_trip() {
let mut failure = QueryFailure::internal("extra detail");
failure.details = Some(vec![1, 2, 3, 4]);
failure.details_encoding = Some("application/phoxal-test".to_string());
assert_eq!(QueryFailure::decode(&encoded(&failure)).unwrap(), failure);
}
#[test]
fn an_error_reply_stays_inside_its_wire_limit_in_both_directions() {
let mut failure = QueryFailure::internal("\u{e9}".repeat(100_000));
failure.details = Some(vec![7; 100_000]);
failure.details_encoding = Some("x".repeat(100_000));
let bytes = encoded(&failure);
assert!(bytes.len() <= MAX_QUERY_FAILURE_BYTES);
let decoded = QueryFailure::decode(&bytes).expect("bounded failure decodes");
assert_eq!(decoded.code, QueryCode::Internal);
assert!(decoded.details.is_none());
assert!(decoded.details_encoding.is_none());
let error = QueryFailure::decode(&vec![0_u8; MAX_QUERY_FAILURE_BYTES + 1]).unwrap_err();
assert!(error.to_string().contains("65536-byte limit"));
}
}