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);