1use crate::auth;
2use crate::error::{GraphQLError, LinearError};
3use crate::pagination::Connection;
4use serde::de::DeserializeOwned;
5
6const LINEAR_API_URL: &str = "https://api.linear.app/graphql";
7
8#[derive(Debug, Clone)]
10pub struct Client {
11 http: reqwest::Client,
12 token: String,
13}
14
15#[derive(serde::Deserialize)]
17struct GraphQLResponse {
18 data: Option<serde_json::Value>,
19 errors: Option<Vec<GraphQLError>>,
20}
21
22impl Client {
23 pub fn from_token(token: impl Into<String>) -> Result<Self, LinearError> {
25 let token = token.into();
26 if token.is_empty() {
27 return Err(LinearError::AuthConfig("Token cannot be empty".to_string()));
28 }
29 Ok(Self {
30 http: reqwest::Client::new(),
31 token,
32 })
33 }
34
35 pub fn from_env() -> Result<Self, LinearError> {
37 Self::from_token(auth::token_from_env()?)
38 }
39
40 pub fn from_file() -> Result<Self, LinearError> {
42 Self::from_token(auth::token_from_file()?)
43 }
44
45 pub fn auto() -> Result<Self, LinearError> {
47 Self::from_token(auth::auto_token()?)
48 }
49
50 pub async fn execute<T: DeserializeOwned>(
52 &self,
53 query: &str,
54 variables: serde_json::Value,
55 data_path: &str,
56 ) -> Result<T, LinearError> {
57 let body = serde_json::json!({
58 "query": query,
59 "variables": variables,
60 });
61
62 let response = self
63 .http
64 .post(LINEAR_API_URL)
65 .header("Authorization", &self.token)
66 .header("Content-Type", "application/json")
67 .json(&body)
68 .send()
69 .await?;
70
71 let status = response.status();
72 if status == 401 || status == 403 {
73 let text = response.text().await.unwrap_or_default();
74 if status == 401 {
75 return Err(LinearError::Authentication(text));
76 }
77 return Err(LinearError::Forbidden(text));
78 }
79 if status == 429 {
80 let retry_after = response
81 .headers()
82 .get("retry-after")
83 .and_then(|v| v.to_str().ok())
84 .and_then(|v| v.parse::<f64>().ok());
85 let text = response.text().await.unwrap_or_default();
86 return Err(LinearError::RateLimited {
87 retry_after,
88 message: text,
89 });
90 }
91
92 let gql_response: GraphQLResponse = response.json().await?;
93
94 if let Some(errors) = gql_response.errors {
96 if !errors.is_empty() {
97 let first_msg = errors[0].message.to_lowercase();
99 if first_msg.contains("authentication") || first_msg.contains("unauthorized") {
100 return Err(LinearError::Authentication(errors[0].message.clone()));
101 }
102 return Err(LinearError::GraphQL(errors));
103 }
104 }
105
106 let data = gql_response
107 .data
108 .ok_or_else(|| LinearError::MissingData("No data in response".to_string()))?;
109
110 let value = data
111 .get(data_path)
112 .ok_or_else(|| {
113 LinearError::MissingData(format!("No '{}' in response data", data_path))
114 })?
115 .clone();
116
117 serde_json::from_value(value).map_err(|e| {
118 LinearError::MissingData(format!("Failed to deserialize '{}': {}", data_path, e))
119 })
120 }
121
122 pub async fn execute_connection<T: DeserializeOwned + Default>(
124 &self,
125 query: &str,
126 variables: serde_json::Value,
127 data_path: &str,
128 ) -> Result<Connection<T>, LinearError> {
129 self.execute::<Connection<T>>(query, variables, data_path)
130 .await
131 }
132}