Skip to main content

ic_rig/
http.rs

1//! HTTP client abstraction.
2//!
3//! This crate ships **no** HTTP implementation. You provide one by implementing
4//! [`HttpClient`]. This lets you plug in whatever transport fits your platform:
5//! - `reqwest` for native and browser-WASM
6//! - `ic_cdk::api::management_canister::http_request` for ICP canisters
7//! - A mock client for tests
8//!
9//! # Example (ICP)
10//!
11//! ```rust,ignore
12//! use irig::http::{HttpClient, HttpRequest, HttpResponse};
13//!
14//! pub struct IcpHttpClient;
15//!
16//! impl HttpClient for IcpHttpClient {
17//!     type Error = String;
18//!
19//!     async fn post(&self, req: HttpRequest) -> Result<HttpResponse, Self::Error> {
20//!         // Build ic_cdk http_request args from `req`, call the management
21//!         // canister, then map the response back to HttpResponse.
22//!         todo!()
23//!     }
24//! }
25//! ```
26
27use std::collections::HashMap;
28use thiserror::Error;
29
30// ── Request / Response ────────────────────────────────────────────────────────
31
32/// A minimal HTTP POST request.
33#[derive(Debug, Clone)]
34pub struct HttpRequest {
35    /// Absolute URL.
36    pub url: String,
37    /// HTTP headers (e.g. `Authorization`, `Content-Type`).
38    pub headers: HashMap<String, String>,
39    /// Raw request body (typically JSON bytes).
40    pub body: Vec<u8>,
41}
42
43impl HttpRequest {
44    pub fn new(url: impl Into<String>) -> Self {
45        Self {
46            url: url.into(),
47            headers: HashMap::new(),
48            body: Vec::new(),
49        }
50    }
51
52    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
53        self.headers.insert(key.into(), value.into());
54        self
55    }
56
57    pub fn json_body(mut self, body: Vec<u8>) -> Self {
58        self.headers
59            .insert("Content-Type".to_owned(), "application/json".to_owned());
60        self.body = body;
61        self
62    }
63}
64
65/// A minimal HTTP response.
66#[derive(Debug, Clone)]
67pub struct HttpResponse {
68    /// HTTP status code.
69    pub status: u16,
70    /// Raw response body.
71    pub body: Vec<u8>,
72}
73
74impl HttpResponse {
75    /// Deserialise the body as JSON.
76    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
77        serde_json::from_slice(&self.body)
78    }
79
80    /// Check whether the status indicates success (2xx).
81    pub fn is_success(&self) -> bool {
82        self.status >= 200 && self.status < 300
83    }
84}
85
86// ── Trait ─────────────────────────────────────────────────────────────────────
87
88/// Implement this trait to provide an HTTP backend.
89///
90/// Only `POST` is required because every LLM provider API uses POST for
91/// inference. If you need other methods (e.g. GET for model listing), extend
92/// this trait in your own provider code.
93pub trait HttpClient {
94    type Error: std::error::Error + 'static;
95
96    /// Send an HTTP POST request and return the response.
97    fn post(
98        &self,
99        req: HttpRequest,
100    ) -> impl std::future::Future<Output = Result<HttpResponse, Self::Error>>;
101}
102
103// ── Error ─────────────────────────────────────────────────────────────────────
104
105/// Error wrapping an [`HttpClient`] transport failure.
106#[derive(Debug, Error)]
107#[error("HTTP transport error: {0}")]
108pub struct HttpError(pub String);