Skip to main content

hclient_core/unversioned/
erased.rs

1//! Type-erased forms of [`crate::unversioned::Transport`] and [`Timer`], so
2//! a facade can be **one concrete type** instead of two type parameters.
3//!
4//! A backend implements nothing here: [`BoxedTransport`] and [`BoxedTimer`]
5//! have blanket impls over every `Transport` and every `Timer`.
6//!
7//! # Nothing boxed here declares `Send`
8//!
9//! The boxed future, body, sleep and instant carry no auto trait, and that
10//! is what makes the blanket impls possible: proving
11//! `Transport::execute`'s RPITIT `Send` for a generic `T` needs return type
12//! notation, unstable as of rustc 1.98. Following the bound down to where
13//! it *can* be proven would put it on seven seam methods, which excludes a
14//! single-threaded runtime such as `hclient-rt-embassy`, whose `connect`
15//! future holds a `RefCell` and always will.
16//!
17//! The consequence a caller meets: **nothing a request produces is
18//! `Send`** — not the future, not the response body. One `BoxBody` serves
19//! every backend, and a browser's body holds a `dyn Stream` with no auto
20//! trait, so declaring `Send` would exclude that backend rather than weaken
21//! it.
22//!
23//! # Where `Send + Sync` does appear
24//!
25//! [`SharedTransport`] and [`SharedTimer`], which a facade writes at its
26//! own use site to hold these behind an `Arc` and cross a `tokio::spawn`.
27//! A backend that cannot satisfy the bound is **refused at the
28//! constructor** — a compile error at the line that asked — rather than
29//! taxed at the seam. `hclient-rt-embassy` is that backend: `RefCell`
30//! throughout, because embassy's executor is single-threaded, so an
31//! embedded caller uses `Transport` directly rather than a facade.
32//!
33//! # The instant is erased as a question, not as a type
34//!
35//! [`Timer::Instant`] is `Copy + PartialOrd`, and `Copy` on a trait object
36//! is not a thing. [`ErasedInstant`] answers the one question a client asks
37//! of a stamp — *how long ago was this* — so the instant stays inside the
38//! clock that made it and `Copy` is asked of nothing erased.
39
40use crate::Error;
41use crate::RequestBody;
42use crate::unversioned::Timer;
43use bytes::Bytes;
44use std::future::Future;
45use std::pin::Pin;
46use std::task::{Context, Poll};
47use std::time::Duration;
48
49/// A response body with its type erased, as an erased transport hands back.
50///
51/// **Not `Send`**, so it cannot cross a `tokio::spawn`. One `BoxBody`
52/// serves every backend and a browser's body holds a `dyn Stream` with no
53/// auto trait, so the bound would exclude that backend rather than weaken
54/// it. A caller who needs a spawnable body reaches past the facade for the
55/// concrete transport's own body type.
56pub type BoxBody = Pin<Box<dyn http_body::Body<Data = Bytes, Error = Error>>>;
57
58/// An erased exchange, as [`BoxedTransport`] hands one back.
59pub type BoxExchange<'a> =
60    Pin<Box<dyn Future<Output = Result<http::Response<BoxBody>, Error>> + 'a>>;
61
62/// An erased sleep, as [`BoxedTimer`] hands one back.
63///
64/// Not `Send`, for [`BoxBody`]'s reason and inseparably from it: a response
65/// body holds a sleep — that is how a total timeout cuts a silent body — so
66/// the two answer the same question.
67pub type BoxSleep = Pin<Box<dyn Future<Output = ()>>>;
68
69/// Erase a body, mapping its error into [`Error`] on the way.
70///
71/// Written here rather than taken from `http-body-util`: `hclient-core`
72/// depends on `http-body` and not on the util crate, and this is a dozen
73/// lines against a dependency every backend would then carry.
74pub fn box_body<B>(body: B) -> BoxBody
75where
76    B: http_body::Body<Data = Bytes> + 'static,
77    B::Error: Into<Error>,
78{
79    Box::pin(MapErr(Box::pin(body)))
80}
81
82/// The inner body is held **already pinned**, so this needs no projection
83/// and therefore no `unsafe` — `hclient-core` is `#![forbid(unsafe_code)]`,
84/// and a newtype that has to project is how that gets quietly broken. The
85/// cost is one allocation, on a path that is boxing anyway.
86struct MapErr<B>(Pin<Box<B>>);
87
88impl<B> http_body::Body for MapErr<B>
89where
90    B: http_body::Body<Data = Bytes>,
91    B::Error: Into<Error>,
92{
93    type Data = Bytes;
94    type Error = Error;
95
96    fn poll_frame(
97        mut self: Pin<&mut Self>,
98        cx: &mut Context<'_>,
99    ) -> Poll<Option<Result<http_body::Frame<Bytes>, Error>>> {
100        self.0
101            .as_mut()
102            .poll_frame(cx)
103            .map(|o| o.map(|r| r.map_err(Into::into)))
104    }
105
106    fn is_end_stream(&self) -> bool {
107        self.0.is_end_stream()
108    }
109
110    fn size_hint(&self) -> http_body::SizeHint {
111        self.0.size_hint()
112    }
113}
114
115/// [`crate::unversioned::Transport`], with the future and the body boxed.
116///
117/// Implemented for every `Transport` whose error and body error convert
118/// into [`Error`], which is every backend in this workspace. A backend
119/// author writes nothing.
120pub trait BoxedTransport {
121    /// [`crate::unversioned::Transport::execute`], boxed.
122    fn execute_boxed<'a>(&'a self, req: http::Request<RequestBody>) -> BoxExchange<'a>;
123
124    /// [`crate::unversioned::Transport::capabilities`], unchanged — it was
125    /// never generic.
126    fn capabilities(&self) -> &crate::Capabilities;
127
128    /// The transport as [`std::any::Any`], so a caller can ask for its
129    /// concrete type back.
130    ///
131    /// Erasure is what makes a facade one type rather than two parameters,
132    /// and the price is exactly this: the type is gone. A caller who needs
133    /// it back — to inspect a mock's recorded requests, or to lend a
134    /// `Native` to a WebSocket connector — downcasts through here, and the
135    /// `Option` is the honest answer, because the client holds whatever
136    /// backend it was built with and nothing checked it against this
137    /// caller's guess.
138    fn as_any(&self) -> &dyn std::any::Any;
139}
140
141impl<T> BoxedTransport for T
142where
143    T: crate::unversioned::Transport + 'static,
144    T::Body: 'static,
145    <T::Body as http_body::Body>::Error: Into<Error>,
146    T::Error: Into<Error>,
147{
148    fn execute_boxed<'a>(&'a self, req: http::Request<RequestBody>) -> BoxExchange<'a> {
149        Box::pin(async move {
150            match crate::unversioned::Transport::execute(self, req).await {
151                Ok(resp) => Ok(resp.map(box_body)),
152                Err(e) => Err(e.into()),
153            }
154        })
155    }
156
157    fn capabilities(&self) -> &crate::Capabilities {
158        crate::unversioned::Transport::capabilities(self)
159    }
160
161    fn as_any(&self) -> &dyn std::any::Any {
162        self
163    }
164}
165
166/// A transport a facade can share between threads, erased.
167///
168/// **The bound lives on this alias rather than at the use sites, and that
169/// is a rule rather than a style.** `cargo fmt` moves a trailing comment
170/// off a line it reflows and deletes one from a `where` clause outright,
171/// so a `send-bound-exception` marker cannot survive on a long signature.
172/// A short named type is a line fmt has no reason to touch, so every use
173/// site writes `Box<SharedTransport>` and carries no marker at all.
174///
175/// The bound is amendment C12's criterion: one this crate chooses so a
176/// caller's value reaches a facade by erasure rather than by a type
177/// parameter, said at the use site and never on the trait. A backend that
178/// cannot satisfy it is refused at a constructor rather than taxed at the
179/// seam.
180pub type SharedTransport = dyn BoxedTransport + Send + Sync; // send-bound-exception: amendment-C12
181
182/// A moment a [`BoxedTimer`] recorded, which can be asked how long ago it
183/// was and nothing else.
184///
185/// One method on purpose: it is what lets an erased clock exist at all.
186/// See this module's own doc.
187pub trait ErasedInstant {
188    /// How long since this stamp was taken, on the clock that took it.
189    fn elapsed(&self) -> Duration;
190}
191
192/// A stamp a [`BoxedTimer`] took, erased.
193///
194/// Not `Send`, for [`BoxSleep`]'s reason: the same body holds the stamp the
195/// sleep was computed from.
196pub type BoxInstant = Box<dyn ErasedInstant>;
197
198/// [`Timer`], with the sleep boxed and the instant behind [`ErasedInstant`].
199pub trait BoxedTimer {
200    /// [`Timer::now`], as a stamp that outlives the borrow.
201    fn now_boxed(&self) -> BoxInstant;
202
203    /// [`Timer::sleep`], boxed.
204    fn sleep_boxed(&self, d: Duration) -> BoxSleep;
205}
206
207/// A clock a facade can share between threads, erased.
208///
209/// [`SharedTransport`]'s reasoning, for the other seam.
210pub type SharedTimer = dyn BoxedTimer + Send + Sync; // send-bound-exception: amendment-C12
211
212/// The stamp the blanket [`BoxedTimer`] hands out: the clock and the moment
213/// together, so `elapsed` is answered by the clock that took it.
214struct Stamp<Tm: Timer> {
215    timer: Tm,
216    at: Tm::Instant,
217}
218
219impl<Tm: Timer> ErasedInstant for Stamp<Tm> {
220    fn elapsed(&self) -> Duration {
221        self.timer.elapsed_since(self.at)
222    }
223}
224
225impl<Tm> BoxedTimer for Tm
226where
227    Tm: Timer + Clone + 'static,
228    Tm::Sleep: 'static,
229{
230    fn now_boxed(&self) -> BoxInstant {
231        Box::new(Stamp {
232            timer: self.clone(),
233            at: self.now(),
234        })
235    }
236
237    fn sleep_boxed(&self, d: Duration) -> BoxSleep {
238        Box::pin(self.sleep(d))
239    }
240}