pincer_core/
client.rs

1//! HTTP client traits.
2//!
3//! - [`HttpClient`] - Low-level HTTP execution
4//! - [`PincerClient`] - High-level client with base URL (for `#[pincer]` macro)
5//!
6//! Most users should use the `#[pincer]` macro which generates clients automatically.
7//! Implement [`PincerClient`] directly for custom auth or testing.
8
9use std::future::Future;
10
11use bytes::Bytes;
12use url::Url;
13
14use crate::{Request, Response, Result};
15
16/// Core HTTP client trait.
17///
18/// This trait defines the interface for executing HTTP requests.
19/// Implementations should be async-first and support connection pooling.
20pub trait HttpClient: Send + Sync {
21    /// Execute an HTTP request and return the response.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if the request fails for any reason:
26    /// - Network errors
27    /// - TLS errors
28    /// - Timeouts
29    /// - Invalid response
30    fn execute(
31        &self,
32        request: Request<Bytes>,
33    ) -> impl Future<Output = Result<Response<Bytes>>> + Send;
34}
35
36/// Extension trait for [`HttpClient`] with convenience methods.
37pub trait HttpClientExt: HttpClient {
38    /// Execute a GET request.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if the request fails.
43    fn get(&self, url: &str) -> impl Future<Output = Result<Response<Bytes>>> + Send {
44        async move {
45            let url = url::Url::parse(url)?;
46            let request = Request::builder(crate::Method::Get, url).build();
47            self.execute(request).await
48        }
49    }
50
51    /// Execute a POST request with a JSON body.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if serialization or the request fails.
56    fn post_json<T: serde::Serialize + Send + Sync>(
57        &self,
58        url: &str,
59        body: &T,
60    ) -> impl Future<Output = Result<Response<Bytes>>> + Send {
61        async move {
62            let url = url::Url::parse(url)?;
63            let request = Request::builder(crate::Method::Post, url)
64                .json(body)?
65                .build();
66            self.execute(request).await
67        }
68    }
69
70    /// Execute a PUT request with a JSON body.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if serialization or the request fails.
75    fn put_json<T: serde::Serialize + Send + Sync>(
76        &self,
77        url: &str,
78        body: &T,
79    ) -> impl Future<Output = Result<Response<Bytes>>> + Send {
80        async move {
81            let url = url::Url::parse(url)?;
82            let request = Request::builder(crate::Method::Put, url)
83                .json(body)?
84                .build();
85            self.execute(request).await
86        }
87    }
88
89    /// Execute a DELETE request.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if the request fails.
94    fn delete(&self, url: &str) -> impl Future<Output = Result<Response<Bytes>>> + Send {
95        async move {
96            let url = url::Url::parse(url)?;
97            let request = Request::builder(crate::Method::Delete, url).build();
98            self.execute(request).await
99        }
100    }
101}
102
103// Blanket implementation for all HttpClient implementors
104impl<T: HttpClient> HttpClientExt for T {}
105
106// ============================================================================
107// Pincer Client Trait
108// ============================================================================
109
110/// Trait for types that can be used as pincer API clients.
111///
112/// This trait combines HTTP execution capability with a base URL, enabling
113/// the `#[pincer]` macro to generate implementations for any compatible type.
114///
115/// # Implementing `PincerClient`
116///
117/// You can implement this trait for your own types to:
118/// - Add custom request interceptors (e.g., authentication headers)
119/// - Create mock clients for testing
120/// - Wrap existing HTTP clients with additional functionality
121///
122/// # Example
123///
124/// ```ignore
125/// use pincer::{PincerClient, Request, Response, Result};
126/// use bytes::Bytes;
127/// use url::Url;
128///
129/// #[derive(Clone)]
130/// struct AuthenticatedClient {
131///     inner: HyperClient,
132///     base_url: Url,
133///     token: String,
134/// }
135///
136/// impl PincerClient for AuthenticatedClient {
137///     fn execute(
138///         &self,
139///         request: Request<Bytes>,
140///     ) -> impl Future<Output = Result<Response<Bytes>>> + Send {
141///         let token = self.token.clone();
142///         let inner = self.inner.clone();
143///         async move {
144///             // Inject auth header
145///             let (method, url, mut headers, body) = request.into_parts();
146///             headers.insert("Authorization".to_string(), format!("Bearer {}", token));
147///             let request = Request::from_parts(method, url, headers, body);
148///             inner.execute(request).await
149///         }
150///     }
151///
152///     fn base_url(&self) -> &Url {
153///         &self.base_url
154///     }
155/// }
156/// ```
157pub trait PincerClient: Clone + Send + Sync {
158    /// Execute an HTTP request and return the response.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the request fails for any reason:
163    /// - Network errors
164    /// - TLS errors
165    /// - Timeouts
166    /// - Invalid response
167    fn execute(
168        &self,
169        request: Request<Bytes>,
170    ) -> impl Future<Output = Result<Response<Bytes>>> + Send;
171
172    /// Get the base URL for this client.
173    ///
174    /// All API paths will be resolved relative to this URL.
175    fn base_url(&self) -> &Url;
176}
177
178// ============================================================================
179// Streaming Client Trait (feature-gated)
180// ============================================================================
181
182/// Streaming HTTP client trait.
183///
184/// This trait extends [`HttpClient`] to support streaming responses.
185/// Enable with the `streaming` feature flag.
186#[cfg(feature = "streaming")]
187pub trait HttpClientStreaming: HttpClient {
188    /// Execute an HTTP request and return a streaming response.
189    ///
190    /// Unlike [`HttpClient::execute`], this method returns a response with
191    /// a streaming body that yields chunks as they arrive from the server.
192    ///
193    /// # Errors
194    ///
195    /// Returns an error if the request fails for any reason:
196    /// - Network errors
197    /// - TLS errors
198    /// - Timeouts
199    /// - Invalid response
200    fn execute_streaming(
201        &self,
202        request: Request<Bytes>,
203    ) -> impl Future<Output = Result<crate::response::streaming::StreamingResponse>> + Send;
204}