linear_tools/
http.rs

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