1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[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#[derive(Debug)]
21pub enum LinearError {
22 Authentication(String),
24 RateLimited {
26 retry_after: Option<f64>,
27 message: String,
28 },
29 InvalidInput(String),
31 Forbidden(String),
33 Network(reqwest::Error),
35 GraphQL {
37 errors: Vec<GraphQLError>,
38 query_name: Option<String>,
39 },
40 MissingData(String),
42 HttpError { status: u16, body: String },
44 AuthConfig(String),
46 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 let err = LinearError::Authentication("test".to_string());
283 assert!(std::error::Error::source(&err).is_none());
284 }
285}