1use thiserror::Error;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum SdpErrorCode {
6 Success = 2000,
7 InvalidValue = 4001,
8 Forbidden = 4002,
9 ClosureRuleViolation = 4003,
10 Internal = 4004,
11 ReferenceExists = 4005,
12 NotFound = 4007,
13 NotUnique = 4008,
14 NonEditableField = 4009,
15 InternalField = 4010,
16 NoSuchField = 4011,
17 MissingMandatoryField = 4012,
18 UnsupportedContentType = 4013,
19 ReadOnlyField = 4014,
20 RateLimitExceeded = 4015,
21 AlreadyInTrash = 4016,
22 NotInTrash = 4017,
23 LicenseRestriction = 7001,
24 Unknown = 0,
25}
26
27impl From<u32> for SdpErrorCode {
28 fn from(code: u32) -> Self {
29 match code {
30 2000 => SdpErrorCode::Success,
31 4001 => SdpErrorCode::InvalidValue,
32 4002 => SdpErrorCode::Forbidden,
33 4003 => SdpErrorCode::ClosureRuleViolation,
34 4004 => SdpErrorCode::Internal,
35 4005 => SdpErrorCode::ReferenceExists,
36 4007 => SdpErrorCode::NotFound,
37 4008 => SdpErrorCode::NotUnique,
38 4009 => SdpErrorCode::NonEditableField,
39 4010 => SdpErrorCode::InternalField,
40 4011 => SdpErrorCode::NoSuchField,
41 4012 => SdpErrorCode::MissingMandatoryField,
42 4013 => SdpErrorCode::UnsupportedContentType,
43 4014 => SdpErrorCode::ReadOnlyField,
44 4015 => SdpErrorCode::RateLimitExceeded,
45 4016 => SdpErrorCode::AlreadyInTrash,
46 4017 => SdpErrorCode::NotInTrash,
47 7001 => SdpErrorCode::LicenseRestriction,
48 _ => SdpErrorCode::Unknown,
49 }
50 }
51}
52
53#[derive(Debug, Error)]
54pub enum Error {
55 #[error("HTTP error: {0}")]
56 Http(#[from] reqwest::Error),
57 #[error("Authentication failed: invalid or expired token")]
58 Unauthorized,
59 #[error("Permission denied: {0}")]
60 Forbidden(String),
61 #[error("Resource not found: {0}")]
62 NotFound(String),
63 #[error("Invalid value: {0}")]
64 InvalidValue(String),
65 #[error("Resource already exists (not unique): {0}")]
66 NotUnique(String),
67 #[error("Cannot delete: resource is referenced elsewhere")]
68 ReferenceExists,
69 #[error("Missing mandatory field: {0}")]
70 MissingField(String),
71 #[error("Field is not editable: {0}")]
72 NotEditable(String),
73 #[error("Field does not exist: {0}")]
74 NoSuchField(String),
75 #[error("Closure rule violation: {0}")]
76 ClosureRuleViolation(String),
77 #[error("Rate limit exceeded")]
78 RateLimited,
79 #[error("License restriction: operation not allowed")]
80 LicenseRestricted,
81 #[error("SDP internal error")]
82 Internal,
83 #[error("Serialization error: {0}")]
84 Serialization(#[from] serde_json::Error),
85 #[error("URL parsing error: {0}")]
86 UrlParse(#[from] url::ParseError),
87 #[error("Form encoding error: {0}")]
88 FormEncoding(#[from] serde_urlencoded::ser::Error),
89 #[error("SDP error (code {code}): {message}")]
90 Sdp { code: u32, message: String },
91 #[error("{0}")]
92 Other(String),
93}
94
95impl Error {
96 pub fn from_sdp(code: u32, message: String, field: Option<String>) -> Self {
98 let field_info = field.clone().unwrap_or_else(|| message.clone());
99
100 match SdpErrorCode::from(code) {
101 SdpErrorCode::InvalidValue => Error::InvalidValue(field_info),
102 SdpErrorCode::Forbidden => Error::Forbidden(field_info),
103 SdpErrorCode::ClosureRuleViolation => Error::ClosureRuleViolation(field_info),
104 SdpErrorCode::Internal => Error::Internal,
105 SdpErrorCode::ReferenceExists => Error::ReferenceExists,
106 SdpErrorCode::NotFound => Error::NotFound(field_info),
107 SdpErrorCode::NotUnique => Error::NotUnique(field_info),
108 SdpErrorCode::NonEditableField | SdpErrorCode::ReadOnlyField => {
109 Error::NotEditable(field_info)
110 }
111 SdpErrorCode::InternalField => {
112 Error::NotEditable(format!("internal field: {}", field_info))
113 }
114 SdpErrorCode::NoSuchField => Error::NoSuchField(field_info),
115 SdpErrorCode::MissingMandatoryField => Error::MissingField(field_info),
116 SdpErrorCode::RateLimitExceeded => Error::RateLimited,
117 SdpErrorCode::LicenseRestriction => Error::LicenseRestricted,
118 SdpErrorCode::AlreadyInTrash | SdpErrorCode::NotInTrash => Error::Sdp { code, message },
119 SdpErrorCode::UnsupportedContentType => Error::Sdp { code, message },
120 SdpErrorCode::Success => Error::Other("Unexpected success code in error path".into()),
121 SdpErrorCode::Unknown => Error::Sdp { code, message },
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn sdp_error_code_from_u32() {
132 assert_eq!(SdpErrorCode::from(2000), SdpErrorCode::Success);
133 assert_eq!(SdpErrorCode::from(4001), SdpErrorCode::InvalidValue);
134 assert_eq!(SdpErrorCode::from(4002), SdpErrorCode::Forbidden);
135 assert_eq!(SdpErrorCode::from(4003), SdpErrorCode::ClosureRuleViolation);
136 assert_eq!(SdpErrorCode::from(4004), SdpErrorCode::Internal);
137 assert_eq!(SdpErrorCode::from(4005), SdpErrorCode::ReferenceExists);
138 assert_eq!(SdpErrorCode::from(4007), SdpErrorCode::NotFound);
139 assert_eq!(SdpErrorCode::from(4008), SdpErrorCode::NotUnique);
140 assert_eq!(SdpErrorCode::from(4009), SdpErrorCode::NonEditableField);
141 assert_eq!(SdpErrorCode::from(4010), SdpErrorCode::InternalField);
142 assert_eq!(SdpErrorCode::from(4011), SdpErrorCode::NoSuchField);
143 assert_eq!(
144 SdpErrorCode::from(4012),
145 SdpErrorCode::MissingMandatoryField
146 );
147 assert_eq!(
148 SdpErrorCode::from(4013),
149 SdpErrorCode::UnsupportedContentType
150 );
151 assert_eq!(SdpErrorCode::from(4014), SdpErrorCode::ReadOnlyField);
152 assert_eq!(SdpErrorCode::from(4015), SdpErrorCode::RateLimitExceeded);
153 assert_eq!(SdpErrorCode::from(4016), SdpErrorCode::AlreadyInTrash);
154 assert_eq!(SdpErrorCode::from(4017), SdpErrorCode::NotInTrash);
155 assert_eq!(SdpErrorCode::from(7001), SdpErrorCode::LicenseRestriction);
156 assert_eq!(SdpErrorCode::from(9999), SdpErrorCode::Unknown);
157 assert_eq!(SdpErrorCode::from(0), SdpErrorCode::Unknown);
158 }
159
160 #[test]
161 fn error_from_sdp_maps_correctly() {
162 assert!(matches!(
163 Error::from_sdp(4001, "msg".into(), None),
164 Error::InvalidValue(_)
165 ));
166 assert!(matches!(
167 Error::from_sdp(4002, "msg".into(), None),
168 Error::Forbidden(_)
169 ));
170 assert!(matches!(
171 Error::from_sdp(4004, "msg".into(), None),
172 Error::Internal
173 ));
174 assert!(matches!(
175 Error::from_sdp(4005, "msg".into(), None),
176 Error::ReferenceExists
177 ));
178 assert!(matches!(
179 Error::from_sdp(4007, "msg".into(), None),
180 Error::NotFound(_)
181 ));
182 assert!(matches!(
183 Error::from_sdp(4009, "msg".into(), None),
184 Error::NotEditable(_)
185 ));
186 assert!(matches!(
187 Error::from_sdp(4014, "msg".into(), None),
188 Error::NotEditable(_)
189 ));
190 assert!(matches!(
191 Error::from_sdp(4012, "msg".into(), None),
192 Error::MissingField(_)
193 ));
194 assert!(matches!(
195 Error::from_sdp(4015, "msg".into(), None),
196 Error::RateLimited
197 ));
198 assert!(matches!(
199 Error::from_sdp(7001, "msg".into(), None),
200 Error::LicenseRestricted
201 ));
202 assert!(matches!(
203 Error::from_sdp(9999, "msg".into(), None),
204 Error::Sdp { .. }
205 ));
206 }
207
208 #[test]
209 fn error_from_sdp_uses_field_when_provided() {
210 let err = Error::from_sdp(4001, "message".into(), Some("field_name".into()));
211 match err {
212 Error::InvalidValue(s) => assert_eq!(s, "field_name"),
213 _ => panic!("expected InvalidValue"),
214 }
215 }
216
217 #[test]
218 fn error_from_sdp_uses_message_when_no_field() {
219 let err = Error::from_sdp(4001, "message".into(), None);
220 match err {
221 Error::InvalidValue(s) => assert_eq!(s, "message"),
222 _ => panic!("expected InvalidValue"),
223 }
224 }
225}