roas_arazzo_executor/
http.rs1use std::borrow::Cow;
13use std::future::Future;
14use std::pin::Pin;
15use std::time::Duration;
16
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct HttpRequest {
24 pub method: String,
26 pub url: String,
28 pub headers: Vec<(String, String)>,
30 pub body: Option<Vec<u8>>,
32 pub timeout: Option<Duration>,
34}
35
36impl HttpRequest {
37 #[must_use]
39 pub fn header(&self, name: &str) -> Option<&str> {
40 header(&self.headers, name)
41 }
42
43 #[must_use]
45 pub fn text(&self) -> Cow<'_, str> {
46 match &self.body {
47 Some(body) => String::from_utf8_lossy(body),
48 None => Cow::Borrowed(""),
49 }
50 }
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
55#[non_exhaustive]
56pub struct HttpResponse {
57 pub status: u16,
59 pub headers: Vec<(String, String)>,
61 pub body: Vec<u8>,
63}
64
65impl HttpResponse {
66 #[must_use]
68 pub fn json(status: u16, body: &serde_json::Value) -> Self {
69 Self {
70 status,
71 headers: vec![("content-type".to_owned(), "application/json".to_owned())],
72 body: body.to_string().into_bytes(),
73 }
74 }
75
76 #[must_use]
78 pub fn header(&self, name: &str) -> Option<&str> {
79 header(&self.headers, name)
80 }
81
82 #[must_use]
84 pub fn text(&self) -> Cow<'_, str> {
85 String::from_utf8_lossy(&self.body)
86 }
87
88 #[must_use]
93 pub fn body_as_json(&self) -> Option<serde_json::Value> {
94 serde_json::from_slice(&self.body).ok()
95 }
96}
97
98fn header<'h>(headers: &'h [(String, String)], name: &str) -> Option<&'h str> {
99 headers
100 .iter()
101 .find(|(header, _)| header.eq_ignore_ascii_case(name))
102 .map(|(_, value)| value.as_str())
103}
104
105#[derive(Debug, thiserror::Error)]
110#[error("{0}")]
111pub struct ClientError(pub String);
112
113impl ClientError {
114 pub fn new(error: impl std::fmt::Display) -> Self {
116 Self(error.to_string())
117 }
118}
119
120pub trait HttpClient {
122 fn send(&mut self, request: &HttpRequest) -> Result<HttpResponse, ClientError>;
129}
130
131pub type SendFuture<'a> =
133 Pin<Box<dyn Future<Output = Result<HttpResponse, ClientError>> + Send + 'a>>;
134
135pub type SleepFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
137
138pub trait AsyncHttpClient {
140 fn send<'a>(&'a mut self, request: &'a HttpRequest) -> SendFuture<'a>;
146
147 fn sleep(&self, duration: Duration) -> SleepFuture<'_>;
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use serde_json::json;
159
160 #[test]
161 fn a_header_is_found_whatever_case_it_was_written_in() {
162 let request = HttpRequest {
163 headers: vec![("Content-Type".to_owned(), "application/json".to_owned())],
164 ..HttpRequest::default()
165 };
166 assert_eq!(request.header("content-type"), Some("application/json"));
167 assert_eq!(request.header("CONTENT-TYPE"), Some("application/json"));
168 assert_eq!(request.header("accept"), None);
169 }
170
171 #[test]
172 fn a_json_response_carries_its_body_both_ways() {
173 let response = HttpResponse::json(201, &json!({ "id": 7 }));
174 assert_eq!(response.status, 201);
175 assert_eq!(response.header("Content-Type"), Some("application/json"));
176 assert_eq!(response.body_as_json(), Some(json!({ "id": 7 })));
177 assert_eq!(response.text(), r#"{"id":7}"#);
178 }
179
180 #[test]
181 fn a_body_that_is_not_json_is_still_text() {
182 let response = HttpResponse {
183 status: 200,
184 headers: Vec::new(),
185 body: b"not json".to_vec(),
186 };
187 assert_eq!(response.body_as_json(), None);
188 assert_eq!(response.text(), "not json");
189 }
190
191 #[test]
192 fn a_request_without_a_body_reads_as_empty() {
193 assert_eq!(HttpRequest::default().text(), "");
194 }
195
196 #[test]
197 fn a_client_error_says_what_it_was_told() {
198 let error = ClientError::new(std::io::Error::other("connection reset"));
199 assert_eq!(error.to_string(), "connection reset");
200 }
201}