1use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ErrorKind {
14 InvalidInput,
16 AuthMissing,
18 AuthExpired,
20 HydraExpired,
23 CdpUnavailable,
25 UrlNotAllowed,
27 OrderingDisabled,
29 ConfirmationRequired,
31 QuoteChanged,
33 CartInvalid,
35 AddressMissing,
37 NotFound,
39 RateLimited,
41 OtpInvalid,
43 CaptchaRequired,
45 LoginExpired,
47 Api,
49 Network,
51 Io,
53 Parse,
55}
56
57impl ErrorKind {
58 pub fn as_str(self) -> &'static str {
59 match self {
60 Self::InvalidInput => "invalid_input",
61 Self::AuthMissing => "auth_missing",
62 Self::AuthExpired => "auth_expired",
63 Self::HydraExpired => "hydra_expired",
64 Self::CdpUnavailable => "cdp_unavailable",
65 Self::UrlNotAllowed => "url_not_allowed",
66 Self::OrderingDisabled => "ordering_disabled",
67 Self::ConfirmationRequired => "confirmation_required",
68 Self::QuoteChanged => "quote_changed",
69 Self::CartInvalid => "cart_invalid",
70 Self::AddressMissing => "address_missing",
71 Self::NotFound => "not_found",
72 Self::RateLimited => "rate_limited",
73 Self::OtpInvalid => "otp_invalid",
74 Self::CaptchaRequired => "captcha_required",
75 Self::LoginExpired => "login_expired",
76 Self::Api => "api",
77 Self::Network => "network",
78 Self::Io => "io",
79 Self::Parse => "parse",
80 }
81 }
82
83 pub fn exit_code(self) -> i32 {
85 match self {
86 Self::InvalidInput | Self::CartInvalid | Self::NotFound | Self::AddressMissing => 2,
87 Self::QuoteChanged => 3,
88 Self::AuthMissing
89 | Self::AuthExpired
90 | Self::HydraExpired
91 | Self::CdpUnavailable
92 | Self::OtpInvalid
93 | Self::CaptchaRequired
94 | Self::LoginExpired => 4,
95 Self::OrderingDisabled | Self::UrlNotAllowed => 5,
96 Self::ConfirmationRequired => 6,
97 Self::RateLimited => 7,
98 Self::Api | Self::Network | Self::Io | Self::Parse => 1,
99 }
100 }
101}
102
103#[derive(Debug, thiserror::Error)]
104#[error("{message}")]
105pub struct Error {
106 pub kind: ErrorKind,
107 pub message: String,
108 pub details: Option<serde_json::Value>,
111}
112
113pub type Result<T, E = Error> = std::result::Result<T, E>;
114
115impl Error {
116 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
117 Self {
118 kind,
119 message: message.into(),
120 details: None,
121 }
122 }
123
124 #[must_use]
125 pub fn with_details(mut self, details: serde_json::Value) -> Self {
126 self.details = Some(details);
127 self
128 }
129
130 pub fn invalid(message: impl Into<String>) -> Self {
131 Self::new(ErrorKind::InvalidInput, message)
132 }
133
134 pub fn parse(message: impl Into<String>) -> Self {
135 Self::new(ErrorKind::Parse, message)
136 }
137
138 pub fn not_found(message: impl Into<String>) -> Self {
139 Self::new(ErrorKind::NotFound, message)
140 }
141
142 pub fn exit_code(&self) -> i32 {
143 self.kind.exit_code()
144 }
145
146 pub fn to_json(&self) -> serde_json::Value {
148 let mut e = serde_json::json!({
149 "kind": self.kind.as_str(),
150 "message": self.message,
151 });
152 if let Some(d) = &self.details {
153 e["details"] = d.clone();
154 }
155 serde_json::json!({ "error": e })
156 }
157}
158
159impl From<std::io::Error> for Error {
160 fn from(e: std::io::Error) -> Self {
161 Self::new(ErrorKind::Io, e.to_string())
162 }
163}
164
165impl From<serde_json::Error> for Error {
166 fn from(e: serde_json::Error) -> Self {
167 Self::new(ErrorKind::Parse, format!("JSON: {e}"))
168 }
169}
170
171impl From<reqwest::Error> for Error {
172 fn from(e: reqwest::Error) -> Self {
173 Self::new(ErrorKind::Network, e.to_string())
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn json_shape() {
185 let e = Error::new(ErrorKind::HydraExpired, "hydra gone");
186 let j = e.to_json();
187 assert_eq!(j["error"]["kind"], "hydra_expired");
188 assert_eq!(j["error"]["message"], "hydra gone");
189 assert_eq!(e.exit_code(), 4);
190 }
191
192 #[test]
193 fn address_missing_is_not_an_auth_error() {
194 assert_eq!(ErrorKind::AddressMissing.as_str(), "address_missing");
195 assert_eq!(ErrorKind::AddressMissing.exit_code(), 2);
196 }
197
198 #[test]
199 fn quote_changed_exits_3() {
200 assert_eq!(ErrorKind::QuoteChanged.exit_code(), 3);
201 }
202}