Skip to main content

aptu_core/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Error types for the aptu-core library.
4//!
5//! Uses `thiserror` for deriving `std::error::Error` implementations.
6//! Application code should use `anyhow::Result` for top-level error handling.
7
8use thiserror::Error;
9
10/// Errors that can occur during Aptu operations.
11#[derive(Error, Debug)]
12pub enum AptuError {
13    /// GitHub API error from octocrab.
14    #[error("GitHub API error: {message}")]
15    GitHub {
16        /// Error message.
17        message: String,
18    },
19
20    /// AI provider error (`OpenRouter`, Ollama, etc.).
21    #[error("AI provider error: {message}")]
22    AI {
23        /// Error message from the AI provider.
24        message: String,
25        /// Optional HTTP status code from the provider.
26        status: Option<u16>,
27        /// Name of the AI provider (e.g., `OpenRouter`, `Ollama`).
28        provider: String,
29    },
30
31    /// User is not authenticated - needs to run `aptu auth login`.
32    #[error(
33        "Authentication required - run `aptu auth login` first, or set GITHUB_TOKEN environment variable"
34    )]
35    NotAuthenticated,
36
37    /// AI provider is not authenticated - missing API key.
38    #[error("AI provider '{provider}' is not authenticated - set {env_var} environment variable")]
39    AiProviderNotAuthenticated {
40        /// Name of the AI provider (e.g., `OpenRouter`, `Ollama`).
41        provider: String,
42        /// Environment variable name to set (e.g., `OPENROUTER_API_KEY`).
43        env_var: String,
44    },
45
46    /// Rate limit exceeded from an AI provider.
47    #[error("Rate limit exceeded on {provider}, retry after {retry_after}s")]
48    RateLimited {
49        /// Name of the provider that rate limited (e.g., `OpenRouter`).
50        provider: String,
51        /// Number of seconds to wait before retrying.
52        retry_after: u64,
53    },
54
55    /// AI response was truncated (incomplete JSON due to EOF).
56    #[error("Truncated response from {provider} - response ended prematurely")]
57    TruncatedResponse {
58        /// Name of the AI provider that returned truncated response.
59        provider: String,
60    },
61
62    /// Configuration file error.
63    #[error("Configuration error: {message}")]
64    Config {
65        /// Error message.
66        message: String,
67    },
68
69    /// Invalid JSON response from AI provider.
70    #[error("Invalid JSON response from AI")]
71    InvalidAIResponse(#[source] serde_json::Error),
72
73    /// Network/HTTP error from reqwest.
74    #[cfg(not(target_arch = "wasm32"))]
75    #[error("Network error: {0}")]
76    Network(#[from] reqwest::Error),
77
78    /// Keyring/credential storage error.
79    #[cfg(feature = "keyring")]
80    #[error("Keyring error: {0}")]
81    Keyring(#[from] keyring_core::error::Error),
82
83    /// Circuit breaker is open - AI provider is unavailable.
84    #[error("Circuit breaker is open - AI provider is temporarily unavailable")]
85    CircuitOpen,
86
87    /// Type mismatch: reference is a different type than expected.
88    #[error("#{number} is {actual}, not {expected}")]
89    TypeMismatch {
90        /// The issue/PR number.
91        number: u64,
92        /// Expected type.
93        expected: ResourceType,
94        /// Actual type.
95        actual: ResourceType,
96    },
97
98    /// Model registry error (runtime model validation).
99    #[error("Model registry error: {message}")]
100    ModelRegistry {
101        /// Error message.
102        message: String,
103    },
104
105    /// Model validation error - invalid model ID with suggestions.
106    #[error("Invalid model ID: {model_id}. Did you mean one of these?\n{suggestions}")]
107    ModelValidation {
108        /// The invalid model ID provided by the user.
109        model_id: String,
110        /// Suggested valid model IDs based on fuzzy matching.
111        suggestions: String,
112    },
113
114    /// Security scan error.
115    #[error("Security scan error: {message}")]
116    SecurityScan {
117        /// Error message.
118        message: String,
119    },
120
121    /// A user-supplied input field exceeds its configured byte limit.
122    #[error(
123        "input field `{field}` exceeds limit: {actual_bytes} bytes (limit: {limit_bytes} bytes)"
124    )]
125    InputExceedsLimit {
126        /// Name of the field that exceeded the limit.
127        field: String,
128        /// Actual byte count of the input.
129        actual_bytes: usize,
130        /// Configured byte limit.
131        limit_bytes: usize,
132    },
133}
134
135/// GitHub resource type for type mismatch errors.
136#[derive(Debug, Clone, Copy)]
137pub enum ResourceType {
138    /// GitHub issue.
139    Issue,
140    /// GitHub pull request.
141    PullRequest,
142}
143
144impl std::fmt::Display for ResourceType {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            ResourceType::Issue => write!(f, "issue"),
148            ResourceType::PullRequest => write!(f, "pull request"),
149        }
150    }
151}
152
153#[cfg(not(target_arch = "wasm32"))]
154impl From<octocrab::Error> for AptuError {
155    fn from(err: octocrab::Error) -> Self {
156        AptuError::GitHub {
157            message: err.to_string(),
158        }
159    }
160}
161
162impl From<config::ConfigError> for AptuError {
163    fn from(err: config::ConfigError) -> Self {
164        AptuError::Config {
165            message: err.to_string(),
166        }
167    }
168}