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 .header(
68 "User-Agent",
69 format!("lineark-sdk/{}", env!("CARGO_PKG_VERSION")),
70 )
71 .json(&body)
72 .send()
73 .await?;
74
75 let status = response.status();
76 if status == 401 || status == 403 {
77 let text = response.text().await.unwrap_or_default();
78 if status == 401 {
79 return Err(LinearError::Authentication(text));
80 }
81 return Err(LinearError::Forbidden(text));
82 }
83 if status == 429 {
84 let retry_after = response
85 .headers()
86 .get("retry-after")
87 .and_then(|v| v.to_str().ok())
88 .and_then(|v| v.parse::<f64>().ok());
89 let text = response.text().await.unwrap_or_default();
90 return Err(LinearError::RateLimited {
91 retry_after,
92 message: text,
93 });
94 }
95 if !status.is_success() {
96 let body = response.text().await.unwrap_or_default();
97 return Err(LinearError::HttpError {
98 status: status.as_u16(),
99 body,
100 });
101 }
102
103 let gql_response: GraphQLResponse = response.json().await?;
104
105 if let Some(errors) = gql_response.errors {
107 if !errors.is_empty() {
108 let first_msg = errors[0].message.to_lowercase();
110 if first_msg.contains("authentication") || first_msg.contains("unauthorized") {
111 return Err(LinearError::Authentication(errors[0].message.clone()));
112 }
113 return Err(LinearError::GraphQL(errors));
114 }
115 }
116
117 let data = gql_response
118 .data
119 .ok_or_else(|| LinearError::MissingData("No data in response".to_string()))?;
120
121 let value = data
122 .get(data_path)
123 .ok_or_else(|| {
124 LinearError::MissingData(format!("No '{}' in response data", data_path))
125 })?
126 .clone();
127
128 serde_json::from_value(value).map_err(|e| {
129 LinearError::MissingData(format!("Failed to deserialize '{}': {}", data_path, e))
130 })
131 }
132
133 pub async fn execute_connection<T: DeserializeOwned>(
135 &self,
136 query: &str,
137 variables: serde_json::Value,
138 data_path: &str,
139 ) -> Result<Connection<T>, LinearError> {
140 self.execute::<Connection<T>>(query, variables, data_path)
141 .await
142 }
143}