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.field("body", &format_args!("{body_repr}")).finish()
53 }
54}
55
56macro_rules! http_method {
57 ($name:ident, $method:expr) => {
58 pub fn $name(url: impl Into<String>) -> HttpRequestBuilder {
59 HttpRequestBuilder {
60 method: Some($method.to_string()),
61 url: Some(url.into()),
62 headers: Some(vec![]),
63 body: Some(vec![]),
64 }
65 }
66 };
67}
68
69impl HttpRequest {
70 http_method!(get, "GET");
71 http_method!(put, "PUT");
72 http_method!(delete, "DELETE");
73 http_method!(post, "POST");
74 http_method!(patch, "PATCH");
75 http_method!(head, "HEAD");
76 http_method!(options, "OPTIONS");
77}
78
79impl HttpRequestBuilder {
80 pub fn header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
81 self.headers.get_or_insert_with(Vec::new).push(HttpHeader {
82 name: name.into(),
83 value: value.into(),
84 });
85 self
86 }
87
88 pub fn query(&mut self, query: &impl Serialize) -> crate::Result<&mut Self> {
93 if let Some(url) = &mut self.url {
94 if url.contains('?') {
95 url.push('&');
96 } else {
97 url.push('?');
98 }
99 url.push_str(&serde_qs::to_string(query)?);
100 }
101
102 Ok(self)
103 }
104
105 pub fn json(&mut self, body: impl serde::Serialize) -> &mut Self {
110 self.body = Some(serde_json::to_vec(&body).unwrap());
111 self
112 }
113
114 #[must_use]
119 pub fn build(&self) -> HttpRequest {
120 self.fallible_build()
121 .expect("All required fields were initialized")
122 }
123}
124
125#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Builder)]
126#[builder(
127 custom_constructor,
128 build_fn(private, name = "fallible_build"),
129 setter(into)
130)]
131pub struct HttpResponse {
132 pub status: u16, #[builder(setter(custom))]
134 pub headers: Vec<HttpHeader>,
135 #[serde(with = "serde_bytes")]
136 pub body: Vec<u8>,
137}
138
139impl HttpResponse {
140 #[must_use]
141 pub fn status(status: u16) -> HttpResponseBuilder {
142 HttpResponseBuilder {
143 status: Some(status),
144 headers: Some(vec![]),
145 body: Some(vec![]),
146 }
147 }
148 #[must_use]
149 pub fn ok() -> HttpResponseBuilder {
150 Self::status(200)
151 }
152}
153
154impl HttpResponseBuilder {
155 pub fn header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
156 self.headers.get_or_insert_with(Vec::new).push(HttpHeader {
157 name: name.into(),
158 value: value.into(),
159 });
160 self
161 }
162
163 pub fn json(&mut self, body: impl serde::Serialize) -> &mut Self {
168 self.body = Some(serde_json::to_vec(&body).unwrap());
169 self
170 }
171
172 #[must_use]
177 pub fn build(&self) -> HttpResponse {
178 self.fallible_build()
179 .expect("All required fields were initialized")
180 }
181}
182
183#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
184pub enum HttpResult {
185 Ok(HttpResponse),
186 Err(HttpError),
187}
188
189impl From<crate::Result<HttpResponse>> for HttpResult {
190 fn from(result: Result<HttpResponse, HttpError>) -> Self {
191 match result {
192 Ok(response) => HttpResult::Ok(response),
193 Err(err) => HttpResult::Err(err),
194 }
195 }
196}
197
198impl crux_core::capability::Operation for HttpRequest {
199 type Output = HttpResult;
200
201 #[cfg(feature = "typegen")]
202 fn register_types(generator: &mut crux_core::typegen::TypeGen) -> crux_core::typegen::Result {
203 generator.register_type::<HttpError>()?;
204 generator.register_type::<Self>()?;
205 generator.register_type::<Self::Output>()?;
206 Ok(())
207 }
208}
209
210#[async_trait]
211pub(crate) trait EffectSender {
212 async fn send(&self, effect: HttpRequest) -> HttpResult;
213}
214
215#[async_trait]
216impl<Ev> EffectSender for crux_core::capability::CapabilityContext<HttpRequest, Ev>
217where
218 Ev: 'static,
219{
220 async fn send(&self, effect: HttpRequest) -> HttpResult {
221 crux_core::capability::CapabilityContext::request_from_shell(self, effect).await
222 }
223}
224
225#[async_trait]
226pub(crate) trait ProtocolRequestBuilder {
227 async fn into_protocol_request(mut self) -> crate::Result<HttpRequest>;
228}
229
230#[async_trait]
231impl ProtocolRequestBuilder for crate::Request {
232 async fn into_protocol_request(mut self) -> crate::Result<HttpRequest> {
233 let body = if self.is_empty() == Some(false) {
234 self.take_body().into_bytes().await?
235 } else {
236 vec![]
237 };
238
239 Ok(HttpRequest {
240 method: self.method().to_string(),
241 url: self.url().to_string(),
242 headers: self
243 .iter()
244 .flat_map(|(name, values)| {
245 values.iter().map(|value| HttpHeader {
246 name: name.to_string(),
247 value: value.to_string(),
248 })
249 })
250 .collect(),
251 body,
252 })
253 }
254}
255
256impl From<HttpResponse> for crate::ResponseAsync {
257 fn from(effect_response: HttpResponse) -> Self {
258 let mut res = http_types::Response::new(effect_response.status);
259 res.set_body(effect_response.body);
260 for header in effect_response.headers {
261 res.append_header(header.name.as_str(), header.value);
262 }
263
264 crate::ResponseAsync::new(res)
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use serde::{Deserialize, Serialize};
272
273 #[test]
274 fn test_http_request_get() {
275 let req = HttpRequest::get("https://example.com").build();
276
277 assert_eq!(
278 req,
279 HttpRequest {
280 method: "GET".to_string(),
281 url: "https://example.com".to_string(),
282 ..Default::default()
283 }
284 );
285 }
286
287 #[test]
288 fn test_http_request_get_with_fields() {
289 let req = HttpRequest::get("https://example.com")
290 .header("foo", "bar")
291 .body("123")
292 .build();
293
294 assert_eq!(
295 req,
296 HttpRequest {
297 method: "GET".to_string(),
298 url: "https://example.com".to_string(),
299 headers: vec![HttpHeader {
300 name: "foo".to_string(),
301 value: "bar".to_string(),
302 }],
303 body: "123".as_bytes().to_vec(),
304 }
305 );
306 }
307
308 #[test]
309 fn test_http_response_status() {
310 let req = HttpResponse::status(302).build();
311
312 assert_eq!(
313 req,
314 HttpResponse {
315 status: 302,
316 ..Default::default()
317 }
318 );
319 }
320
321 #[test]
322 fn test_http_response_status_with_fields() {
323 let req = HttpResponse::status(302)
324 .header("foo", "bar")
325 .body("hello world")
326 .build();
327
328 assert_eq!(
329 req,
330 HttpResponse {
331 status: 302,
332 headers: vec![HttpHeader {
333 name: "foo".to_string(),
334 value: "bar".to_string(),
335 }],
336 body: "hello world".as_bytes().to_vec(),
337 }
338 );
339 }
340
341 #[test]
342 fn test_http_request_debug_repr() {
343 {
344 let req = HttpRequest::post("http://example.com")
346 .header("foo", "bar")
347 .body("hello world!")
348 .build();
349 let repr = format!("{req:?}");
350 assert_eq!(
351 repr,
352 r#"HttpRequest { method: "POST", url: "http://example.com", headers: [HttpHeader { name: "foo", value: "bar" }], body: "hello world!" }"#
353 );
354 }
355
356 {
357 let req = HttpRequest::post("http://example.com")
359 .body("abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu😀😀😀😀😀😀")
361 .build();
362 let repr = format!("{req:?}");
363 assert_eq!(
364 repr,
365 r#"HttpRequest { method: "POST", url: "http://example.com", body: "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu😀😀"... }"#
366 );
367 }
368
369 {
370 let req = HttpRequest::post("http://example.com")
372 .body(vec![255, 254, 253, 252])
373 .build();
374 let repr = format!("{req:?}");
375 assert_eq!(
376 repr,
377 r#"HttpRequest { method: "POST", url: "http://example.com", body: <binary data - 4 bytes> }"#
378 );
379 }
380 }
381
382 #[test]
383 fn test_http_request_query() {
384 #[derive(Serialize, Deserialize)]
385 struct QueryParams {
386 page: u32,
387 limit: u32,
388 search: String,
389 }
390
391 let query = QueryParams {
392 page: 2,
393 limit: 10,
394 search: "test".to_string(),
395 };
396
397 let mut builder = HttpRequestBuilder {
398 method: Some("GET".to_string()),
399 url: Some("https://example.com".to_string()),
400 headers: Some(vec![HttpHeader {
401 name: "foo".to_string(),
402 value: "bar".to_string(),
403 }]),
404 body: Some(vec![]),
405 };
406
407 builder
408 .query(&query)
409 .expect("should serialize query params");
410 let req = builder.build();
411
412 assert_eq!(
413 req,
414 HttpRequest {
415 method: "GET".to_string(),
416 url: "https://example.com?page=2&limit=10&search=test".to_string(),
417 headers: vec![HttpHeader {
418 name: "foo".to_string(),
419 value: "bar".to_string(),
420 }],
421 body: vec![],
422 }
423 );
424 }
425
426 #[test]
427 fn test_http_request_query_with_special_chars() {
428 #[derive(Serialize, Deserialize)]
429 struct QueryParams {
430 name: String,
431 email: String,
432 }
433
434 let query = QueryParams {
435 name: "John Doe".to_string(),
436 email: "john@example.com".to_string(),
437 };
438
439 let mut builder = HttpRequestBuilder {
440 method: Some("GET".to_string()),
441 url: Some("https://example.com".to_string()),
442 headers: Some(vec![]),
443 body: Some(vec![]),
444 };
445
446 builder
447 .query(&query)
448 .expect("should serialize query params with special chars");
449 let req = builder.build();
450
451 assert_eq!(
452 req,
453 HttpRequest {
454 method: "GET".to_string(),
455 url: "https://example.com?name=John+Doe&email=john%40example.com".to_string(),
456 headers: vec![],
457 body: vec![],
458 }
459 );
460 }
461
462 #[test]
463 fn test_http_request_query_with_empty_values() {
464 #[derive(Serialize, Deserialize)]
465 struct QueryParams {
466 empty: String,
467 none: Option<String>,
468 }
469
470 let query = QueryParams {
471 empty: String::new(),
472 none: None,
473 };
474
475 let mut builder = HttpRequestBuilder {
476 method: Some("GET".to_string()),
477 url: Some("https://example.com".to_string()),
478 headers: Some(vec![]),
479 body: Some(vec![]),
480 };
481
482 builder
483 .query(&query)
484 .expect("should serialize query params with empty values");
485 let req = builder.build();
486
487 assert_eq!(
488 req,
489 HttpRequest {
490 method: "GET".to_string(),
491 url: "https://example.com?empty=".to_string(),
492 headers: vec![],
493 body: vec![],
494 }
495 );
496 }
497
498 #[test]
499 fn test_http_request_query_with_url_with_existing_query_params() {
500 #[derive(Serialize, Deserialize)]
501 struct QueryParams {
502 name: String,
503 email: String,
504 }
505
506 let query = QueryParams {
507 name: "John Doe".to_string(),
508 email: "john@example.com".to_string(),
509 };
510
511 let mut builder = HttpRequestBuilder {
512 method: Some("GET".to_string()),
513 url: Some("https://example.com?foo=bar".to_string()),
514 headers: Some(vec![]),
515 body: Some(vec![]),
516 };
517
518 builder
519 .query(&query)
520 .expect("should serialize query params");
521 let req = builder.build();
522
523 assert_eq!(
524 req,
525 HttpRequest {
526 method: "GET".to_string(),
527 url: "https://example.com?foo=bar&name=John+Doe&email=john%40example.com"
528 .to_string(),
529 headers: vec![],
530 body: vec![],
531 }
532 );
533 }
534}