Skip to main content

linear_tools/
http.rs

1use anyhow::Result;
2use anyhow::anyhow;
3use cynic::http::ReqwestExt;
4use reqwest::Client;
5
6pub struct LinearClient {
7    client: Client,
8    url: String,
9    api_key: String,
10}
11
12/// Centralized GraphQL error extraction - fails fast on any errors
13pub fn extract_data<Q>(resp: cynic::GraphQlResponse<Q>) -> Result<Q> {
14    if let Some(errors) = resp.errors
15        && !errors.is_empty()
16    {
17        let mut parts = Vec::new();
18        for e in errors {
19            let path = e.path.unwrap_or_default();
20            let path_str = if path.is_empty() {
21                String::new()
22            } else {
23                let p = path
24                    .into_iter()
25                    .map(|v| match v {
26                        cynic::GraphQlErrorPathSegment::Field(f) => f,
27                        cynic::GraphQlErrorPathSegment::Index(i) => i.to_string(),
28                    })
29                    .collect::<Vec<_>>()
30                    .join(".");
31                format!(" (path: {})", p)
32            };
33            parts.push(format!("{}{}", e.message, path_str));
34        }
35        return Err(anyhow!(
36            "GraphQL errors from Linear:\n- {}",
37            parts.join("\n- ")
38        ));
39    }
40
41    match resp.data {
42        Some(data) => Ok(data),
43        None => Err(anyhow!("No data returned from Linear")),
44    }
45}
46
47impl LinearClient {
48    pub fn new(api_key: Option<String>) -> Result<Self> {
49        let api_key = match api_key.or_else(|| std::env::var("LINEAR_API_KEY").ok()) {
50            Some(k) if !k.is_empty() => k,
51            _ => return Err(anyhow!("LINEAR_API_KEY environment variable is not set")),
52        };
53
54        let url = std::env::var("LINEAR_GRAPHQL_URL")
55            .ok()
56            .filter(|u| !u.is_empty())
57            .unwrap_or_else(|| "https://api.linear.app/graphql".to_string());
58
59        let client = Client::builder().user_agent("linear-tools/0.1.0").build()?;
60
61        Ok(Self {
62            client,
63            url,
64            api_key,
65        })
66    }
67
68    pub async fn run<Q, V>(&self, op: cynic::Operation<Q, V>) -> Result<cynic::GraphQlResponse<Q>>
69    where
70        Q: serde::de::DeserializeOwned + 'static,
71        V: serde::Serialize,
72    {
73        let mut req = self
74            .client
75            .post(&self.url)
76            .header("Content-Type", "application/json");
77
78        // Auto-detect auth header type:
79        // - Personal API key: "lin_api_*" => raw Authorization header
80        // - OAuth2 token: anything else => Bearer token
81        if self.api_key.starts_with("lin_api_") {
82            req = req.header("Authorization", &self.api_key);
83        } else {
84            req = req.bearer_auth(&self.api_key);
85        }
86
87        let result = req.run_graphql(op).await;
88        result.map_err(|e| anyhow!(e))
89    }
90}