1use std::future::{Future, ready};
2use std::sync::Arc;
3
4use spire_core::backend::{Backend, Session, StatusPolicy};
5use spire_core::http::{Request, Response};
6use spire_core::identity::Identity;
7use spire_core::{Error, ErrorKind, Result};
8
9use crate::clients::Clients;
10use crate::conversion::{head_from_reqwest, request_to_reqwest};
11
12#[derive(Debug, Clone, Default)]
41pub struct ReqwestBackend {
42 clients: Clients,
43 status_policy: StatusPolicy,
44}
45
46impl ReqwestBackend {
47 #[must_use]
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn from_builder<F>(builder: F) -> Result<Self>
72 where
73 F: Fn() -> reqwest::ClientBuilder + Send + Sync + 'static,
74 {
75 Ok(Self {
76 clients: Clients::new(Arc::new(builder))?,
77 status_policy: StatusPolicy::default(),
78 })
79 }
80
81 #[must_use]
83 pub const fn with_status_policy(mut self, policy: StatusPolicy) -> Self {
84 self.status_policy = policy;
85 self
86 }
87
88 #[must_use]
90 pub const fn as_reqwest(&self) -> &reqwest::Client {
91 self.clients.direct()
92 }
93
94 #[must_use]
96 pub const fn status_policy(&self) -> StatusPolicy {
97 self.status_policy
98 }
99}
100
101impl Backend for ReqwestBackend {
102 type Session = ReqwestSession;
103
104 fn connect(&self) -> impl Future<Output = Result<Self::Session>> + Send {
105 ready(Ok(self.session(self.clients.direct().clone())))
106 }
107
108 fn connect_as(
111 &self,
112 identity: &Identity,
113 ) -> impl Future<Output = Result<Self::Session>> + Send {
114 ready(self.clients.of(identity).map(|client| self.session(client)))
115 }
116}
117
118impl ReqwestBackend {
119 fn session(&self, client: reqwest::Client) -> ReqwestSession {
120 ReqwestSession {
121 http: self.clone(),
122 client,
123 }
124 }
125}
126
127#[derive(Debug)]
130pub struct ReqwestSession {
131 http: ReqwestBackend,
132 client: reqwest::Client,
134}
135
136impl ReqwestSession {
137 #[must_use]
139 pub const fn http(&self) -> &ReqwestBackend {
140 &self.http
141 }
142}
143
144impl Session for ReqwestSession {
145 async fn fetch(&self, request: &Request) -> Result<Response> {
146 let uri = request.uri().clone();
147 let failed = |error: Error| error.with_uri(uri.clone());
148 let converted = request_to_reqwest(request).map_err(failed)?;
149 let response = self
150 .client
151 .execute(converted)
152 .await
153 .map_err(|error| failed(classify(error)))?;
154
155 let head = self
157 .http
158 .status_policy
159 .check(head_from_reqwest(&response))?;
160 let body = response
161 .bytes()
162 .await
163 .map_err(|error| failed(classify(error)))?;
164 Ok(head.map(|()| body))
165 }
166}
167
168fn classify(error: reqwest::Error) -> Error {
171 let kind = if error.is_timeout() {
172 ErrorKind::Timeout
173 } else if error.is_builder() {
174 ErrorKind::Invalid
175 } else if error.is_redirect() {
176 ErrorKind::Rejected
178 } else {
179 ErrorKind::Http
181 };
182 Error::new(kind).with_source(error)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[tokio::test]
190 async fn invalid_request_is_rejected_before_sending() {
191 let session = ReqwestBackend::new().connect().await.unwrap();
192 let error = session.fetch(&Request::default()).await.unwrap_err();
193 assert_eq!(error.kind(), ErrorKind::Invalid);
194 }
195
196 #[tokio::test]
197 async fn sessions_share_the_client() {
198 let backend = ReqwestBackend::new().with_status_policy(StatusPolicy::Accept);
199 let session = backend.connect().await.unwrap();
200 assert_eq!(session.http().status_policy(), StatusPolicy::Accept);
201 }
202}