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. A 403 that says why is reported as
35    // what it says (`WikiRejected`); this is the one that came with nothing,
36    // so both likely causes are named rather than the token blamed outright.
37    #[error(
38        "the Wiki refused this write (403) without saying why: either the token lacks the \
39         wiki:write permission — sign in again with `ytcli auth login` — or this account \
40         may not edit or create pages there (`ytcli wiki access <page>` shows who may)"
41    )]
42    WikiWriteForbidden,
43    #[error("{0} not found")]
44    NotFound(String),
45    #[error("rate limited by Tracker (429)")]
46    RateLimited,
47    #[error("Tracker rejected the request ({status}): {message}")]
48    Rejected {
49        status: reqwest::StatusCode,
50        message: String,
51    },
52    // The Wiki and Tracker share a client, and a refusal that names the wrong
53    // one sends the reader to look at identities in the system that did not
54    // answer.
55    #[error("the Wiki rejected the request ({status}): {message}")]
56    WikiRejected {
57        status: reqwest::StatusCode,
58        message: String,
59    },
60    #[error("could not decode the Tracker response")]
61    Decode(#[source] serde_json::Error),
62}
63
64impl ApiError {
65    #[must_use]
66    pub fn exit_code(&self) -> ExitCode {
67        match self {
68            Self::Unauthorized | Self::WikiForbidden | Self::WikiWriteForbidden => ExitCode::Auth,
69            Self::NotFound(_) => ExitCode::NotFound,
70            Self::Forbidden
71            | Self::WikiNotEnabled
72            | Self::RateLimited
73            | Self::Rejected { .. }
74            | Self::WikiRejected { .. } => ExitCode::ApiRejected,
75            Self::Transport(_) | Self::Decode(_) => ExitCode::Failure,
76        }
77    }
78}