Skip to main content

ytcli/api/
error.rs

1//! Typed API failures.
2//!
3//! The variants exist so the shell can map them to distinct exit codes and to
4//! actionable messages; a single opaque "request failed" would make both
5//! impossible.
6
7use crate::exit::ExitCode;
8
9#[derive(Debug, thiserror::Error)]
10pub enum ApiError {
11    #[error("transport error talking to Tracker")]
12    Transport(#[from] reqwest::Error),
13    #[error("not authenticated: the token was rejected (401)")]
14    Unauthorized,
15    #[error("forbidden (403): the account lacks rights, or the organisation header is wrong")]
16    Forbidden,
17    // The Wiki's refusal has a likelier cause than Tracker's: a token issued
18    // before the Wiki permission was added to the application. Saying so turns
19    // a rights puzzle into one command.
20    #[error(
21        "the Wiki refused this token: it needs the wiki:read permission — sign in again \
22         with `ytcli auth login` — or this account cannot see that page"
23    )]
24    WikiForbidden,
25    // The Wiki answers 403 with `FORCED_SYNC_REQUIRED` when it has never heard of
26    // the organisation: the Wiki was not opened there yet. No sign-in fixes
27    // that, so blaming the token would send people round in circles.
28    #[error(
29        "the Wiki is not set up in this organisation yet — open https://wiki.yandex.ru once, \
30         signed in to it, and try again"
31    )]
32    WikiNotEnabled,
33    // Reading and writing are separate permissions, and a token signed in for
34    // reading only is refused every write: naming the one it lacks is the fix.
35    #[error(
36        "the Wiki refused this write: the token needs the wiki:write permission — sign in \
37         again with `ytcli auth login` — or this account may not edit that page"
38    )]
39    WikiWriteForbidden,
40    #[error("{0} not found")]
41    NotFound(String),
42    #[error("rate limited by Tracker (429)")]
43    RateLimited,
44    #[error("Tracker rejected the request ({status}): {message}")]
45    Rejected {
46        status: reqwest::StatusCode,
47        message: String,
48    },
49    #[error("could not decode the Tracker response")]
50    Decode(#[source] serde_json::Error),
51}
52
53impl ApiError {
54    #[must_use]
55    pub fn exit_code(&self) -> ExitCode {
56        match self {
57            Self::Unauthorized | Self::WikiForbidden | Self::WikiWriteForbidden => ExitCode::Auth,
58            Self::NotFound(_) => ExitCode::NotFound,
59            Self::Forbidden | Self::WikiNotEnabled | Self::RateLimited | Self::Rejected { .. } => {
60                ExitCode::ApiRejected
61            }
62            Self::Transport(_) | Self::Decode(_) => ExitCode::Failure,
63        }
64    }
65}