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}
16
17#[derive(Debug)]
19pub enum LinearError {
20 Authentication(String),
22 RateLimited {
24 retry_after: Option<f64>,
25 message: String,
26 },
27 InvalidInput(String),
29 Forbidden(String),
31 Network(reqwest::Error),
33 GraphQL(Vec<GraphQLError>),
35 MissingData(String),
37 HttpError { status: u16, body: String },
39 AuthConfig(String),
41 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 let err = LinearError::Authentication("test".to_string());
233 assert!(std::error::Error::source(&err).is_none());
234 }
235}