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
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, // FIXME this probably should be a giant enum instead.
109    #[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            // small
309            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            // big
322            let req = HttpRequest::post("http://example.com")
323                // we check that we handle unicode boundaries correctly
324                .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            // binary
335            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}