typed_openapi/transport.rs
1//! The one seam between a decided request and the network.
2//!
3//! The vocabulary is `http::Request<Vec<u8>>` in, `http::Response<Vec<u8>>`
4//! out, which is what every client-agnostic Rust crate converges on (oauth2,
5//! rustify, atrium-xrpc, kube-core). There is no de-facto *trait*, so this
6//! crate defines its own two — a sync one and an async one, because a single
7//! trait cannot be both and `maybe-async` makes the flavour a global switch.
8//!
9//! This crate ships no adapter and depends on no HTTP client, in any feature
10//! combination: an adopter's choice of client is theirs, and a crate that
11//! pinned one would make it everyone's. `examples/toy/cli/src/client.rs` has
12//! both adapters in full — a ureq 3 agent in nine lines, a `reqwest::Client`
13//! in twelve — written to be copied rather than depended on.
14//!
15//! An adapter in an adopter's crate wraps the client in a newtype, because
16//! [`SyncClient`] and the client are both foreign there. A crate that owns
17//! either one writes `impl SyncClient for ureq::Agent` directly.
18//!
19//! No adapter may turn a status code into an error: the status belongs to the
20//! layer above, which needs the body that came with it. ureq does this by
21//! default and must be built with `http_status_as_error(false)`.
22
23use std::collections::VecDeque;
24use std::convert::Infallible;
25use std::sync::{Mutex, PoisonError};
26
27use http::{Request, Response, StatusCode, header};
28
29/// What every adapter takes.
30pub type HttpRequest = Request<Vec<u8>>;
31/// What every adapter returns.
32pub type HttpResponse = Response<Vec<u8>>;
33
34/// Something that can send a request and wait for the answer.
35pub trait SyncClient {
36 type Error: std::error::Error + Send + Sync + 'static;
37
38 fn send(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error>;
39}
40
41/// The same, for a caller inside a runtime. `Send` on the future so that a
42/// call can be `tokio::spawn`ed.
43pub trait AsyncClient {
44 type Error: std::error::Error + Send + Sync + 'static;
45
46 fn send(
47 &self,
48 request: HttpRequest,
49 ) -> impl Future<Output = Result<HttpResponse, Self::Error>> + Send;
50}
51
52/// A client that sends nothing, answers from a script, and keeps every request
53/// it was given.
54///
55/// It is both a [`SyncClient`] and an [`AsyncClient`], so one fixture tests
56/// both call paths, and because it *answers* it can stand in for the server
57/// through a multi-step chain — no socket, no runtime, no fixture server.
58#[derive(Debug, Default)]
59pub struct Recorder {
60 answers: Mutex<VecDeque<HttpResponse>>,
61 sent: Mutex<Vec<HttpRequest>>,
62}
63
64impl Recorder {
65 /// A recorder with an empty script: every request is answered `200 {}`.
66 #[must_use]
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 /// Append one scripted answer. They are used in order; once the script runs
72 /// out, `200 {}` is the answer.
73 #[must_use]
74 pub fn answering(self, status: StatusCode, body: &serde_json::Value) -> Self {
75 self.answers
76 .lock()
77 .unwrap_or_else(PoisonError::into_inner)
78 .push_back(json_response(status, body));
79 self
80 }
81
82 /// Every request sent so far, oldest first, and the recorder is left empty.
83 pub fn take(&self) -> Vec<HttpRequest> {
84 std::mem::take(&mut self.sent.lock().unwrap_or_else(PoisonError::into_inner))
85 }
86
87 fn answer(&self, request: HttpRequest) -> HttpResponse {
88 self.sent
89 .lock()
90 .unwrap_or_else(PoisonError::into_inner)
91 .push(request);
92 self.answers
93 .lock()
94 .unwrap_or_else(PoisonError::into_inner)
95 .pop_front()
96 .unwrap_or_else(|| json_response(StatusCode::OK, &serde_json::json!({})))
97 }
98}
99
100impl SyncClient for Recorder {
101 type Error = Infallible;
102
103 fn send(&self, request: HttpRequest) -> Result<HttpResponse, Infallible> {
104 Ok(self.answer(request))
105 }
106}
107
108impl AsyncClient for Recorder {
109 type Error = Infallible;
110
111 fn send(
112 &self,
113 request: HttpRequest,
114 ) -> impl Future<Output = Result<HttpResponse, Infallible>> + Send {
115 std::future::ready(Ok(self.answer(request)))
116 }
117}
118
119fn json_response(status: StatusCode, body: &serde_json::Value) -> HttpResponse {
120 let mut response = Response::new(body.to_string().into_bytes());
121 *response.status_mut() = status;
122 response.headers_mut().insert(
123 header::CONTENT_TYPE,
124 header::HeaderValue::from_static("application/json"),
125 );
126 response
127}