1use std::{collections::BTreeMap, future::Future, pin::Pin};
4
5use serde::{Deserialize, Serialize};
6
7pub const RECEIPT_HEADERS: [&str; 7] = [
9 "cache-control",
10 "content-encoding",
11 "content-length",
12 "content-type",
13 "etag",
14 "last-modified",
15 "location",
16];
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct TransportRequest {
21 pub url: String,
22 pub method: String,
23 pub headers: BTreeMap<String, String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "camelCase")]
29pub struct TransportDiagnostics {
30 pub outcome: String,
31}
32
33impl TransportDiagnostics {
34 #[must_use]
35 pub fn response() -> Self {
36 Self {
37 outcome: "response".to_string(),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "camelCase")]
45pub struct ResponseReceipt {
46 pub body: Vec<u8>,
47 pub final_url: String,
48 pub status: u16,
49 pub headers: BTreeMap<String, String>,
50 pub diagnostics: TransportDiagnostics,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, thiserror::Error)]
55#[error("{message}")]
56#[serde(rename_all = "camelCase")]
57pub struct TransportError {
58 pub kind: String,
59 pub message: String,
60 pub source_url: String,
61}
62
63pub type TransportFuture<'a> =
64 Pin<Box<dyn Future<Output = std::result::Result<ResponseReceipt, TransportError>> + Send + 'a>>;
65
66pub trait Transport: Send + Sync {
71 fn execute(&self, request: TransportRequest) -> TransportFuture<'_>;
72}
73
74impl<F, Fut> Transport for F
75where
76 F: Fn(TransportRequest) -> Fut + Send + Sync,
77 Fut: Future<Output = std::result::Result<ResponseReceipt, TransportError>> + Send + 'static,
78{
79 fn execute(&self, request: TransportRequest) -> TransportFuture<'_> {
80 Box::pin(self(request))
81 }
82}
83
84#[cfg(feature = "runtime")]
86#[derive(Debug, Clone)]
87pub struct ReqwestTransport {
88 client: reqwest::Client,
89}
90
91#[cfg(feature = "runtime")]
92impl ReqwestTransport {
93 #[must_use]
94 pub const fn new(client: reqwest::Client) -> Self {
95 Self { client }
96 }
97}
98
99#[cfg(feature = "runtime")]
100impl Default for ReqwestTransport {
101 fn default() -> Self {
102 Self::new(reqwest::Client::new())
103 }
104}
105
106#[cfg(feature = "runtime")]
107impl Transport for ReqwestTransport {
108 fn execute(&self, request: TransportRequest) -> TransportFuture<'_> {
109 Box::pin(async move {
110 let method =
111 reqwest::Method::from_bytes(request.method.as_bytes()).map_err(|error| {
112 TransportError {
113 kind: "invalid_request".to_string(),
114 message: error.to_string(),
115 source_url: request.url.clone(),
116 }
117 })?;
118 let mut builder = self.client.request(method, &request.url);
119 for (name, value) in &request.headers {
120 builder = builder.header(name, value);
121 }
122 let response = builder.send().await.map_err(|error| TransportError {
123 kind: if error.is_timeout() {
124 "timeout"
125 } else if error.is_connect() {
126 "connect"
127 } else {
128 "transport"
129 }
130 .to_string(),
131 message: error.to_string(),
132 source_url: request.url.clone(),
133 })?;
134 let status = response.status().as_u16();
135 let final_url = response.url().to_string();
136 let headers = RECEIPT_HEADERS
137 .iter()
138 .filter_map(|name| {
139 response
140 .headers()
141 .get(*name)
142 .and_then(|value| value.to_str().ok())
143 .map(|value| ((*name).to_string(), value.to_string()))
144 })
145 .collect();
146 let body = response
147 .bytes()
148 .await
149 .map_err(|error| TransportError {
150 kind: "body".to_string(),
151 message: error.to_string(),
152 source_url: request.url,
153 })?
154 .to_vec();
155 Ok(ResponseReceipt {
156 body,
157 final_url,
158 status,
159 headers,
160 diagnostics: TransportDiagnostics::response(),
161 })
162 })
163 }
164}
165
166pub async fn capture_response_with_transport(
168 request: TransportRequest,
169 transport: &dyn Transport,
170) -> std::result::Result<ResponseReceipt, TransportError> {
171 transport.execute(request).await
172}
173
174#[cfg(feature = "runtime")]
176pub async fn capture_response(
177 request: TransportRequest,
178) -> std::result::Result<ResponseReceipt, TransportError> {
179 capture_response_with_transport(request, &ReqwestTransport::default()).await
180}