facebook_graph_api_object_error/
lib.rs1use serde::{Deserialize, Serialize};
5use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str};
6use serde_json::{Map, Value};
7
8const CODE_STATUS_CODE_AND_BODY: i32 = -2_147_483_001;
10
11#[derive(Deserialize, Serialize, Debug, Clone)]
15pub struct Error {
16 pub message: String,
17 pub r#type: Option<ErrorType>,
18 pub code: i32,
19 pub error_subcode: Option<i32>,
20 pub error_user_title: Option<String>,
21 pub error_user_msg: Option<String>,
22 pub fbtrace_id: Option<String>,
23 #[serde(flatten, skip_serializing_if = "Option::is_none")]
28 _extra: Option<Map<String, Value>>,
29}
30
31impl Error {
32 pub fn extra(&self) -> Option<&Map<String, Value>> {
33 self._extra.as_ref()
34 }
35
36 pub fn new_with_status_code_and_body(status_code: u16, body: &str) -> Self {
37 let mut extra = Map::new();
38 extra.insert("status_code".to_string(), Value::from(status_code));
39 extra.insert("body".to_string(), Value::from(body));
40
41 Self {
42 message: format!("status_code:{status_code} body:{body}"),
43 r#type: None,
44 code: CODE_STATUS_CODE_AND_BODY,
45 error_subcode: None,
46 error_user_title: None,
47 error_user_msg: None,
48 fbtrace_id: None,
49 _extra: Some(extra),
50 }
51 }
52
53 pub fn as_status_code_and_body(&self) -> Option<(u16, &str)> {
54 if self.code != CODE_STATUS_CODE_AND_BODY {
55 return None;
56 }
57
58 if let Some(extra) = self.extra() {
59 if let Some(status_code) = extra.get("status_code").and_then(|x| x.as_i64()) {
60 if let Some(body) = extra.get("body").and_then(|x| x.as_str()) {
61 return Some((status_code as u16, body));
62 }
63 }
64 }
65
66 None
67 }
68}
69
70impl Error {
71 pub fn is_error_validating_access_token(&self) -> bool {
72 self.message
73 .to_lowercase()
74 .contains("error validating access token")
75 }
76
77 pub fn is_access_token_session_has_been_invalidated(&self) -> bool {
78 self.message
79 .to_lowercase()
80 .contains("session has been invalidated")
81 }
82
83 pub fn is_access_token_session_has_expired(&self) -> bool {
84 self.message.to_lowercase().contains("session has expired")
85 }
86
87 pub fn is_access_token_session_key_is_malformed(&self) -> bool {
88 self.message
89 .to_lowercase()
90 .contains("session key is malformed")
91 || (self.message.to_lowercase().contains("session key ")
92 && self.message.to_lowercase().contains(" is malformed"))
93 }
94}
95
96#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, Clone)]
97pub enum ErrorType {
98 OAuthException,
99 GraphMethodException,
100 #[serde(other)]
101 Other(String),
102}
103
104impl core::fmt::Display for Error {
105 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106 write!(f, "{self:?}")
107 }
108}
109
110impl std::error::Error for Error {}
111
112#[derive(Debug, Clone, Copy)]
116#[non_exhaustive]
117pub enum KnownErrorCase {
118 ApiTooManyCalls,
119 ApiUserTooManyCalls,
120 AccessTokenExpiredOrRevokedOrInvalid,
121 PermissionNotGrantedOrRemoved,
122 RetryLater,
123}
124
125impl KnownErrorCase {
126 pub fn is_api_too_many_calls(&self) -> bool {
127 matches!(self, Self::ApiTooManyCalls)
128 }
129
130 pub fn is_api_user_too_many_calls(&self) -> bool {
131 matches!(self, Self::ApiUserTooManyCalls)
132 }
133
134 pub fn is_access_token_expired_or_revoked_or_invalid(&self) -> bool {
135 matches!(self, Self::AccessTokenExpiredOrRevokedOrInvalid)
136 }
137
138 pub fn is_permission_not_granted_or_removed(&self) -> bool {
139 matches!(self, Self::PermissionNotGrantedOrRemoved)
140 }
141
142 pub fn is_retry_later(&self) -> bool {
143 matches!(self, Self::RetryLater)
144 }
145}
146
147impl core::fmt::Display for KnownErrorCase {
148 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149 write!(f, "{self:?}")
150 }
151}
152
153impl std::error::Error for KnownErrorCase {}
154
155impl Error {
156 pub fn to_known_error_case(&self) -> Option<KnownErrorCase> {
157 if let Some(error_subcode) = self.error_subcode {
158 #[allow(clippy::single_match)]
159 match error_subcode {
160 463 | 467 => return Some(KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid),
161 _ => {}
162 }
163 }
164
165 match self.code {
166 102 => {
167 if self.error_subcode.is_none() {
168 return Some(KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid);
169 }
170 }
171 2 => return Some(KnownErrorCase::RetryLater),
172 4 => return Some(KnownErrorCase::ApiTooManyCalls),
173 17 => return Some(KnownErrorCase::ApiUserTooManyCalls),
174 10 => return Some(KnownErrorCase::PermissionNotGrantedOrRemoved),
175 190 => return Some(KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid),
176 200..=299 => return Some(KnownErrorCase::PermissionNotGrantedOrRemoved),
177 _ => {}
178 }
179
180 None
181 }
182}
183
184impl From<&Error> for Option<KnownErrorCase> {
185 fn from(error: &Error) -> Self {
186 error.to_known_error_case()
187 }
188}
189
190impl From<Error> for Option<KnownErrorCase> {
191 fn from(error: Error) -> Self {
192 error.to_known_error_case()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[derive(Deserialize, Debug)]
201 struct ResponseBodyErrJson {
202 error: Error,
203 }
204
205 #[test]
206 fn test_de_error() {
207 let content = include_str!(
209 "../tests/response_body_json_files/err__access_token_session_has_been_invalidated.json"
210 );
211 match serde_json::from_str::<ResponseBodyErrJson>(content) {
212 Ok(err_json) => {
213 assert!(matches!(
215 err_json.error.to_known_error_case().unwrap(),
216 KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
217 ));
218 assert!(err_json.error.is_error_validating_access_token());
219 assert!(err_json
220 .error
221 .is_access_token_session_has_been_invalidated());
222 }
223 Err(err) => panic!("{}", err),
224 }
225
226 let content = include_str!(
228 "../tests/response_body_json_files/err__access_token_session_has_expired.json"
229 );
230 match serde_json::from_str::<ResponseBodyErrJson>(content) {
231 Ok(err_json) => {
232 assert!(matches!(
234 err_json.error.to_known_error_case().unwrap(),
235 KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
236 ));
237 assert!(err_json.error.is_error_validating_access_token());
238 assert!(err_json.error.is_access_token_session_has_expired());
239 }
240 Err(err) => panic!("{}", err),
241 }
242
243 let content = include_str!(
245 "../tests/response_body_json_files/err__access_token_session_key_is_malformed.json"
246 );
247 match serde_json::from_str::<ResponseBodyErrJson>(content) {
248 Ok(err_json) => {
249 assert!(matches!(
251 err_json.error.to_known_error_case().unwrap(),
252 KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
253 ));
254 assert!(err_json.error.is_error_validating_access_token());
255 assert!(err_json.error.is_access_token_session_key_is_malformed());
256 }
257 Err(err) => panic!("{}", err),
258 }
259
260 let content = include_str!(
264 "../tests/response_body_json_files/err__access_token_session_key_x_is_malformed.json"
265 );
266 match serde_json::from_str::<ResponseBodyErrJson>(content) {
267 Ok(err_json) => {
268 assert!(matches!(
270 err_json.error.to_known_error_case().unwrap(),
271 KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
272 ));
273 assert!(err_json.error.is_error_validating_access_token());
274 assert!(err_json.error.is_access_token_session_key_is_malformed());
275 }
276 Err(err) => panic!("{}", err),
277 }
278
279 let content =
281 include_str!("../tests/response_body_json_files/err__access_token_unknown_1.json");
282 match serde_json::from_str::<ResponseBodyErrJson>(content) {
283 Ok(err_json) => {
284 assert!(matches!(
286 err_json.error.to_known_error_case().unwrap(),
287 KnownErrorCase::AccessTokenExpiredOrRevokedOrInvalid
288 ));
289 assert!(!err_json.error.is_error_validating_access_token());
290 }
291 Err(err) => panic!("{}", err),
292 }
293 }
294}