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