1use async_trait::async_trait;
8use derive_builder::Builder;
9use serde::{Deserialize, Serialize};
10
11use crate::HttpError;
12
13#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
14pub struct HttpHeader {
15 pub name: String,
16 pub value: String,
17}
18
19#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, Builder)]
20#[builder(
21 custom_constructor,
22 build_fn(private, name = "fallible_build"),
23 setter(into)
24)]
25pub struct HttpRequest {
26 pub method: String,
27 pub url: String,
28 #[builder(setter(custom))]
29 pub headers: Vec<HttpHeader>,
30 #[serde(with = "serde_bytes")]
31 pub body: Vec<u8>,
32}
33
34impl std::fmt::Debug for HttpRequest {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 let body_repr = if let Ok(s) = std::str::from_utf8(&self.body) {
37 if s.len() < 50 {
38 format!("\"{s}\"")
39 } else {
40 format!("\"{}\"...", s.chars().take(50).collect::<String>())
41 }
42 } else {
43 format!("<binary data - {} bytes>", self.body.len())
44 };
45 let mut builder = f.debug_struct("HttpRequest");
46 builder
47 .field("method", &self.method)
48 .field("url", &self.url);
49 if !self.headers.is_empty() {
50 builder.field("headers", &self.headers);
51 };
52 builder
53 .field("body", &format_args!("{}", body_repr))
54 .finish()
55 }
56}
57
58macro_rules! http_method {
59 ($name:ident, $method:expr) => {
60 pub fn $name(url: impl Into<String>) -> HttpRequestBuilder {
61 HttpRequestBuilder {
62 method: Some($method.to_string()),
63 url: Some(url.into()),
64 headers: Some(vec![]),
65 body: Some(vec![]),
66 }
67 }
68 };
69}
70
71impl HttpRequest {
72 http_method!(get, "GET");
73 http_method!(put, "PUT");
74 http_method!(delete, "DELETE");
75 http_method!(post, "POST");
76 http_method!(patch, "PATCH");
77 http_method!(head, "HEAD");
78 http_method!(options, "OPTIONS");
79}
80
81impl HttpRequestBuilder {
82 pub fn header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
83 self.headers.get_or_insert_with(Vec::new).push(HttpHeader {
84 name: name.into(),
85 value: value.into(),
86 });
87 self
88 }
89
90 pub fn json(&mut self, body: impl serde::Serialize) -> &mut Self {
91 self.body = Some(serde_json::to_vec(&body).unwrap());
92 self
93 }
94
95 pub fn build(&self) -> HttpRequest {
96 self.fallible_build()
97 .expect("All required fields were initialized")
98 }
99}
100
101#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Builder)]
102#[builder(
103 custom_constructor,
104 build_fn(private, name = "fallible_build"),
105 setter(into)
106)]
107pub struct HttpResponse {
108 pub status: u16, #[builder(setter(custom))]
110 pub headers: Vec<HttpHeader>,
111 #[serde(with = "serde_bytes")]
112 pub body: Vec<u8>,
113}
114
115impl HttpResponse {
116 pub fn status(status: u16) -> HttpResponseBuilder {
117 HttpResponseBuilder {
118 status: Some(status),
119 headers: Some(vec![]),
120 body: Some(vec![]),
121 }
122 }
123 pub fn ok() -> HttpResponseBuilder {
124 Self::status(200)
125 }
126}
127
128impl HttpResponseBuilder {
129 pub fn header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
130 self.headers.get_or_insert_with(Vec::new).push(HttpHeader {
131 name: name.into(),
132 value: value.into(),
133 });
134 self
135 }
136
137 pub fn json(&mut self, body: impl serde::Serialize) -> &mut Self {
138 self.body = Some(serde_json::to_vec(&body).unwrap());
139 self
140 }
141
142 pub fn build(&self) -> HttpResponse {
143 self.fallible_build()
144 .expect("All required fields were initialized")
145 }
146}
147
148#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
149pub enum HttpResult {
150 Ok(HttpResponse),
151 Err(HttpError),
152}
153
154impl From<crate::Result<HttpResponse>> for HttpResult {
155 fn from(result: Result<HttpResponse, HttpError>) -> Self {
156 match result {
157 Ok(response) => HttpResult::Ok(response),
158 Err(err) => HttpResult::Err(err),
159 }
160 }
161}
162
163impl crux_core::capability::Operation for HttpRequest {
164 type Output = HttpResult;
165
166 #[cfg(feature = "typegen")]
167 fn register_types(generator: &mut crux_core::typegen::TypeGen) -> crux_core::typegen::Result {
168 generator.register_type::<HttpError>()?;
169 generator.register_type::<Self>()?;
170 generator.register_type::<Self::Output>()?;
171 Ok(())
172 }
173}
174
175#[async_trait]
176pub(crate) trait EffectSender {
177 async fn send(&self, effect: HttpRequest) -> HttpResult;
178}
179
180#[async_trait]
181impl<Ev> EffectSender for crux_core::capability::CapabilityContext<HttpRequest, Ev>
182where
183 Ev: 'static,
184{
185 async fn send(&self, effect: HttpRequest) -> HttpResult {
186 crux_core::capability::CapabilityContext::request_from_shell(self, effect).await
187 }
188}
189
190#[async_trait]
191pub(crate) trait ProtocolRequestBuilder {
192 async fn into_protocol_request(mut self) -> crate::Result<HttpRequest>;
193}
194
195#[async_trait]
196impl ProtocolRequestBuilder for crate::Request {
197 async fn into_protocol_request(mut self) -> crate::Result<HttpRequest> {
198 let body = if self.is_empty() == Some(false) {
199 self.take_body().into_bytes().await?
200 } else {
201 vec![]
202 };
203
204 Ok(HttpRequest {
205 method: self.method().to_string(),
206 url: self.url().to_string(),
207 headers: self
208 .iter()
209 .flat_map(|(name, values)| {
210 values.iter().map(|value| HttpHeader {
211 name: name.to_string(),
212 value: value.to_string(),
213 })
214 })
215 .collect(),
216 body,
217 })
218 }
219}
220
221impl From<HttpResponse> for crate::ResponseAsync {
222 fn from(effect_response: HttpResponse) -> Self {
223 let mut res = http_types::Response::new(effect_response.status);
224 res.set_body(effect_response.body);
225 for header in effect_response.headers {
226 res.append_header(header.name.as_str(), header.value);
227 }
228
229 crate::ResponseAsync::new(res)
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_http_request_get() {
239 let req = HttpRequest::get("https://example.com").build();
240
241 assert_eq!(
242 req,
243 HttpRequest {
244 method: "GET".to_string(),
245 url: "https://example.com".to_string(),
246 ..Default::default()
247 }
248 );
249 }
250
251 #[test]
252 fn test_http_request_get_with_fields() {
253 let req = HttpRequest::get("https://example.com")
254 .header("foo", "bar")
255 .body("123")
256 .build();
257
258 assert_eq!(
259 req,
260 HttpRequest {
261 method: "GET".to_string(),
262 url: "https://example.com".to_string(),
263 headers: vec![HttpHeader {
264 name: "foo".to_string(),
265 value: "bar".to_string(),
266 }],
267 body: "123".as_bytes().to_vec(),
268 }
269 );
270 }
271
272 #[test]
273 fn test_http_response_status() {
274 let req = HttpResponse::status(302).build();
275
276 assert_eq!(
277 req,
278 HttpResponse {
279 status: 302,
280 ..Default::default()
281 }
282 );
283 }
284
285 #[test]
286 fn test_http_response_status_with_fields() {
287 let req = HttpResponse::status(302)
288 .header("foo", "bar")
289 .body("hello world")
290 .build();
291
292 assert_eq!(
293 req,
294 HttpResponse {
295 status: 302,
296 headers: vec![HttpHeader {
297 name: "foo".to_string(),
298 value: "bar".to_string(),
299 }],
300 body: "hello world".as_bytes().to_vec(),
301 }
302 );
303 }
304
305 #[test]
306 fn test_http_request_debug_repr() {
307 {
308 let req = HttpRequest::post("http://example.com")
310 .header("foo", "bar")
311 .body("hello world!")
312 .build();
313 let repr = format!("{req:?}");
314 assert_eq!(
315 repr,
316 r#"HttpRequest { method: "POST", url: "http://example.com", headers: [HttpHeader { name: "foo", value: "bar" }], body: "hello world!" }"#
317 );
318 }
319
320 {
321 let req = HttpRequest::post("http://example.com")
323 .body("abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu😀😀😀😀😀😀")
325 .build();
326 let repr = format!("{req:?}");
327 assert_eq!(
328 repr,
329 r#"HttpRequest { method: "POST", url: "http://example.com", body: "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu😀😀"... }"#
330 );
331 }
332
333 {
334 let req = HttpRequest::post("http://example.com")
336 .body(vec![255, 254, 253, 252])
337 .build();
338 let repr = format!("{req:?}");
339 assert_eq!(
340 repr,
341 r#"HttpRequest { method: "POST", url: "http://example.com", body: <binary data - 4 bytes> }"#
342 );
343 }
344 }
345}