crux_http/
protocol.rs

1//! The protocol for communicating with the shell
2//!
3//! Crux capabilities don't interface with the outside world themselves, they carry
4//! out all their operations by exchanging messages with the platform specific shell.
5//! This module defines the protocol for `crux_http` to communicate with the shell.
6
7use 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    /// Sets the query parameters of the request to the given value.
89    ///
90    /// # Errors
91    /// Returns an [`HttpError`] if the serialization fails.
92    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    /// Sets the body of the request to the JSON representation of the given value.
106    ///
107    /// # Panics
108    /// Panics if the serialization fails.
109    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    /// Builds the request.
115    ///
116    /// # Panics
117    /// Panics if any required fields are missing.
118    #[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, // FIXME this probably should be a giant enum instead.
133    #[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    /// Sets the body of the response to the given JSON.
164    ///
165    /// # Panics
166    /// If the JSON serialization fails.
167    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    /// Builds the response.
173    ///
174    /// # Panics
175    /// If a required field has not been initialized.
176    #[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            // small
345            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            // big
358            let req = HttpRequest::post("http://example.com")
359                // we check that we handle unicode boundaries correctly
360                .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            // binary
371            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}