yandex-tracker-cli 2.1.1

Token-efficient Yandex Tracker CLI for humans and AI agents
Documentation
//! Typed API failures.
//!
//! The variants exist so the shell can map them to distinct exit codes and to
//! actionable messages; a single opaque "request failed" would make both
//! impossible.

use crate::exit::ExitCode;

#[derive(Debug, thiserror::Error)]
pub enum ApiError {
    #[error("transport error talking to Tracker")]
    Transport(#[from] reqwest::Error),
    #[error("not authenticated: the token was rejected (401)")]
    Unauthorized,
    #[error("forbidden (403): the account lacks rights, or the organisation header is wrong")]
    Forbidden,
    // The Wiki's refusal has a likelier cause than Tracker's: a token issued
    // before the Wiki permission was added to the application. Saying so turns
    // a rights puzzle into one command.
    #[error(
        "the Wiki refused this token: it needs the wiki:read permission — sign in again \
         with `ytcli auth login` — or this account cannot see that page"
    )]
    WikiForbidden,
    // The Wiki answers 403 with `FORCED_SYNC_REQUIRED` when it has never heard of
    // the organisation: the Wiki was not opened there yet. No sign-in fixes
    // that, so blaming the token would send people round in circles.
    #[error(
        "the Wiki is not set up in this organisation yet — open https://wiki.yandex.ru once, \
         signed in to it, and try again"
    )]
    WikiNotEnabled,
    // Reading and writing are separate permissions, and a token signed in for
    // reading only is refused every write. A 403 that says why is reported as
    // what it says (`WikiRejected`); this is the one that came with nothing,
    // so both likely causes are named rather than the token blamed outright.
    #[error(
        "the Wiki refused this write (403) without saying why: either the token lacks the \
         wiki:write permission — sign in again with `ytcli auth login` — or this account \
         may not edit or create pages there (`ytcli wiki access <page>` shows who may)"
    )]
    WikiWriteForbidden,
    #[error("{0} not found")]
    NotFound(String),
    #[error("rate limited by Tracker (429)")]
    RateLimited,
    #[error("Tracker rejected the request ({status}): {message}")]
    Rejected {
        status: reqwest::StatusCode,
        message: String,
    },
    // The Wiki and Tracker share a client, and a refusal that names the wrong
    // one sends the reader to look at identities in the system that did not
    // answer.
    #[error("the Wiki rejected the request ({status}): {message}")]
    WikiRejected {
        status: reqwest::StatusCode,
        message: String,
    },
    #[error("could not decode the Tracker response")]
    Decode(#[source] serde_json::Error),
}

impl ApiError {
    #[must_use]
    pub fn exit_code(&self) -> ExitCode {
        match self {
            Self::Unauthorized | Self::WikiForbidden | Self::WikiWriteForbidden => ExitCode::Auth,
            Self::NotFound(_) => ExitCode::NotFound,
            Self::Forbidden
            | Self::WikiNotEnabled
            | Self::RateLimited
            | Self::Rejected { .. }
            | Self::WikiRejected { .. } => ExitCode::ApiRejected,
            Self::Transport(_) | Self::Decode(_) => ExitCode::Failure,
        }
    }
}