use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ParalockErrorCode {
InvalidRequest,
QuoteRejected,
QuoteExpired,
NotFound,
Conflict,
LimitExceeded,
Busy,
InsufficientLiquidity,
UnsafeTimeouts,
LockMismatch,
Unavailable,
Timeout,
Internal,
}
impl ParalockErrorCode {
pub const ALL: [Self; 13] = [
Self::InvalidRequest,
Self::QuoteRejected,
Self::QuoteExpired,
Self::NotFound,
Self::Conflict,
Self::LimitExceeded,
Self::Busy,
Self::InsufficientLiquidity,
Self::UnsafeTimeouts,
Self::LockMismatch,
Self::Unavailable,
Self::Timeout,
Self::Internal,
];
pub const fn status(self) -> u16 {
match self {
Self::InvalidRequest => 400,
Self::QuoteRejected | Self::LimitExceeded | Self::UnsafeTimeouts | Self::LockMismatch => 422,
Self::QuoteExpired => 410,
Self::NotFound => 404,
Self::Conflict => 409,
Self::Busy => 429,
Self::InsufficientLiquidity | Self::Unavailable => 503,
Self::Timeout => 504,
Self::Internal => 500,
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::InvalidRequest => "invalid_request",
Self::QuoteRejected => "quote_rejected",
Self::QuoteExpired => "quote_expired",
Self::NotFound => "not_found",
Self::Conflict => "conflict",
Self::LimitExceeded => "limit_exceeded",
Self::Busy => "busy",
Self::InsufficientLiquidity => "insufficient_liquidity",
Self::UnsafeTimeouts => "unsafe_timeouts",
Self::LockMismatch => "lock_mismatch",
Self::Unavailable => "unavailable",
Self::Timeout => "timeout",
Self::Internal => "internal",
}
}
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|c| c.as_str() == s)
}
}
impl fmt::Display for ParalockErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct ParalockError {
pub code: ParalockErrorCode,
pub message: String,
}
impl ParalockError {
pub fn new(code: ParalockErrorCode, message: impl Into<String>) -> Self {
Self { code, message: message.into() }
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(ParalockErrorCode::InvalidRequest, message)
}
pub const fn status(&self) -> u16 {
self.code.status()
}
pub fn to_body(&self) -> ErrorBody {
ErrorBody { error: self.message.clone(), code: self.code }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorBody {
pub error: String,
pub code: ParalockErrorCode,
}
impl ErrorBody {
pub fn from_response(status: u16, body: &str) -> ParalockError {
let value: serde_json::Value = serde_json::from_str(body).unwrap_or(serde_json::Value::Null);
let code = value
.get("code")
.and_then(serde_json::Value::as_str)
.and_then(ParalockErrorCode::parse)
.unwrap_or(ParalockErrorCode::Unavailable);
let message = match value.get("error").and_then(serde_json::Value::as_str) {
Some(text) => truncate_utf16(text, 300),
None => format!("HTTP {status}"),
};
ParalockError::new(code, format!("maker: {message}"))
}
}
pub(crate) fn truncate_utf16(s: &str, max: usize) -> String {
let mut units = 0;
let mut out = String::new();
for c in s.chars() {
units += c.len_utf16();
if units > max {
break;
}
out.push(c);
}
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
#[test]
fn codes_round_trip_and_map_to_statuses() {
for code in ParalockErrorCode::ALL {
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, format!("\"{}\"", code.as_str()));
assert_eq!(ParalockErrorCode::parse(code.as_str()), Some(code));
}
assert_eq!(ParalockErrorCode::Busy.status(), 429);
assert_eq!(ParalockErrorCode::InsufficientLiquidity.status(), 503);
}
#[test]
fn error_bodies_serialize_like_the_maker() {
let err = ParalockError::invalid_request("quoteId must be a quote id");
assert_eq!(
serde_json::to_string(&err.to_body()).unwrap(),
r#"{"error":"quoteId must be a quote id","code":"invalid_request"}"#
);
let parsed = ErrorBody::from_response(422, r#"{"error":"nope","code":"quote_rejected"}"#);
assert_eq!(parsed, ParalockError::new(ParalockErrorCode::QuoteRejected, "maker: nope"));
let unknown = ErrorBody::from_response(502, "<html>");
assert_eq!(unknown, ParalockError::new(ParalockErrorCode::Unavailable, "maker: HTTP 502"));
}
}