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}