1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct GraphQLError {
7 pub message: String,
8 #[serde(default)]
9 pub extensions: Option<serde_json::Value>,
10}
11
12#[derive(Debug)]
14pub enum LinearError {
15 Authentication(String),
17 RateLimited {
19 retry_after: Option<f64>,
20 message: String,
21 },
22 InvalidInput(String),
24 Forbidden(String),
26 Network(reqwest::Error),
28 GraphQL(Vec<GraphQLError>),
30 MissingData(String),
32 HttpError { status: u16, body: String },
34 AuthConfig(String),
36}
37
38impl fmt::Display for LinearError {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Self::Authentication(msg) => write!(f, "Authentication error: {}", msg),
42 Self::RateLimited { message, .. } => write!(f, "Rate limited: {}", message),
43 Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
44 Self::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
45 Self::Network(e) => write!(f, "Network error: {}", e),
46 Self::GraphQL(errors) => {
47 let msgs: Vec<String> = errors
48 .iter()
49 .map(|e| {
50 if let Some(ext) = &e.extensions {
51 format!("{} ({})", e.message, ext)
52 } else {
53 e.message.clone()
54 }
55 })
56 .collect();
57 write!(f, "GraphQL errors: {}", msgs.join("; "))
58 }
59 Self::HttpError { status, body } => {
60 write!(f, "HTTP error {}: {}", status, body)
61 }
62 Self::MissingData(path) => write!(f, "Missing data at path: {}", path),
63 Self::AuthConfig(msg) => write!(f, "Auth configuration error: {}", msg),
64 }
65 }
66}
67
68impl std::error::Error for LinearError {
69 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70 match self {
71 Self::Network(e) => Some(e),
72 _ => None,
73 }
74 }
75}
76
77impl From<reqwest::Error> for LinearError {
78 fn from(e: reqwest::Error) -> Self {
79 Self::Network(e)
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn display_authentication_error() {
89 let err = LinearError::Authentication("Invalid token".to_string());
90 assert_eq!(err.to_string(), "Authentication error: Invalid token");
91 }
92
93 #[test]
94 fn display_rate_limited_error() {
95 let err = LinearError::RateLimited {
96 retry_after: Some(30.0),
97 message: "Too many requests".to_string(),
98 };
99 assert_eq!(err.to_string(), "Rate limited: Too many requests");
100 }
101
102 #[test]
103 fn display_invalid_input_error() {
104 let err = LinearError::InvalidInput("bad field".to_string());
105 assert_eq!(err.to_string(), "Invalid input: bad field");
106 }
107
108 #[test]
109 fn display_forbidden_error() {
110 let err = LinearError::Forbidden("not allowed".to_string());
111 assert_eq!(err.to_string(), "Forbidden: not allowed");
112 }
113
114 #[test]
115 fn display_graphql_error_single() {
116 let err = LinearError::GraphQL(vec![GraphQLError {
117 message: "Field not found".to_string(),
118 extensions: None,
119 }]);
120 assert_eq!(err.to_string(), "GraphQL errors: Field not found");
121 }
122
123 #[test]
124 fn display_graphql_error_with_extensions() {
125 let err = LinearError::GraphQL(vec![GraphQLError {
126 message: "Error".to_string(),
127 extensions: Some(serde_json::json!({"code": "VALIDATION"})),
128 }]);
129 let display = err.to_string();
130 assert!(display.contains("Error"));
131 assert!(display.contains("VALIDATION"));
132 }
133
134 #[test]
135 fn display_graphql_error_multiple() {
136 let err = LinearError::GraphQL(vec![
137 GraphQLError {
138 message: "Error 1".to_string(),
139 extensions: None,
140 },
141 GraphQLError {
142 message: "Error 2".to_string(),
143 extensions: None,
144 },
145 ]);
146 let display = err.to_string();
147 assert!(display.contains("Error 1"));
148 assert!(display.contains("Error 2"));
149 assert!(display.contains("; "));
150 }
151
152 #[test]
153 fn display_http_error() {
154 let err = LinearError::HttpError {
155 status: 500,
156 body: "Internal Server Error".to_string(),
157 };
158 assert_eq!(err.to_string(), "HTTP error 500: Internal Server Error");
159 }
160
161 #[test]
162 fn display_missing_data_error() {
163 let err = LinearError::MissingData("No 'viewer' in response data".to_string());
164 assert_eq!(
165 err.to_string(),
166 "Missing data at path: No 'viewer' in response data"
167 );
168 }
169
170 #[test]
171 fn display_auth_config_error() {
172 let err = LinearError::AuthConfig("Token file not found".to_string());
173 assert_eq!(
174 err.to_string(),
175 "Auth configuration error: Token file not found"
176 );
177 }
178
179 #[test]
180 fn graphql_error_deserializes() {
181 let json = r#"{"message": "Something failed", "extensions": {"code": "BAD_INPUT"}}"#;
182 let err: GraphQLError = serde_json::from_str(json).unwrap();
183 assert_eq!(err.message, "Something failed");
184 assert!(err.extensions.is_some());
185 }
186
187 #[test]
188 fn graphql_error_deserializes_without_extensions() {
189 let json = r#"{"message": "Something failed"}"#;
190 let err: GraphQLError = serde_json::from_str(json).unwrap();
191 assert_eq!(err.message, "Something failed");
192 assert!(err.extensions.is_none());
193 }
194
195 #[test]
196 fn graphql_error_serializes() {
197 let err = GraphQLError {
198 message: "test".to_string(),
199 extensions: None,
200 };
201 let json = serde_json::to_value(&err).unwrap();
202 assert_eq!(json["message"], "test");
203 }
204
205 #[test]
206 fn linear_error_is_std_error() {
207 let err = LinearError::Authentication("test".to_string());
208 let _: &dyn std::error::Error = &err;
209 }
210
211 #[test]
212 fn network_error_has_source() {
213 let err = LinearError::Authentication("test".to_string());
216 assert!(std::error::Error::source(&err).is_none());
217 }
218}