1use core::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ParalockErrorCode {
9 InvalidRequest,
11 QuoteRejected,
13 QuoteExpired,
15 NotFound,
17 Conflict,
19 LimitExceeded,
21 Busy,
23 InsufficientLiquidity,
25 UnsafeTimeouts,
27 LockMismatch,
29 Unavailable,
31 Timeout,
33 Internal,
35}
36
37impl ParalockErrorCode {
38 pub const ALL: [Self; 13] = [
40 Self::InvalidRequest,
41 Self::QuoteRejected,
42 Self::QuoteExpired,
43 Self::NotFound,
44 Self::Conflict,
45 Self::LimitExceeded,
46 Self::Busy,
47 Self::InsufficientLiquidity,
48 Self::UnsafeTimeouts,
49 Self::LockMismatch,
50 Self::Unavailable,
51 Self::Timeout,
52 Self::Internal,
53 ];
54
55 pub const fn status(self) -> u16 {
57 match self {
58 Self::InvalidRequest => 400,
59 Self::QuoteRejected | Self::LimitExceeded | Self::UnsafeTimeouts | Self::LockMismatch => 422,
60 Self::QuoteExpired => 410,
61 Self::NotFound => 404,
62 Self::Conflict => 409,
63 Self::Busy => 429,
64 Self::InsufficientLiquidity | Self::Unavailable => 503,
65 Self::Timeout => 504,
66 Self::Internal => 500,
67 }
68 }
69
70 pub const fn as_str(self) -> &'static str {
72 match self {
73 Self::InvalidRequest => "invalid_request",
74 Self::QuoteRejected => "quote_rejected",
75 Self::QuoteExpired => "quote_expired",
76 Self::NotFound => "not_found",
77 Self::Conflict => "conflict",
78 Self::LimitExceeded => "limit_exceeded",
79 Self::Busy => "busy",
80 Self::InsufficientLiquidity => "insufficient_liquidity",
81 Self::UnsafeTimeouts => "unsafe_timeouts",
82 Self::LockMismatch => "lock_mismatch",
83 Self::Unavailable => "unavailable",
84 Self::Timeout => "timeout",
85 Self::Internal => "internal",
86 }
87 }
88
89 pub fn parse(s: &str) -> Option<Self> {
91 Self::ALL.into_iter().find(|c| c.as_str() == s)
92 }
93}
94
95impl fmt::Display for ParalockErrorCode {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str(self.as_str())
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
104#[error("{message}")]
105pub struct ParalockError {
106 pub code: ParalockErrorCode,
108 pub message: String,
110}
111
112impl ParalockError {
113 pub fn new(code: ParalockErrorCode, message: impl Into<String>) -> Self {
115 Self { code, message: message.into() }
116 }
117
118 pub fn invalid_request(message: impl Into<String>) -> Self {
120 Self::new(ParalockErrorCode::InvalidRequest, message)
121 }
122
123 pub const fn status(&self) -> u16 {
125 self.code.status()
126 }
127
128 pub fn to_body(&self) -> ErrorBody {
130 ErrorBody { error: self.message.clone(), code: self.code }
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct ErrorBody {
137 pub error: String,
139 pub code: ParalockErrorCode,
141}
142
143impl ErrorBody {
144 pub fn from_response(status: u16, body: &str) -> ParalockError {
147 let value: serde_json::Value = serde_json::from_str(body).unwrap_or(serde_json::Value::Null);
148 let code = value
149 .get("code")
150 .and_then(serde_json::Value::as_str)
151 .and_then(ParalockErrorCode::parse)
152 .unwrap_or(ParalockErrorCode::Unavailable);
153 let message = match value.get("error").and_then(serde_json::Value::as_str) {
154 Some(text) => truncate_utf16(text, 300),
155 None => format!("HTTP {status}"),
156 };
157 ParalockError::new(code, format!("maker: {message}"))
158 }
159}
160
161pub(crate) fn truncate_utf16(s: &str, max: usize) -> String {
163 let mut units = 0;
164 let mut out = String::new();
165 for c in s.chars() {
166 units += c.len_utf16();
167 if units > max {
168 break;
169 }
170 out.push(c);
171 }
172 out
173}
174
175#[cfg(test)]
176mod tests {
177 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
178 use super::*;
179
180 #[test]
181 fn codes_round_trip_and_map_to_statuses() {
182 for code in ParalockErrorCode::ALL {
183 let json = serde_json::to_string(&code).unwrap();
184 assert_eq!(json, format!("\"{}\"", code.as_str()));
185 assert_eq!(ParalockErrorCode::parse(code.as_str()), Some(code));
186 }
187 assert_eq!(ParalockErrorCode::Busy.status(), 429);
188 assert_eq!(ParalockErrorCode::InsufficientLiquidity.status(), 503);
189 }
190
191 #[test]
192 fn error_bodies_serialize_like_the_maker() {
193 let err = ParalockError::invalid_request("quoteId must be a quote id");
194 assert_eq!(
195 serde_json::to_string(&err.to_body()).unwrap(),
196 r#"{"error":"quoteId must be a quote id","code":"invalid_request"}"#
197 );
198 let parsed = ErrorBody::from_response(422, r#"{"error":"nope","code":"quote_rejected"}"#);
199 assert_eq!(parsed, ParalockError::new(ParalockErrorCode::QuoteRejected, "maker: nope"));
200 let unknown = ErrorBody::from_response(502, "<html>");
201 assert_eq!(unknown, ParalockError::new(ParalockErrorCode::Unavailable, "maker: HTTP 502"));
202 }
203}