lin-cli 0.7.0

A fast CLI for Linear
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};

use crate::api::types::GraphQLResponse;
use crate::error::LinError;

const LINEAR_API_URL: &str = "https://api.linear.app/graphql";

pub struct LinearClient {
    client: Client,
    token: String,
    verbose: bool,
}

impl LinearClient {
    pub fn new(token: &str) -> Self {
        Self {
            client: Client::new(),
            token: token.to_string(),
            verbose: false,
        }
    }

    pub fn with_verbose(mut self, verbose: bool) -> Self {
        self.verbose = verbose;
        self
    }

    async fn send_query(&self, query: &str, variables: Option<Value>) -> Result<Value, LinError> {
        let body = json!({
            "query": query,
            "variables": variables.unwrap_or(json!({})),
        });

        let response = self
            .client
            .post(LINEAR_API_URL)
            .header("Authorization", &self.token)
            .header("Content-Type", "application/json")
            .json(&body)
            .send()
            .await?;

        let status = response.status();
        if !status.is_success() {
            let text = response.text().await.unwrap_or_default();
            return Err(LinError::ApiError(format!("HTTP {status}: {text}")));
        }

        let text = response.text().await?;
        let gql_response: GraphQLResponse<Value> = serde_json::from_str(&text).map_err(|e| {
            if self.verbose {
                LinError::ApiError(format!("Failed to decode response: {e}\n\nBody:\n{text}"))
            } else {
                LinError::ApiError(format!("Failed to decode response: {e}"))
            }
        })?;

        if let Some(errors) = gql_response.errors {
            let messages: Vec<String> = errors.into_iter().map(|e| e.message).collect();
            return Err(LinError::GraphQLErrors(messages));
        }

        gql_response
            .data
            .ok_or_else(|| LinError::ApiError("No data in response".to_string()))
    }

    pub async fn execute<T: DeserializeOwned>(
        &self,
        query: &str,
        variables: Option<Value>,
    ) -> Result<T, LinError> {
        let raw = self.send_query(query, variables).await?;
        let raw_str = if self.verbose {
            Some(raw.to_string())
        } else {
            None
        };
        serde_json::from_value(raw).map_err(|e| {
            if let Some(body) = raw_str {
                LinError::ApiError(format!("Failed to decode response: {e}\n\nBody:\n{body}"))
            } else {
                LinError::ApiError(format!("Failed to decode response: {e}"))
            }
        })
    }

    pub fn token(&self) -> &str {
        &self.token
    }

    pub async fn execute_raw(
        &self,
        query: &str,
        variables: Option<Value>,
    ) -> Result<Value, LinError> {
        self.send_query(query, variables).await
    }
}