Skip to main content

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}