use crate::event::Event;
use crate::ops::{AdminOp, ClientOp, GatewayOp};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
pub const MAX_ERROR_MESSAGE_BYTES: usize = 4096;
pub const OP_ID_CONFLICT_MESSAGE: &str = "op_id conflict: request differs from durable receipt";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub struct ResBody {
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorPayload>,
}
impl ResBody {
pub fn ok(data: serde_json::Value) -> Self {
ResBody {
ok: true,
data: Some(data),
error: None,
}
}
pub fn err(code: ErrorCode, message: impl Into<String>, field: Option<String>) -> Self {
ResBody {
ok: false,
data: None,
error: Some(ErrorPayload {
code,
message: message.into(),
field,
}),
}
}
pub fn err_with_data(
code: ErrorCode,
message: impl Into<String>,
field: Option<String>,
data: serde_json::Value,
) -> Self {
ResBody {
ok: false,
data: Some(data),
error: Some(ErrorPayload {
code,
message: message.into(),
field,
}),
}
}
pub fn data(&self) -> Option<&serde_json::Value> {
self.data.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ErrorPayload {
pub code: ErrorCode,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
}
impl ErrorPayload {
pub fn trimmed(mut self) -> Self {
if self.message.len() > MAX_ERROR_MESSAGE_BYTES {
let mut cut = MAX_ERROR_MESSAGE_BYTES;
while cut > 0 && !self.message.is_char_boundary(cut) {
cut -= 1;
}
self.message.truncate(cut);
}
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
Invalid,
UnknownOp,
AclDenied,
UnknownRole,
RecipientOffline,
Duplicate,
Conflict,
Unauthorized,
Forbidden,
NotAdmin,
FrameTooLarge,
BadFrame,
ProtocolVersion,
Internal,
}
impl ErrorCode {
pub fn as_str(self) -> &'static str {
match self {
ErrorCode::Invalid => "invalid",
ErrorCode::UnknownOp => "unknown_op",
ErrorCode::AclDenied => "acl_denied",
ErrorCode::UnknownRole => "unknown_role",
ErrorCode::RecipientOffline => "recipient_offline",
ErrorCode::Duplicate => "duplicate",
ErrorCode::Conflict => "conflict",
ErrorCode::Unauthorized => "unauthorized",
ErrorCode::Forbidden => "forbidden",
ErrorCode::NotAdmin => "not_admin",
ErrorCode::FrameTooLarge => "frame_too_large",
ErrorCode::BadFrame => "bad_frame",
ErrorCode::ProtocolVersion => "protocol_version",
ErrorCode::Internal => "internal",
}
}
pub const ALL: [ErrorCode; 14] = [
ErrorCode::Invalid,
ErrorCode::UnknownOp,
ErrorCode::AclDenied,
ErrorCode::UnknownRole,
ErrorCode::RecipientOffline,
ErrorCode::Duplicate,
ErrorCode::Conflict,
ErrorCode::Unauthorized,
ErrorCode::Forbidden,
ErrorCode::NotAdmin,
ErrorCode::FrameTooLarge,
ErrorCode::BadFrame,
ErrorCode::ProtocolVersion,
ErrorCode::Internal,
];
pub fn retry(self) -> Retry {
match self {
ErrorCode::RecipientOffline | ErrorCode::Internal => Retry::UnderBackoff,
ErrorCode::Duplicate => Retry::Never,
ErrorCode::Unauthorized | ErrorCode::ProtocolVersion => Retry::AfterHuman,
_ => Retry::Never,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Retry {
Never,
AfterHuman,
UnderBackoff,
}
impl Retry {
pub fn is_transient(self) -> bool {
matches!(self, Retry::UnderBackoff)
}
pub fn ends_the_run(self) -> bool {
!self.is_transient()
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl std::error::Error for ErrorCode {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", tag = "f")]
pub enum Frame<R = ClientOp> {
Req {
id: String,
#[serde(flatten)]
op: R,
},
Res {
id: String,
#[serde(flatten)]
body: ResBody,
},
Ev {
seq: u64,
#[serde(flatten)]
event: Box<Event>,
},
Ack { seq: u64 },
Ping { t: i64 },
Pong { t: i64, server_seq: u64 },
Bye { reason: String },
}
pub type AdminFrame = Frame<AdminOp>;
pub type GatewayFrame = Frame<GatewayOp>;
impl<R> Frame<R> {
pub fn req(id: impl Into<String>, op: R) -> Self {
Frame::Req { id: id.into(), op }
}
pub fn id(&self) -> Option<&str> {
match self {
Frame::Req { id, .. } | Frame::Res { id, .. } => Some(id),
Frame::Ev { .. }
| Frame::Ack { .. }
| Frame::Ping { .. }
| Frame::Pong { .. }
| Frame::Bye { .. } => None,
}
}
pub fn is_response(&self) -> bool {
matches!(self, Frame::Res { .. })
}
}
impl Frame {
pub fn res(id: impl Into<String>, body: ResBody) -> Self {
Frame::Res {
id: id.into(),
body,
}
}
pub fn ok(id: impl Into<String>, data: serde_json::Value) -> Self {
Frame::res(id, ResBody::ok(data))
}
pub fn error(
id: impl Into<String>,
code: ErrorCode,
message: impl Into<String>,
field: Option<String>,
) -> Self {
Frame::res(id, ResBody::err(code, message, field))
}
pub fn event(seq: u64, event: Event) -> Self {
Frame::Ev {
seq,
event: Box::new(event),
}
}
pub fn bye(reason: impl Into<String>) -> Self {
Frame::Bye {
reason: reason.into(),
}
}
}