scratch_io/itch_api/
errors.rs

1use thiserror::Error;
2
3const ERROR_INVALID_API_KEY: &str = "invalid key";
4const ERROR_INVALID_USER_OR_PASSWORD: &[&str] = &[
5  "Incorrect username or password",
6  "username must be provided",
7  "password must be provided",
8];
9const ERROR_INVALID_CAPTCHA_CODE: &[&str] = &[
10  "Please correctly complete reCAPTCHA",
11  "Please complete reCAPTCHA to continue",
12];
13const ERROR_INVALID_TOTP_CODE: &str = "invalid code";
14const ERROR_TOTP_TOKEN_TIMED_OUT: &str = "two-factor login attempt timed out";
15const ERROR_INVALID_TOTP_TOKEN: &str = "invalid token";
16const ERROR_INVALID_USER: &[&str] = &["invalid user", "user_id: expected database ID integer"];
17const ERROR_INVALID_COLLECTION: &[&str] = &[
18  "invalid collection",
19  "collection_id: expected database id",
20  "collection_id: expected integer",
21];
22const ERROR_INVALID_GAME: &[&str] = &[
23  "invalid game",
24  "game_id: expected database id",
25  "game_id: expected integer",
26];
27const ERROR_INVALID_UPLOAD: &[&str] = &[
28  "invalid upload",
29  "upload_id: expected database id",
30  "upload_id: expected integer",
31];
32const ERROR_INVALID_BUILD: &[&str] = &[
33  "invalid build",
34  "build_id: expected database id",
35  "build_id: expected integer",
36];
37const ERROR_INVALID_TARGET_BUILD: &str =
38  "target_build_id: expected empty, or integer then database id";
39const ERROR_NO_UPGRADE_PATH: &str = "no upgrade path";
40
41/// Error returned from [`super::ItchClient::itch_request_json`]
42#[derive(Error, Debug)]
43#[error("An API call to \"{url}\" failed:\n{kind}")]
44pub struct ItchRequestJSONError<T>
45where
46  T: std::error::Error + std::fmt::Debug,
47{
48  pub url: String,
49  #[source]
50  pub kind: ItchRequestJSONErrorKind<T>,
51}
52
53#[derive(Error, Debug)]
54pub enum ItchRequestJSONErrorKind<T>
55where
56  T: std::error::Error + std::fmt::Debug,
57{
58  #[error(
59    "Error while sending request, redirect loop was detected or redirect limit was exhausted:\n{0}"
60  )]
61  CouldntSend(#[source] reqwest::Error),
62
63  #[error("Couldn't get the network request response body:\n{0}")]
64  CouldntGetText(#[source] reqwest::Error),
65
66  #[error("Couldn't parse the request response body into JSON:\n{body}\n\n{error}")]
67  InvalidJSON {
68    body: String,
69    #[source]
70    error: serde_json::Error,
71  },
72
73  #[error("The itch.io API server returned an error:\n{0}")]
74  ServerRepliedWithError(T),
75}
76
77#[derive(Error, Debug)]
78#[error("The provided API key is invalid!")]
79pub struct InvalidApiKey;
80
81#[derive(Error, Debug)]
82#[error("The username or the password is incorrect.")]
83pub struct IncorrectUsernameOrPassword;
84
85#[derive(Error, Debug)]
86#[error("The reCAPTCHA response code is incorrect!")]
87pub struct IncorrectCaptchaCode;
88
89#[derive(Error, Debug)]
90#[error("The TOTP code is incorrect!")]
91pub struct IncorrectTOTPCode;
92
93#[derive(Error, Debug)]
94#[error(
95  "The TOTP token timed out!
96Login again with a username and password to get another TOTP token."
97)]
98pub struct TOTPTokenTimedOut;
99
100#[derive(Error, Debug)]
101#[error("The TOTP token is invalid!")]
102pub struct InvalidTOTPToken;
103
104#[derive(Error, Debug)]
105#[error("The provided user ID is invalid.")]
106pub struct InvalidUserID;
107
108#[derive(Error, Debug)]
109#[error("The provided collection ID is invalid.")]
110pub struct InvalidCollectionID;
111
112#[derive(Error, Debug)]
113#[error("The provided game ID is invalid.")]
114pub struct InvalidGameID;
115
116#[derive(Error, Debug)]
117#[error("The provided upload ID is invalid.")]
118pub struct InvalidUploadID;
119
120#[derive(Error, Debug)]
121#[error("The provided build ID is invalid.")]
122pub struct InvalidBuildID;
123
124#[derive(Error, Debug)]
125#[error("The provided target build ID is invalid.")]
126pub struct InvalidTargetBuildID;
127
128#[derive(Error, Debug)]
129#[error("No upgrade path was found.")]
130pub struct NoUpgradePath;
131
132/// All possible errors returned from the Itch.io API
133#[derive(Error, Debug)]
134pub enum ApiResponseErrorKind {
135  #[error(transparent)]
136  InvalidApiKey(#[from] InvalidApiKey),
137
138  #[error(transparent)]
139  IncorrectUsernameOrPassword(#[from] IncorrectUsernameOrPassword),
140
141  #[error(transparent)]
142  IncorrectCaptchaCode(#[from] IncorrectCaptchaCode),
143
144  #[error(transparent)]
145  IncorrectTOTPCode(#[from] IncorrectTOTPCode),
146
147  #[error(transparent)]
148  TOTPTokenTimedOut(#[from] TOTPTokenTimedOut),
149
150  #[error(transparent)]
151  InvalidTOTPToken(#[from] InvalidTOTPToken),
152
153  #[error(transparent)]
154  InvalidUserID(#[from] InvalidUserID),
155
156  #[error(transparent)]
157  InvalidCollectionID(#[from] InvalidCollectionID),
158
159  #[error(transparent)]
160  InvalidGameID(#[from] InvalidGameID),
161
162  #[error(transparent)]
163  InvalidUploadID(#[from] InvalidUploadID),
164
165  #[error(transparent)]
166  InvalidBuildID(#[from] InvalidBuildID),
167
168  #[error(transparent)]
169  InvalidTargetBuildID(#[from] InvalidTargetBuildID),
170
171  #[error(transparent)]
172  NoUpgradePath(#[from] NoUpgradePath),
173
174  #[error("An unknown error occurred!")]
175  Other,
176}
177
178impl From<&[String]> for ApiResponseErrorKind {
179  fn from(value: &[String]) -> Self {
180    match value {
181      [v] if v == ERROR_INVALID_API_KEY => Self::InvalidApiKey(InvalidApiKey),
182      [v, ..] if ERROR_INVALID_USER_OR_PASSWORD.contains(&&**v) => {
183        Self::IncorrectUsernameOrPassword(IncorrectUsernameOrPassword)
184      }
185      [v] if ERROR_INVALID_CAPTCHA_CODE.contains(&&**v) => {
186        Self::IncorrectCaptchaCode(IncorrectCaptchaCode)
187      }
188      [v] if v == ERROR_INVALID_TOTP_CODE => Self::IncorrectTOTPCode(IncorrectTOTPCode),
189      [v] if v == ERROR_TOTP_TOKEN_TIMED_OUT => Self::TOTPTokenTimedOut(TOTPTokenTimedOut),
190      [v] if v == ERROR_INVALID_TOTP_TOKEN => Self::InvalidTOTPToken(InvalidTOTPToken),
191      [v] if ERROR_INVALID_USER.contains(&&**v) => Self::InvalidUserID(InvalidUserID),
192      [v] if ERROR_INVALID_COLLECTION.contains(&&**v) => {
193        Self::InvalidCollectionID(InvalidCollectionID)
194      }
195      [v] if ERROR_INVALID_GAME.contains(&&**v) => Self::InvalidGameID(InvalidGameID),
196      [v] if ERROR_INVALID_UPLOAD.contains(&&**v) => Self::InvalidUploadID(InvalidUploadID),
197      [v] if ERROR_INVALID_BUILD.contains(&&**v) => Self::InvalidBuildID(InvalidBuildID),
198      [v] if ERROR_INVALID_TARGET_BUILD == v => Self::InvalidTargetBuildID(InvalidTargetBuildID),
199      [v] if v == ERROR_NO_UPGRADE_PATH => Self::NoUpgradePath(NoUpgradePath),
200      _ => Self::Other,
201    }
202  }
203}
204
205#[derive(Error, Debug)]
206#[error("{kind}\n{errors:#?}")]
207pub struct ApiResponseError {
208  pub errors: Vec<String>,
209  #[source]
210  pub kind: ApiResponseErrorKind,
211}
212
213impl From<Vec<String>> for ApiResponseError {
214  fn from(value: Vec<String>) -> Self {
215    Self {
216      kind: value.as_slice().into(),
217      errors: value,
218    }
219  }
220}
221
222/// Common errors to every API call
223#[derive(Error, Debug)]
224pub enum ApiResponseCommonErrors {
225  #[error(transparent)]
226  InvalidApiKey(#[from] InvalidApiKey),
227
228  #[error("An unknown error occurred:\n{0:#?}")]
229  Other(Vec<String>),
230}
231
232impl From<ApiResponseError> for ApiResponseCommonErrors {
233  fn from(value: ApiResponseError) -> Self {
234    match value.kind {
235      ApiResponseErrorKind::InvalidApiKey(v) => v.into(),
236      _ => Self::Other(value.errors),
237    }
238  }
239}
240
241/// Errors returned from the login API call
242#[derive(Error, Debug)]
243pub enum LoginResponseError {
244  #[error(transparent)]
245  IncorrectUsernameOrPassword(#[from] IncorrectUsernameOrPassword),
246
247  #[error(transparent)]
248  IncorrectCaptchaCode(#[from] IncorrectCaptchaCode),
249
250  #[error(transparent)]
251  Other(#[from] ApiResponseCommonErrors),
252}
253
254impl From<ApiResponseError> for LoginResponseError {
255  fn from(value: ApiResponseError) -> Self {
256    match value.kind {
257      ApiResponseErrorKind::IncorrectUsernameOrPassword(v) => v.into(),
258      ApiResponseErrorKind::IncorrectCaptchaCode(v) => v.into(),
259      _ => Self::Other(value.into()),
260    }
261  }
262}
263
264/// Errors returned from the TOTP verification API call
265#[derive(Error, Debug)]
266pub enum TOTPResponseError {
267  #[error(transparent)]
268  IncorrectTOTPCode(#[from] IncorrectTOTPCode),
269
270  #[error(transparent)]
271  TOTPTokenTimedOut(#[from] TOTPTokenTimedOut),
272
273  #[error(transparent)]
274  InvalidTOTPToken(#[from] InvalidTOTPToken),
275
276  #[error(transparent)]
277  Other(#[from] ApiResponseCommonErrors),
278}
279
280impl From<ApiResponseError> for TOTPResponseError {
281  fn from(value: ApiResponseError) -> Self {
282    match value.kind {
283      ApiResponseErrorKind::IncorrectTOTPCode(v) => v.into(),
284      ApiResponseErrorKind::TOTPTokenTimedOut(v) => v.into(),
285      ApiResponseErrorKind::InvalidTOTPToken(v) => v.into(),
286      _ => Self::Other(value.into()),
287    }
288  }
289}
290
291/// Errors returned from all the API calls that require a user ID as a parameter
292#[derive(Error, Debug)]
293pub enum UserResponseError {
294  #[error(transparent)]
295  InvalidUserID(#[from] InvalidUserID),
296
297  #[error(transparent)]
298  Other(#[from] ApiResponseCommonErrors),
299}
300
301impl From<ApiResponseError> for UserResponseError {
302  fn from(value: ApiResponseError) -> Self {
303    match value.kind {
304      ApiResponseErrorKind::InvalidUserID(v) => v.into(),
305      _ => Self::Other(value.into()),
306    }
307  }
308}
309
310/// Errors returned from all the API calls that require a collection ID as a parameter
311#[derive(Error, Debug)]
312pub enum CollectionResponseError {
313  #[error(transparent)]
314  InvalidCollectionID(#[from] InvalidCollectionID),
315
316  #[error(transparent)]
317  Other(#[from] ApiResponseCommonErrors),
318}
319
320impl From<ApiResponseError> for CollectionResponseError {
321  fn from(value: ApiResponseError) -> Self {
322    match value.kind {
323      ApiResponseErrorKind::InvalidCollectionID(v) => v.into(),
324      _ => Self::Other(value.into()),
325    }
326  }
327}
328
329/// Errors returned from all the API calls that require a game ID as a parameter
330#[derive(Error, Debug)]
331pub enum GameResponseError {
332  #[error(transparent)]
333  InvalidGameID(#[from] InvalidGameID),
334
335  #[error(transparent)]
336  Other(#[from] ApiResponseCommonErrors),
337}
338
339impl From<ApiResponseError> for GameResponseError {
340  fn from(value: ApiResponseError) -> Self {
341    match value.kind {
342      ApiResponseErrorKind::InvalidGameID(v) => v.into(),
343      _ => Self::Other(value.into()),
344    }
345  }
346}
347
348/// Errors returned from all the API calls that require an upload ID as a parameter
349#[derive(Error, Debug)]
350pub enum UploadResponseError {
351  #[error(transparent)]
352  InvalidUploadID(#[from] InvalidUploadID),
353
354  #[error(transparent)]
355  Other(#[from] ApiResponseCommonErrors),
356}
357
358impl From<ApiResponseError> for UploadResponseError {
359  fn from(value: ApiResponseError) -> Self {
360    match value.kind {
361      ApiResponseErrorKind::InvalidUploadID(v) => v.into(),
362      ApiResponseErrorKind::InvalidGameID(_) => Self::InvalidUploadID(InvalidUploadID),
363      _ => Self::Other(value.into()),
364    }
365  }
366}
367
368/// Errors returned from all the API calls that require a build ID as a parameter
369#[derive(Error, Debug)]
370pub enum BuildResponseError {
371  #[error(transparent)]
372  InvalidBuildID(#[from] InvalidBuildID),
373
374  #[error(transparent)]
375  Other(#[from] ApiResponseCommonErrors),
376}
377
378impl From<ApiResponseError> for BuildResponseError {
379  fn from(value: ApiResponseError) -> Self {
380    match value.kind {
381      ApiResponseErrorKind::InvalidBuildID(v) => v.into(),
382      ApiResponseErrorKind::InvalidUploadID(_) | ApiResponseErrorKind::InvalidGameID(_) => {
383        Self::InvalidBuildID(InvalidBuildID)
384      }
385      _ => Self::Other(value.into()),
386    }
387  }
388}
389
390/// Errors returned from the upgrade path API call
391#[derive(Error, Debug)]
392pub enum UpgradePathResponseError {
393  #[error(transparent)]
394  NoUpgradePath(#[from] NoUpgradePath),
395
396  #[error(transparent)]
397  InvalidBuildID(#[from] InvalidBuildID),
398
399  #[error(transparent)]
400  InvalidTargetBuildID(#[from] InvalidTargetBuildID),
401
402  #[error(transparent)]
403  Other(#[from] ApiResponseCommonErrors),
404}
405
406impl From<ApiResponseError> for UpgradePathResponseError {
407  fn from(value: ApiResponseError) -> Self {
408    match value.kind {
409      ApiResponseErrorKind::NoUpgradePath(v) => v.into(),
410      ApiResponseErrorKind::InvalidBuildID(v) => v.into(),
411      ApiResponseErrorKind::InvalidUploadID(_) | ApiResponseErrorKind::InvalidGameID(_) => {
412        Self::InvalidBuildID(InvalidBuildID)
413      }
414      ApiResponseErrorKind::InvalidTargetBuildID(v) => v.into(),
415      _ => Self::Other(value.into()),
416    }
417  }
418}