Skip to main content

pi_ai/auth/
error.rs

1//! Auth and model-collection error types.
2//!
3//! [`ModelsError`] is the shared failure type for auth resolution, catalog
4//! loading, and provider/stream setup. Store and interactive login failures use
5//! the narrower [`StoreError`] and [`AuthError`] types until a caller lifts
6//! them into a [`ModelsError`].
7
8use std::fmt;
9
10/// Machine-readable models/auth error classification.
11///
12/// Wire/text form matches the TypeScript `ModelsErrorCode` union.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum ModelsErrorCode {
15    /// Model catalog/source failure.
16    ModelSource,
17    /// Model payload failed validation.
18    ModelValidation,
19    /// Provider registration or lookup failure.
20    Provider,
21    /// Streaming infrastructure failure.
22    Stream,
23    /// Credential storage or ambient auth failure.
24    Auth,
25    /// OAuth login/refresh failure.
26    Oauth,
27}
28
29impl ModelsErrorCode {
30    /// Stable `snake_case` code string used by the TypeScript surface.
31    #[must_use]
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            Self::ModelSource => "model_source",
35            Self::ModelValidation => "model_validation",
36            Self::Provider => "provider",
37            Self::Stream => "stream",
38            Self::Auth => "auth",
39            Self::Oauth => "oauth",
40        }
41    }
42}
43
44impl fmt::Display for ModelsErrorCode {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(self.as_str())
47    }
48}
49
50impl std::str::FromStr for ModelsErrorCode {
51    type Err = ModelsErrorCodeParseError;
52
53    fn from_str(value: &str) -> Result<Self, Self::Err> {
54        match value {
55            "model_source" => Ok(Self::ModelSource),
56            "model_validation" => Ok(Self::ModelValidation),
57            "provider" => Ok(Self::Provider),
58            "stream" => Ok(Self::Stream),
59            "auth" => Ok(Self::Auth),
60            "oauth" => Ok(Self::Oauth),
61            other => Err(ModelsErrorCodeParseError {
62                value: other.to_owned(),
63            }),
64        }
65    }
66}
67
68/// Failure parsing a [`ModelsErrorCode`] string.
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct ModelsErrorCodeParseError {
71    value: String,
72}
73
74impl fmt::Display for ModelsErrorCodeParseError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "unknown models error code: {}", self.value)
77    }
78}
79
80impl std::error::Error for ModelsErrorCodeParseError {}
81
82/// Shared models/auth failure with a stable code.
83#[derive(Clone, Debug, thiserror::Error)]
84#[error("{message}")]
85pub struct ModelsError {
86    /// Stable error classification.
87    pub code: ModelsErrorCode,
88    message: String,
89    cancelled: bool,
90}
91
92impl ModelsError {
93    /// Create a models error from a code and message.
94    #[must_use]
95    pub fn new(code: ModelsErrorCode, message: impl Into<String>) -> Self {
96        Self {
97            code,
98            message: message.into(),
99            cancelled: false,
100        }
101    }
102
103    /// Create a request-cancellation error without expanding the stable
104    /// [`ModelsErrorCode`] wire contract.
105    #[must_use]
106    pub fn cancelled() -> Self {
107        Self {
108            code: ModelsErrorCode::Oauth,
109            message: "Login cancelled".to_owned(),
110            cancelled: true,
111        }
112    }
113
114    /// Whether auth resolution stopped because its request was cancelled.
115    #[must_use]
116    pub const fn is_cancelled(&self) -> bool {
117        self.cancelled
118    }
119
120    /// Human-readable error message.
121    #[must_use]
122    pub fn message(&self) -> &str {
123        &self.message
124    }
125}
126
127/// Interactive login/auth-flow failure.
128#[derive(Clone, Debug, thiserror::Error)]
129pub enum AuthError {
130    /// User cancelled the login flow.
131    #[error("Login cancelled")]
132    Cancelled,
133    /// Flow-specific failure message.
134    #[error("{0}")]
135    Message(String),
136}
137
138impl AuthError {
139    /// Create a message-carrying auth error.
140    #[must_use]
141    pub fn message(message: impl Into<String>) -> Self {
142        Self::Message(message.into())
143    }
144}
145
146/// Credential-store persistence or mutation failure.
147#[derive(Clone, Debug, thiserror::Error)]
148pub enum StoreError {
149    /// Generic storage failure.
150    #[error("{0}")]
151    Message(String),
152    /// Failure raised by a `modify` callback.
153    #[error(transparent)]
154    Auth(#[from] AuthError),
155}
156
157impl StoreError {
158    /// Create a message-carrying store error.
159    #[must_use]
160    pub fn message(message: impl Into<String>) -> Self {
161        Self::Message(message.into())
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn models_error_code_roundtrips_snake_case() -> Result<(), ModelsErrorCodeParseError> {
171        for code in [
172            ModelsErrorCode::ModelSource,
173            ModelsErrorCode::ModelValidation,
174            ModelsErrorCode::Provider,
175            ModelsErrorCode::Stream,
176            ModelsErrorCode::Auth,
177            ModelsErrorCode::Oauth,
178        ] {
179            let text = code.as_str();
180            let parsed: ModelsErrorCode = text.parse()?;
181            assert_eq!(parsed, code);
182            assert_eq!(parsed.to_string(), text);
183        }
184        Ok(())
185    }
186
187    #[test]
188    fn models_error_preserves_code_and_message() {
189        let err = ModelsError::new(ModelsErrorCode::Oauth, "refresh failed");
190        assert_eq!(err.code, ModelsErrorCode::Oauth);
191        assert_eq!(err.message(), "refresh failed");
192        assert_eq!(err.to_string(), "refresh failed");
193    }
194}