Skip to main content

lineark_sdk/
error.rs

1//! Error types for the Linear SDK.
2//!
3//! [`LinearError`] covers authentication failures, HTTP transport errors,
4//! GraphQL-level errors, rate limiting, and more.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// A single GraphQL error from the API response.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct GraphQLError {
12    pub message: String,
13    #[serde(default)]
14    pub extensions: Option<serde_json::Value>,
15}
16
17/// Errors that can occur when interacting with the Linear API.
18#[derive(Debug)]
19pub enum LinearError {
20    /// Authentication failed (invalid or expired token).
21    Authentication(String),
22    /// Request was rate-limited.
23    RateLimited {
24        retry_after: Option<f64>,
25        message: String,
26    },
27    /// Invalid input (bad arguments to a mutation).
28    InvalidInput(String),
29    /// Forbidden (insufficient permissions).
30    Forbidden(String),
31    /// Network or HTTP transport error.
32    Network(reqwest::Error),
33    /// GraphQL errors returned by the API.
34    GraphQL(Vec<GraphQLError>),
35    /// The requested data path was not found in the response.
36    MissingData(String),
37    /// Non-2xx HTTP response not covered by a more specific variant.
38    HttpError { status: u16, body: String },
39    /// Auth configuration error (no token found).
40    AuthConfig(String),
41    /// Internal error (e.g. runtime creation failure).
42    Internal(String),
43}
44
45impl fmt::Display for LinearError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Authentication(msg) => write!(f, "Authentication error: {}", msg),
49            Self::RateLimited { message, .. } => write!(f, "Rate limited: {}", message),
50            Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
51            Self::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
52            Self::Network(e) => write!(f, "Network error: {}", e),
53            Self::GraphQL(errors) => {
54                let msgs: Vec<String> = errors
55                    .iter()
56                    .map(|e| {
57                        if let Some(ext) = &e.extensions {
58                            format!("{} ({})", e.message, ext)
59                        } else {
60                            e.message.clone()
61                        }
62                    })
63                    .collect();
64                write!(f, "GraphQL errors: {}", msgs.join("; "))
65            }
66            Self::HttpError { status, body } => {
67                write!(f, "HTTP error {}: {}", status, body)
68            }
69            Self::MissingData(path) => write!(f, "Missing data at path: {}", path),
70            Self::AuthConfig(msg) => write!(f, "Auth configuration error: {}", msg),
71            Self::Internal(msg) => write!(f, "Internal error: {}", msg),
72        }
73    }
74}
75
76impl std::error::Error for LinearError {
77    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78        match self {
79            Self::Network(e) => Some(e),
80            _ => None,
81        }
82    }
83}
84
85impl From<reqwest::Error> for LinearError {
86    fn from(e: reqwest::Error) -> Self {
87        Self::Network(e)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn display_authentication_error() {
97        let err = LinearError::Authentication("Invalid token".to_string());
98        assert_eq!(err.to_string(), "Authentication error: Invalid token");
99    }
100
101    #[test]
102    fn display_rate_limited_error() {
103        let err = LinearError::RateLimited {
104            retry_after: Some(30.0),
105            message: "Too many requests".to_string(),
106        };
107        assert_eq!(err.to_string(), "Rate limited: Too many requests");
108    }
109
110    #[test]
111    fn display_invalid_input_error() {
112        let err = LinearError::InvalidInput("bad field".to_string());
113        assert_eq!(err.to_string(), "Invalid input: bad field");
114    }
115
116    #[test]
117    fn display_forbidden_error() {
118        let err = LinearError::Forbidden("not allowed".to_string());
119        assert_eq!(err.to_string(), "Forbidden: not allowed");
120    }
121
122    #[test]
123    fn display_graphql_error_single() {
124        let err = LinearError::GraphQL(vec![GraphQLError {
125            message: "Field not found".to_string(),
126            extensions: None,
127        }]);
128        assert_eq!(err.to_string(), "GraphQL errors: Field not found");
129    }
130
131    #[test]
132    fn display_graphql_error_with_extensions() {
133        let err = LinearError::GraphQL(vec![GraphQLError {
134            message: "Error".to_string(),
135            extensions: Some(serde_json::json!({"code": "VALIDATION"})),
136        }]);
137        let display = err.to_string();
138        assert!(display.contains("Error"));
139        assert!(display.contains("VALIDATION"));
140    }
141
142    #[test]
143    fn display_graphql_error_multiple() {
144        let err = LinearError::GraphQL(vec![
145            GraphQLError {
146                message: "Error 1".to_string(),
147                extensions: None,
148            },
149            GraphQLError {
150                message: "Error 2".to_string(),
151                extensions: None,
152            },
153        ]);
154        let display = err.to_string();
155        assert!(display.contains("Error 1"));
156        assert!(display.contains("Error 2"));
157        assert!(display.contains("; "));
158    }
159
160    #[test]
161    fn display_http_error() {
162        let err = LinearError::HttpError {
163            status: 500,
164            body: "Internal Server Error".to_string(),
165        };
166        assert_eq!(err.to_string(), "HTTP error 500: Internal Server Error");
167    }
168
169    #[test]
170    fn display_missing_data_error() {
171        let err = LinearError::MissingData("No 'viewer' in response data".to_string());
172        assert_eq!(
173            err.to_string(),
174            "Missing data at path: No 'viewer' in response data"
175        );
176    }
177
178    #[test]
179    fn display_auth_config_error() {
180        let err = LinearError::AuthConfig("Token file not found".to_string());
181        assert_eq!(
182            err.to_string(),
183            "Auth configuration error: Token file not found"
184        );
185    }
186
187    #[test]
188    fn graphql_error_deserializes() {
189        let json = r#"{"message": "Something failed", "extensions": {"code": "BAD_INPUT"}}"#;
190        let err: GraphQLError = serde_json::from_str(json).unwrap();
191        assert_eq!(err.message, "Something failed");
192        assert!(err.extensions.is_some());
193    }
194
195    #[test]
196    fn graphql_error_deserializes_without_extensions() {
197        let json = r#"{"message": "Something failed"}"#;
198        let err: GraphQLError = serde_json::from_str(json).unwrap();
199        assert_eq!(err.message, "Something failed");
200        assert!(err.extensions.is_none());
201    }
202
203    #[test]
204    fn graphql_error_serializes() {
205        let err = GraphQLError {
206            message: "test".to_string(),
207            extensions: None,
208        };
209        let json = serde_json::to_value(&err).unwrap();
210        assert_eq!(json["message"], "test");
211    }
212
213    #[test]
214    fn linear_error_is_std_error() {
215        let err = LinearError::Authentication("test".to_string());
216        let _: &dyn std::error::Error = &err;
217    }
218
219    #[test]
220    fn display_internal_error() {
221        let err = LinearError::Internal("Failed to create tokio runtime: foo".to_string());
222        assert_eq!(
223            err.to_string(),
224            "Internal error: Failed to create tokio runtime: foo"
225        );
226    }
227
228    #[test]
229    fn network_error_has_source() {
230        // We can't easily construct a reqwest::Error directly, but we can verify
231        // the source() method returns None for non-Network variants.
232        let err = LinearError::Authentication("test".to_string());
233        assert!(std::error::Error::source(&err).is_none());
234    }
235}