Skip to main content

sdp_request_client/
error.rs

1use thiserror::Error;
2
3/// SDP API error codes
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub(crate) 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("Required field missing: {0}")]
92    FieldRequired(String),
93    #[error("{0}")]
94    Other(String),
95    #[error("io error: {0}")]
96    Io(#[from] std::io::Error),
97}
98
99impl Error {
100    /// Create an error from SDP response status code and message
101    pub fn from_sdp(code: u32, message: String, field: Option<String>) -> Self {
102        let field_info = field.clone().unwrap_or_else(|| message.clone());
103
104        match SdpErrorCode::from(code) {
105            SdpErrorCode::InvalidValue => Error::InvalidValue(field_info),
106            SdpErrorCode::Forbidden => Error::Forbidden(field_info),
107            SdpErrorCode::ClosureRuleViolation => Error::ClosureRuleViolation(field_info),
108            SdpErrorCode::Internal => Error::Internal,
109            SdpErrorCode::ReferenceExists => Error::ReferenceExists,
110            SdpErrorCode::NotFound => Error::NotFound(field_info),
111            SdpErrorCode::NotUnique => Error::NotUnique(field_info),
112            SdpErrorCode::NonEditableField | SdpErrorCode::ReadOnlyField => {
113                Error::NotEditable(field_info)
114            }
115            SdpErrorCode::InternalField => {
116                Error::NotEditable(format!("internal field: {}", field_info))
117            }
118            SdpErrorCode::NoSuchField => Error::NoSuchField(field_info),
119            SdpErrorCode::MissingMandatoryField => Error::MissingField(field_info),
120            SdpErrorCode::RateLimitExceeded => Error::RateLimited,
121            SdpErrorCode::LicenseRestriction => Error::LicenseRestricted,
122            SdpErrorCode::AlreadyInTrash | SdpErrorCode::NotInTrash => Error::Sdp { code, message },
123            SdpErrorCode::UnsupportedContentType => Error::Sdp { code, message },
124            SdpErrorCode::Success => Error::Other("Unexpected success code in error path".into()),
125            SdpErrorCode::Unknown => Error::Sdp { code, message },
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn sdp_error_code_from_u32() {
136        assert_eq!(SdpErrorCode::from(2000), SdpErrorCode::Success);
137        assert_eq!(SdpErrorCode::from(4001), SdpErrorCode::InvalidValue);
138        assert_eq!(SdpErrorCode::from(4002), SdpErrorCode::Forbidden);
139        assert_eq!(SdpErrorCode::from(4003), SdpErrorCode::ClosureRuleViolation);
140        assert_eq!(SdpErrorCode::from(4004), SdpErrorCode::Internal);
141        assert_eq!(SdpErrorCode::from(4005), SdpErrorCode::ReferenceExists);
142        assert_eq!(SdpErrorCode::from(4007), SdpErrorCode::NotFound);
143        assert_eq!(SdpErrorCode::from(4008), SdpErrorCode::NotUnique);
144        assert_eq!(SdpErrorCode::from(4009), SdpErrorCode::NonEditableField);
145        assert_eq!(SdpErrorCode::from(4010), SdpErrorCode::InternalField);
146        assert_eq!(SdpErrorCode::from(4011), SdpErrorCode::NoSuchField);
147        assert_eq!(
148            SdpErrorCode::from(4012),
149            SdpErrorCode::MissingMandatoryField
150        );
151        assert_eq!(
152            SdpErrorCode::from(4013),
153            SdpErrorCode::UnsupportedContentType
154        );
155        assert_eq!(SdpErrorCode::from(4014), SdpErrorCode::ReadOnlyField);
156        assert_eq!(SdpErrorCode::from(4015), SdpErrorCode::RateLimitExceeded);
157        assert_eq!(SdpErrorCode::from(4016), SdpErrorCode::AlreadyInTrash);
158        assert_eq!(SdpErrorCode::from(4017), SdpErrorCode::NotInTrash);
159        assert_eq!(SdpErrorCode::from(7001), SdpErrorCode::LicenseRestriction);
160        assert_eq!(SdpErrorCode::from(9999), SdpErrorCode::Unknown);
161        assert_eq!(SdpErrorCode::from(0), SdpErrorCode::Unknown);
162    }
163
164    #[test]
165    fn error_from_sdp_maps_correctly() {
166        assert!(matches!(
167            Error::from_sdp(4001, "msg".into(), None),
168            Error::InvalidValue(_)
169        ));
170        assert!(matches!(
171            Error::from_sdp(4002, "msg".into(), None),
172            Error::Forbidden(_)
173        ));
174        assert!(matches!(
175            Error::from_sdp(4004, "msg".into(), None),
176            Error::Internal
177        ));
178        assert!(matches!(
179            Error::from_sdp(4005, "msg".into(), None),
180            Error::ReferenceExists
181        ));
182        assert!(matches!(
183            Error::from_sdp(4007, "msg".into(), None),
184            Error::NotFound(_)
185        ));
186        assert!(matches!(
187            Error::from_sdp(4009, "msg".into(), None),
188            Error::NotEditable(_)
189        ));
190        assert!(matches!(
191            Error::from_sdp(4014, "msg".into(), None),
192            Error::NotEditable(_)
193        ));
194        assert!(matches!(
195            Error::from_sdp(4012, "msg".into(), None),
196            Error::MissingField(_)
197        ));
198        assert!(matches!(
199            Error::from_sdp(4015, "msg".into(), None),
200            Error::RateLimited
201        ));
202        assert!(matches!(
203            Error::from_sdp(7001, "msg".into(), None),
204            Error::LicenseRestricted
205        ));
206        assert!(matches!(
207            Error::from_sdp(9999, "msg".into(), None),
208            Error::Sdp { .. }
209        ));
210    }
211
212    #[test]
213    fn error_from_sdp_uses_field_when_provided() {
214        let err = Error::from_sdp(4001, "message".into(), Some("field_name".into()));
215        match err {
216            Error::InvalidValue(s) => assert_eq!(s, "field_name"),
217            _ => panic!("expected InvalidValue"),
218        }
219    }
220
221    #[test]
222    fn error_from_sdp_uses_message_when_no_field() {
223        let err = Error::from_sdp(4001, "message".into(), None);
224        match err {
225            Error::InvalidValue(s) => assert_eq!(s, "message"),
226            _ => panic!("expected InvalidValue"),
227        }
228    }
229}