bestool_canopy/transport.rs
1//! The HTTP layer underneath [`CanopyClient`](crate::CanopyClient).
2//!
3//! Everything above this layer — the generated wire types, the per-endpoint
4//! methods, gzipping, status handling, JSON parsing — is transport-agnostic. A
5//! caller that can't (or doesn't want to) reach canopy over
6//! [`ReqwestTransport`](crate::ReqwestTransport), the default, implements
7//! [`CanopyTransport`] and keeps the whole typed interface on top of it.
8
9use std::sync::Arc;
10
11use bytes::Bytes;
12use miette::Result;
13
14/// A request built by [`CanopyClient`](crate::CanopyClient), ready for a
15/// [`CanopyTransport`] to send.
16///
17/// The URI is the endpoint **path** in origin form (path plus query, no scheme
18/// or authority), e.g. `/backup-target` — resolving it against a base URL is
19/// the transport's job. The body is already serialised and gzipped when there is
20/// one (with `content-type` and `content-encoding` set to match) and empty when
21/// there isn't.
22pub type CanopyRequest = http::Request<Bytes>;
23
24/// A response handed back to [`CanopyClient`](crate::CanopyClient) by a
25/// [`CanopyTransport`], with its body buffered.
26///
27/// The status is interpreted by the client: a non-2xx becomes a
28/// [`CanopyHttpError`](crate::CanopyHttpError) carrying the body, and a success
29/// has its body parsed into the endpoint's response type.
30pub type CanopyResponse = http::Response<Bytes>;
31
32/// The HTTP transport a [`CanopyClient`](crate::CanopyClient) sends through.
33///
34/// Implement this to route canopy calls somewhere of your own choosing — a
35/// proxy that isn't a plain HTTP proxy, an in-process handler, a recorded
36/// fixture in tests — and pass it to
37/// [`CanopyClient::with_transport`](crate::CanopyClient::with_transport). The
38/// generated per-endpoint methods, the wire types, and the error handling all
39/// work unchanged on top; callers who don't need this get
40/// [`ReqwestTransport`](crate::ReqwestTransport) and never see this trait.
41///
42/// # Contract
43///
44/// - Requests arrive with a path-only URI (see [`CanopyRequest`]); the transport
45/// decides what host, scheme, and auth to use, and may rewrite the path (the
46/// default transport prefixes `/public` when it goes over the tailnet).
47/// - Return canopy's response as-is, non-2xx included: statuses are the client's
48/// to interpret, since endpoints give meaning to specific codes (e.g. a
49/// backup-target `412` means the device is dormant, see
50/// [`TargetOutcome::from_result`](crate::TargetOutcome::from_result)).
51/// - `Err` is for a failure to obtain any response at all (connect, timeout,
52/// protocol error).
53///
54/// # Example
55///
56/// ```no_run
57/// use bestool_canopy::{
58/// CanopyClient, CanopyRequest, CanopyResponse, CanopyTransport, async_trait,
59/// };
60/// use miette::Result;
61///
62/// struct MyProxy;
63///
64/// #[async_trait]
65/// impl CanopyTransport for MyProxy {
66/// async fn call(&self, request: CanopyRequest) -> Result<CanopyResponse> {
67/// // Hand `request` to whatever reaches canopy from here, and return
68/// // what comes back.
69/// todo!()
70/// }
71/// }
72///
73/// # async fn example() -> Result<()> {
74/// let client = CanopyClient::with_transport(MyProxy);
75/// let servers = client.servers().await?;
76/// # Ok(())
77/// # }
78/// ```
79#[async_trait::async_trait]
80pub trait CanopyTransport: Send + Sync {
81 /// Send `request` and return canopy's response.
82 async fn call(&self, request: CanopyRequest) -> Result<CanopyResponse>;
83}
84
85#[async_trait::async_trait]
86impl<T: CanopyTransport + ?Sized> CanopyTransport for Arc<T> {
87 async fn call(&self, request: CanopyRequest) -> Result<CanopyResponse> {
88 (**self).call(request).await
89 }
90}
91
92#[async_trait::async_trait]
93impl<T: CanopyTransport + ?Sized> CanopyTransport for Box<T> {
94 async fn call(&self, request: CanopyRequest) -> Result<CanopyResponse> {
95 (**self).call(request).await
96 }
97}