Skip to main content

ferriskey_sdk/
transport.rs

1//! Transport seam and HTTP request/response types for the FerrisKey SDK.
2//!
3//! ## Design Philosophy
4//!
5//! This module leverages `tower::Service` as the foundational abstraction for transport,
6//! enabling composition of middleware layers (retry, timeout, rate-limiting) without
7//! custom framework abstractions. The `Transport` trait is implemented as a blanket
8//! implementation over any `tower::Service<SdkRequest>`, providing maximum flexibility.
9
10use std::{
11    collections::BTreeMap,
12    future::Future,
13    pin::Pin,
14    task::{Context, Poll},
15};
16
17use tower::{Service, ServiceExt};
18
19use crate::error::TransportError;
20
21/// Canonical SDK request passed to a transport implementation.
22///
23/// ## Type Safety
24///
25/// The builder pattern ensures required fields are set at compile time.
26/// Use [`SdkRequest::builder()`] to construct requests with guaranteed validity.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct SdkRequest {
29    /// Raw request body bytes.
30    pub body: Option<Vec<u8>>,
31    /// Whether the request requires bearer authentication.
32    pub requires_auth: bool,
33    /// Header values to attach to the outgoing request.
34    pub headers: BTreeMap<String, String>,
35    /// HTTP method to execute.
36    pub method: String,
37    /// Absolute or relative path for the request.
38    pub path: String,
39}
40
41impl SdkRequest {
42    /// Create a typed builder for constructing requests.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use ferriskey_sdk::SdkRequest;
48    ///
49    /// let request = SdkRequest::builder("GET", "/api/users")
50    ///     .header("accept", "application/json")
51    ///     .auth_required(true)
52    ///     .build();
53    /// ```
54    #[must_use]
55    pub fn builder(
56        method: impl Into<String>,
57        path: impl Into<String>,
58    ) -> SdkRequestBuilder<MethodSet, PathSet> {
59        SdkRequestBuilder {
60            method: method.into(),
61            path: path.into(),
62            body: None,
63            requires_auth: false,
64            headers: BTreeMap::new(),
65            _state: std::marker::PhantomData,
66        }
67    }
68
69    /// Construct a request with an HTTP method and request path.
70    /// Prefer using [`Self::builder()`] for more complex requests.
71    #[must_use]
72    pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
73        Self {
74            body: None,
75            requires_auth: false,
76            headers: BTreeMap::new(),
77            method: method.into(),
78            path: path.into(),
79        }
80    }
81}
82
83// ---------------------------------------------------------------------------
84// TypeState markers for SdkRequestBuilder
85// ---------------------------------------------------------------------------
86
87/// TypeState marker: HTTP method has been set.
88#[derive(Debug, Clone, Copy)]
89pub struct MethodSet;
90
91/// TypeState marker: path has been set.
92#[derive(Debug, Clone, Copy)]
93pub struct PathSet;
94
95/// Typed builder for [`SdkRequest`] with compile-time field validation.
96///
97/// ## Type-State Pattern
98///
99/// The builder uses phantom type parameters to track which required fields
100/// have been set. This prevents calling `.build()` before all required
101/// fields are provided—caught at compile time, not runtime.
102#[derive(Debug)]
103pub struct SdkRequestBuilder<M, P> {
104    method: String,
105    path: String,
106    body: Option<Vec<u8>>,
107    requires_auth: bool,
108    headers: BTreeMap<String, String>,
109    _state: std::marker::PhantomData<(M, P)>,
110}
111
112impl SdkRequestBuilder<MethodSet, PathSet> {
113    /// Build the request. Available only when both method and path are set.
114    #[must_use]
115    pub fn build(self) -> SdkRequest {
116        SdkRequest {
117            method: self.method,
118            path: self.path,
119            body: self.body,
120            requires_auth: self.requires_auth,
121            headers: self.headers,
122        }
123    }
124}
125
126impl<M, P> SdkRequestBuilder<M, P> {
127    /// Set the request body.
128    #[must_use]
129    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
130        self.body = Some(body.into());
131        self
132    }
133
134    /// Mark the request as requiring authentication.
135    #[must_use]
136    pub const fn auth_required(mut self, required: bool) -> Self {
137        self.requires_auth = required;
138        self
139    }
140
141    /// Add a header to the request.
142    #[must_use]
143    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
144        self.headers.insert(name.into(), value.into());
145        self
146    }
147
148    /// Add multiple headers to the request.
149    #[must_use]
150    pub fn headers(mut self, headers: BTreeMap<String, String>) -> Self {
151        self.headers.extend(headers);
152        self
153    }
154}
155
156/// Canonical SDK response returned by a transport implementation.
157#[derive(Clone, Debug, Eq, PartialEq)]
158pub struct SdkResponse {
159    /// Response body bytes.
160    pub body: Vec<u8>,
161    /// Response headers represented as UTF-8 strings when possible.
162    pub headers: BTreeMap<String, String>,
163    /// HTTP status code.
164    pub status: u16,
165}
166
167/// Transport contract using `tower::Service` as the foundation.
168///
169/// ## Design Decision: Blanket Implementation
170///
171/// Rather than defining a custom `Transport` trait, we implement a blanket
172/// `Transport` impl for any type that implements `Service<SdkRequest>`.
173/// This allows seamless integration with the tower ecosystem:
174/// - `tower::retry::Retry` for automatic retries
175/// - `tower::timeout::Timeout` for request timeouts
176/// - `tower::limit::rate::RateLimit` for rate limiting
177/// - Custom middleware via `tower::Layer`
178///
179/// ## Example: Composing Middleware
180///
181/// ```no_run
182/// use std::time::Duration;
183///
184/// use ferriskey_sdk::HpxTransport;
185/// use tower::ServiceBuilder;
186///
187/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
188/// let transport =
189///     ServiceBuilder::new().timeout(Duration::from_secs(30)).service(HpxTransport::default());
190/// # Ok(())
191/// # }
192/// ```
193pub trait Transport:
194    Service<SdkRequest, Response = SdkResponse, Error = TransportError> + Send + Sync
195{
196}
197
198/// Blanket implementation: any Service<SdkRequest> with the right associated types is a Transport.
199impl<T> Transport for T where
200    T: Service<SdkRequest, Response = SdkResponse, Error = TransportError> + Send + Sync
201{
202}
203
204/// Extension trait providing convenience methods for Transport implementors.
205///
206/// ## Why an Extension Trait?
207///
208/// Extension traits allow adding methods to all `Transport` implementors
209/// without modifying the core trait. This follows the Open/Closed Principle
210/// and avoids trait method bloat.
211pub trait TransportExt: Transport {
212    /// Execute a request and return the response, consuming `self` for one-shot use.
213    ///
214    /// This is a convenience wrapper around `tower::ServiceExt::oneshot`.
215    fn execute(
216        &mut self,
217        request: SdkRequest,
218    ) -> impl Future<Output = Result<SdkResponse, TransportError>> + Send;
219}
220
221impl<T> TransportExt for T
222where
223    T: Transport + Clone,
224    <T as Service<SdkRequest>>::Future: Send,
225{
226    async fn execute(&mut self, request: SdkRequest) -> Result<SdkResponse, TransportError> {
227        // Use tower's oneshot for clean single-request execution
228        let transport = self.clone();
229        transport.oneshot(request).await
230    }
231}
232
233/// Primary HTTP transport adapter backed by `hpx`.
234///
235/// ## Usage
236///
237/// `HpxTransport` implements `tower::Service` directly, making it composable
238/// with any tower middleware layer.
239#[derive(Clone)]
240pub struct HpxTransport {
241    client: hpx::Client,
242}
243
244impl std::fmt::Debug for HpxTransport {
245    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        formatter.debug_struct("HpxTransport").finish_non_exhaustive()
247    }
248}
249
250impl Default for HpxTransport {
251    fn default() -> Self {
252        Self::new(hpx::Client::new())
253    }
254}
255
256impl HpxTransport {
257    /// Build a transport from an `hpx` client instance.
258    #[must_use]
259    pub const fn new(client: hpx::Client) -> Self {
260        Self { client }
261    }
262}
263
264/// Implement `tower::Service` for `HpxTransport`.
265///
266/// This makes HpxTransport composable with any tower middleware.
267impl Service<SdkRequest> for HpxTransport {
268    type Response = SdkResponse;
269    type Error = TransportError;
270    type Future = Pin<Box<dyn Future<Output = Result<SdkResponse, TransportError>> + Send>>;
271
272    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
273        // hpx::Client is always ready
274        Poll::Ready(Ok(()))
275    }
276
277    fn call(&mut self, request: SdkRequest) -> Self::Future {
278        let client = self.client.clone();
279        Box::pin(async move {
280            let method = hpx::Method::from_bytes(request.method.as_bytes())
281                .map_err(|_| TransportError::InvalidMethod { method: request.method.clone() })?;
282            let mut builder = client.request(method, request.path);
283
284            for (name, value) in request.headers {
285                builder = builder.header(name, value);
286            }
287
288            if let Some(body) = request.body {
289                builder = builder.body(body);
290            }
291
292            let response = builder.send().await?;
293            let status = response.status().as_u16();
294            let headers = response
295                .headers()
296                .iter()
297                .filter_map(|(name, value)| {
298                    value.to_str().ok().map(|value| (name.to_string(), value.to_string()))
299                })
300                .collect::<BTreeMap<_, _>>();
301            let body = response.bytes().await?.to_vec();
302
303            Ok(SdkResponse { body, headers, status })
304        })
305    }
306}