cp-cli 0.1.0

Human-first command-line client for competitive-programming platforms
Documentation
pub(crate) type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("invalid account name: use 1–128 ASCII letters, digits, dots, hyphens or underscores")]
    InvalidAccountName,
    #[error("invalid HackerEarth topic: use lowercase URL path segments separated by slashes")]
    InvalidPracticeTopic,
    #[error("invalid problem slug: use 1–128 ASCII letters, digits, hyphens or underscores")]
    InvalidProblemId,
    #[error("invalid problem identifier: use a positive problem number or title slug")]
    InvalidProblemSelector,
    #[error("problem number was not found; search for it and use the returned slug")]
    ProblemNumberNotFound,
    #[error("invalid language: use a LeetCode language slug such as rust, cpp or python3")]
    InvalidLanguage,
    #[error("this problem does not provide a {0} starter")]
    LanguageUnavailable(Box<str>),
    #[error("invalid search query: use 1–100 printable ASCII characters")]
    InvalidSearchQuery,
    #[error("invalid problem tag: use 1–64 lowercase letters, digits or hyphens")]
    InvalidProblemTag,
    #[error("invalid contest ID: use 1–128 ASCII letters, digits or internal hyphens")]
    InvalidContestSlug,
    #[error("invalid discussion identifier: use a positive numeric topic ID")]
    InvalidDiscussionId,
    #[error("invalid {platform} problem ID; use {expected}")]
    InvalidCatalogProblemId {
        platform: &'static str,
        expected: &'static str,
    },
    #[error("{platform} problem pages start at 1")]
    InvalidProblemPage { platform: &'static str },
    #[error("{platform} catalogue page {page} does not exist; the last page is {last}")]
    ProblemPageNotFound {
        platform: &'static str,
        page: u8,
        last: usize,
    },
    #[error("Exercism requires --track <slug>, such as --track rust")]
    ExercismTrackRequired,
    #[error("{platform} requires an account name for {capability}")]
    AccountNameRequired {
        platform: &'static str,
        capability: &'static str,
    },
    #[error("{platform} does not provide {capability}")]
    CapabilityUnavailable {
        platform: &'static str,
        capability: &'static str,
    },
    #[error("contest {id} was not found on {platform}")]
    ContestNotFound {
        platform: &'static str,
        id: Box<str>,
    },
    #[error("{0}")]
    ExercismCli(Box<str>),
    #[error("{platform} does not expose a callable terminal API for `cp-cli {command}`")]
    UnsupportedPlatformCommand {
        platform: &'static str,
        command: &'static str,
    },
    #[cfg(not(feature = "codechef"))]
    #[error("CodeChef support is disabled; rebuild with --features codechef")]
    CodeChefDisabled,
    #[cfg(feature = "codechef")]
    #[error(transparent)]
    CodeChef(#[from] platform_codechef::Error),
    #[cfg(not(feature = "codeforces"))]
    #[error("Codeforces support is disabled; rebuild with --features codeforces")]
    CodeforcesDisabled,
    #[cfg(feature = "codeforces")]
    #[error(transparent)]
    Codeforces(#[from] platform_codeforces::Error),
    #[cfg(not(feature = "hackerrank"))]
    #[error("HackerRank support is disabled; rebuild with --features hackerrank")]
    HackerRankDisabled,
    #[cfg(feature = "hackerrank")]
    #[error(transparent)]
    HackerRank(#[from] platform_hackerrank::Error),
    #[cfg(not(feature = "hackerearth"))]
    #[error("HackerEarth support is disabled; rebuild with --features hackerearth")]
    HackerEarthDisabled,
    #[cfg(feature = "hackerearth")]
    #[error(transparent)]
    HackerEarth(#[from] platform_hackerearth::Error),
    #[cfg(not(feature = "project-euler"))]
    #[error("Project Euler support is disabled; rebuild with --features project-euler")]
    ProjectEulerDisabled,
    #[cfg(feature = "project-euler")]
    #[error(transparent)]
    ProjectEuler(#[from] platform_project_euler::Error),
    #[cfg(not(feature = "leetcode"))]
    #[error("LeetCode support is disabled; rebuild with --features leetcode")]
    LeetCodeDisabled,
    #[cfg(feature = "leetcode")]
    #[error(transparent)]
    LeetCode(#[from] platform_leetcode::Error),
    #[error(
        "LeetCode authentication is required; set LEETCODE_SESSION and LEETCODE_CSRFTOKEN together"
    )]
    AuthenticationRequired,
    #[error("LEETCODE_SESSION and LEETCODE_CSRFTOKEN must be set together as valid Unicode")]
    InvalidAuthenticationEnvironment,
    #[error("CODEFORCES_API_KEY and CODEFORCES_API_SECRET must be set together as valid Unicode")]
    InvalidCodeforcesAuthenticationEnvironment,
    #[error("auth login requires an interactive terminal")]
    InteractiveLoginRequired,
    #[cfg(any(feature = "codeforces", feature = "leetcode"))]
    #[error("could not access the operating system's secure credential store")]
    CredentialStore(#[source] keyring::Error),
    #[error(
        "saved LeetCode credentials are unreadable; run `cp-cli auth logout`, then configure a valid environment session"
    )]
    InvalidStoredCredentials,
    #[error(
        "saved Codeforces API credentials are unreadable; run `cp-cli --platform codeforces auth logout`, then log in again"
    )]
    InvalidCodeforcesStoredCredentials,
    #[error("a solution file or its metadata already exists at {0}")]
    SolutionExists(std::path::PathBuf),
    #[error("could not access workspace path {path}: {source}")]
    WorkspaceIo {
        path: std::path::PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("invalid solution workspace at {path}: {reason}")]
    InvalidWorkspace {
        path: std::path::PathBuf,
        reason: &'static str,
    },
    #[error("choose --lang and --dir when problem pick is not attached to a terminal")]
    InteractivePickRequired,
    #[error("problem pick was cancelled")]
    PickCancelled,
    #[error("the LeetCode test run {0} did not finish within 60 seconds")]
    TestJudgeTimeout(Box<str>),
    #[error("the LeetCode judge did not finish submission {0} within 60 seconds")]
    JudgeTimeout(u64),
    #[error("could not write output: {0}")]
    Io(#[from] std::io::Error),
    #[error("could not write JSON output: {0}")]
    Json(#[from] serde_json::Error),
}

impl From<crate::exercism_cli::ExercismCliError> for Error {
    fn from(error: crate::exercism_cli::ExercismCliError) -> Self {
        Self::ExercismCli(error.to_string().into())
    }
}

impl Error {
    pub fn is_broken_pipe(&self) -> bool {
        match self {
            Self::Io(error) => error.kind() == std::io::ErrorKind::BrokenPipe,
            Self::Json(error) => error.io_error_kind() == Some(std::io::ErrorKind::BrokenPipe),
            _ => false,
        }
    }
}