tranc-cli 0.1.1

Tranc CLI — trade indicator queries from the command line.
//! HTTP client wrapper for the Tranc API.
//!
//! Builds a `reqwest::Client` with the bearer token from the OS keyring
//! (or `TRANC_API_KEY` env var as an escape hatch for agent/CI use).

use anyhow::{bail, Context, Result};
use reqwest::{Client, RequestBuilder, Response, StatusCode};
use serde_json::Value;

use crate::keyring_store;

/// Build an authenticated `reqwest::Client`.
///
/// Token resolution order:
/// 1. `TRANC_API_KEY` environment variable (agent / CI use)
/// 2. OS keyring (interactive users)
pub fn build_client() -> Result<(Client, String)> {
    let token = if let Ok(key) = std::env::var("TRANC_API_KEY") {
        key
    } else {
        keyring_store::get_token()
            .context("failed to read token from keyring")?
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "not authenticated — run `tranc auth login` first, \
                     or set TRANC_API_KEY"
                )
            })?
    };

    let client = Client::builder()
        .user_agent(concat!("tranc-cli/", env!("CARGO_PKG_VERSION")))
        .build()
        .context("failed to build HTTP client")?;

    Ok((client, token))
}

/// Execute a GET request and return the parsed JSON body.
///
/// Errors on non-2xx status codes with a helpful message.
pub async fn get_json(rb: RequestBuilder) -> Result<Value> {
    let resp: Response = rb.send().await.context("HTTP request failed")?;
    parse_response(resp).await
}

/// Execute a POST request and return the parsed JSON body.
pub async fn post_json(rb: RequestBuilder) -> Result<Value> {
    let resp: Response = rb.send().await.context("HTTP request failed")?;
    parse_response(resp).await
}

async fn parse_response(resp: Response) -> Result<Value> {
    let status = resp.status();
    let body = resp.text().await.context("failed to read response body")?;

    if status == StatusCode::UNAUTHORIZED {
        bail!(
            "authentication failed (401) — run `tranc auth login` to refresh your token, \
             or set TRANC_API_KEY"
        );
    }

    if !status.is_success() {
        // Try to extract an error message from a JSON body.
        let detail = serde_json::from_str::<Value>(&body)
            .ok()
            .and_then(|v| v.get("error").or(v.get("message")).cloned())
            .map(|v| v.to_string())
            .unwrap_or_else(|| body.clone());
        bail!("API error {status}: {detail}");
    }

    serde_json::from_str(&body).context("failed to parse JSON response")
}