1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4use crate::TicketID;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub(crate) enum SdpErrorCode {
9 Success = 2000,
10 InvalidValue = 4001,
11 Forbidden = 4002,
12 ClosureRuleViolation = 4003,
13 Internal = 4004,
14 ReferenceExists = 4005,
15 NotFound = 4007,
16 NotUnique = 4008,
17 NonEditableField = 4009,
18 InternalField = 4010,
19 NoSuchField = 4011,
20 MissingMandatoryField = 4012,
21 UnsupportedContentType = 4013,
22 ReadOnlyField = 4014,
23 RateLimitExceeded = 4015,
24 AlreadyInTrash = 4016,
25 NotInTrash = 4017,
26 LicenseRestriction = 7001,
27 Unknown = 0,
28}
29
30impl From<u32> for SdpErrorCode {
31 fn from(code: u32) -> Self {
32 match code {
33 2000 => SdpErrorCode::Success,
34 4001 => SdpErrorCode::InvalidValue,
35 4002 => SdpErrorCode::Forbidden,
36 4003 => SdpErrorCode::ClosureRuleViolation,
37 4004 => SdpErrorCode::Internal,
38 4005 => SdpErrorCode::ReferenceExists,
39 4007 => SdpErrorCode::NotFound,
40 4008 => SdpErrorCode::NotUnique,
41 4009 => SdpErrorCode::NonEditableField,
42 4010 => SdpErrorCode::InternalField,
43 4011 => SdpErrorCode::NoSuchField,
44 4012 => SdpErrorCode::MissingMandatoryField,
45 4013 => SdpErrorCode::UnsupportedContentType,
46 4014 => SdpErrorCode::ReadOnlyField,
47 4015 => SdpErrorCode::RateLimitExceeded,
48 4016 => SdpErrorCode::AlreadyInTrash,
49 4017 => SdpErrorCode::NotInTrash,
50 7001 => SdpErrorCode::LicenseRestriction,
51 _ => SdpErrorCode::Unknown,
52 }
53 }
54}
55
56#[derive(Debug, Error)]
57pub enum Error {
58 #[error("HTTP error: {0}")]
59 Http(#[from] reqwest::Error),
60 #[error("Authentication failed: invalid or expired token")]
61 Unauthorized,
62 #[error("Permission denied: {0}")]
63 Forbidden(String),
64 #[error("Resource not found: {0}")]
65 NotFound(String),
66 #[error("request has been merged into parent {parent_request_id}: {message}")]
67 RequestMerged {
68 parent_request_id: TicketID,
69 message: String,
70 },
71 #[error("Invalid value: {0}")]
72 InvalidValue(String),
73 #[error("Resource already exists (not unique): {0}")]
74 NotUnique(String),
75 #[error("Cannot delete: resource is referenced elsewhere")]
76 ReferenceExists,
77 #[error("Missing mandatory field: {0}")]
78 MissingField(String),
79 #[error("Field is not editable: {0}")]
80 NotEditable(String),
81 #[error("Field does not exist: {0}")]
82 NoSuchField(String),
83 #[error("Closure rule violation: {0}")]
84 ClosureRuleViolation(String),
85 #[error("Rate limit exceeded")]
86 RateLimited,
87 #[error("License restriction: operation not allowed")]
88 LicenseRestricted,
89 #[error("SDP internal error")]
90 Internal,
91 #[error("Serialization error: {0}")]
92 Serialization(#[from] serde_json::Error),
93 #[error("URL parsing error: {0}")]
94 UrlParse(#[from] url::ParseError),
95 #[error("Form encoding error: {0}")]
96 FormEncoding(#[from] serde_urlencoded::ser::Error),
97 #[error("SDP error (code {code}): {message}")]
98 Sdp { code: u32, message: String },
99 #[error("Required field missing: {0}")]
100 FieldRequired(String),
101 #[error("{0}")]
102 Other(String),
103 #[error("io error: {0}")]
104 Io(#[from] std::io::Error),
105}
106
107#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
108#[serde(untagged)]
109pub(crate) enum SdpErrorMessage {
110 Merged {
111 parent_request: ParentRequest,
112 message: String,
113 },
114 Text(String),
115 Other(serde_json::Value),
116}
117
118impl SdpErrorMessage {
119 fn into_text(self) -> String {
120 match self {
121 Self::Merged { message, .. } | Self::Text(message) => message,
122 Self::Other(value) => value.to_string(),
123 }
124 }
125}
126
127#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
128pub(crate) struct ParentRequest {
129 id: TicketID,
130}
131
132impl Error {
133 pub(crate) fn from_sdp_message(code: u32, response: SdpErrorMessage) -> Self {
134 if let SdpErrorMessage::Merged {
135 parent_request,
136 message,
137 } = response
138 {
139 return Self::RequestMerged {
140 parent_request_id: parent_request.id,
141 message,
142 };
143 }
144
145 Self::from_sdp(code, response.into_text(), None)
146 }
147
148 #[must_use]
150 pub fn from_sdp(code: u32, message: String, field: Option<String>) -> Self {
151 let field_info = field.clone().unwrap_or_else(|| message.clone());
152
153 match SdpErrorCode::from(code) {
154 SdpErrorCode::InvalidValue => Error::InvalidValue(field_info),
155 SdpErrorCode::Forbidden => Error::Forbidden(field_info),
156 SdpErrorCode::ClosureRuleViolation => Error::ClosureRuleViolation(field_info),
157 SdpErrorCode::Internal => Error::Internal,
158 SdpErrorCode::ReferenceExists => Error::ReferenceExists,
159 SdpErrorCode::NotFound => Error::NotFound(field_info),
160 SdpErrorCode::NotUnique => Error::NotUnique(field_info),
161 SdpErrorCode::NonEditableField | SdpErrorCode::ReadOnlyField => {
162 Error::NotEditable(field_info)
163 }
164 SdpErrorCode::InternalField => {
165 Error::NotEditable(format!("internal field: {field_info}"))
166 }
167 SdpErrorCode::NoSuchField => Error::NoSuchField(field_info),
168 SdpErrorCode::MissingMandatoryField => Error::MissingField(field_info),
169 SdpErrorCode::RateLimitExceeded => Error::RateLimited,
170 SdpErrorCode::LicenseRestriction => Error::LicenseRestricted,
171 SdpErrorCode::AlreadyInTrash | SdpErrorCode::NotInTrash => Error::Sdp { code, message },
172 SdpErrorCode::UnsupportedContentType => Error::Sdp { code, message },
173 SdpErrorCode::Success => Error::Other("Unexpected success code in error path".into()),
174 SdpErrorCode::Unknown => Error::Sdp { code, message },
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn sdp_error_code_from_u32() {
185 assert_eq!(SdpErrorCode::from(2000), SdpErrorCode::Success);
186 assert_eq!(SdpErrorCode::from(4001), SdpErrorCode::InvalidValue);
187 assert_eq!(SdpErrorCode::from(4002), SdpErrorCode::Forbidden);
188 assert_eq!(SdpErrorCode::from(4003), SdpErrorCode::ClosureRuleViolation);
189 assert_eq!(SdpErrorCode::from(4004), SdpErrorCode::Internal);
190 assert_eq!(SdpErrorCode::from(4005), SdpErrorCode::ReferenceExists);
191 assert_eq!(SdpErrorCode::from(4007), SdpErrorCode::NotFound);
192 assert_eq!(SdpErrorCode::from(4008), SdpErrorCode::NotUnique);
193 assert_eq!(SdpErrorCode::from(4009), SdpErrorCode::NonEditableField);
194 assert_eq!(SdpErrorCode::from(4010), SdpErrorCode::InternalField);
195 assert_eq!(SdpErrorCode::from(4011), SdpErrorCode::NoSuchField);
196 assert_eq!(
197 SdpErrorCode::from(4012),
198 SdpErrorCode::MissingMandatoryField
199 );
200 assert_eq!(
201 SdpErrorCode::from(4013),
202 SdpErrorCode::UnsupportedContentType
203 );
204 assert_eq!(SdpErrorCode::from(4014), SdpErrorCode::ReadOnlyField);
205 assert_eq!(SdpErrorCode::from(4015), SdpErrorCode::RateLimitExceeded);
206 assert_eq!(SdpErrorCode::from(4016), SdpErrorCode::AlreadyInTrash);
207 assert_eq!(SdpErrorCode::from(4017), SdpErrorCode::NotInTrash);
208 assert_eq!(SdpErrorCode::from(7001), SdpErrorCode::LicenseRestriction);
209 assert_eq!(SdpErrorCode::from(9999), SdpErrorCode::Unknown);
210 assert_eq!(SdpErrorCode::from(0), SdpErrorCode::Unknown);
211 }
212
213 #[test]
214 fn error_from_sdp_maps_correctly() {
215 assert!(matches!(
216 Error::from_sdp(4001, "msg".into(), None),
217 Error::InvalidValue(_)
218 ));
219 assert!(matches!(
220 Error::from_sdp(4002, "msg".into(), None),
221 Error::Forbidden(_)
222 ));
223 assert!(matches!(
224 Error::from_sdp(4004, "msg".into(), None),
225 Error::Internal
226 ));
227 assert!(matches!(
228 Error::from_sdp(4005, "msg".into(), None),
229 Error::ReferenceExists
230 ));
231 assert!(matches!(
232 Error::from_sdp(4007, "msg".into(), None),
233 Error::NotFound(_)
234 ));
235 assert!(matches!(
236 Error::from_sdp(4009, "msg".into(), None),
237 Error::NotEditable(_)
238 ));
239 assert!(matches!(
240 Error::from_sdp(4014, "msg".into(), None),
241 Error::NotEditable(_)
242 ));
243 assert!(matches!(
244 Error::from_sdp(4012, "msg".into(), None),
245 Error::MissingField(_)
246 ));
247 assert!(matches!(
248 Error::from_sdp(4015, "msg".into(), None),
249 Error::RateLimited
250 ));
251 assert!(matches!(
252 Error::from_sdp(7001, "msg".into(), None),
253 Error::LicenseRestricted
254 ));
255 assert!(matches!(
256 Error::from_sdp(9999, "msg".into(), None),
257 Error::Sdp { .. }
258 ));
259 }
260
261 #[test]
262 fn error_from_sdp_uses_field_when_provided() {
263 let err = Error::from_sdp(4001, "message".into(), Some("field_name".into()));
264 match err {
265 Error::InvalidValue(s) => assert_eq!(s, "field_name"),
266 _ => panic!("expected InvalidValue"),
267 }
268 }
269
270 #[test]
271 fn error_from_sdp_uses_message_when_no_field() {
272 let err = Error::from_sdp(4001, "message".into(), None);
273 match err {
274 Error::InvalidValue(s) => assert_eq!(s, "message"),
275 _ => panic!("expected InvalidValue"),
276 }
277 }
278
279 #[test]
280 fn structured_merged_response_maps_to_request_merged() {
281 let response: SdpErrorMessage = serde_json::from_value(serde_json::json!({
282 "parent_request": { "id": 583415 },
283 "message": "Request Id(s) 583550 has been Merged with Parent Request 583415."
284 }))
285 .unwrap();
286
287 let error = Error::from_sdp_message(4007, response);
288
289 assert!(matches!(
290 error,
291 Error::RequestMerged {
292 parent_request_id: TicketID(583415),
293 message
294 } if message.contains("583550")
295 ));
296 }
297}