seda_sdk_rs/
http.rs

1//! HTTP fetch action and associated types for the `seda_runtime_sdk`.
2//!
3//! Defines JSON-serializable request and response structs ([`HttpFetchAction`], [`HttpFetchOptions`], [`HttpFetchResponse`])
4//! and provides [`http_fetch`] for executing HTTP requests via VM FFI calls.
5
6use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::errors::Result;
11use crate::{
12    bytes::{Bytes, FromBytes, ToBytes},
13    promise::PromiseStatus,
14};
15
16/// An HTTP fetch action containing the target URL and fetch options.
17/// This action is serialized and sent to the VM for execution.
18#[derive(Serialize, Deserialize, Clone, Debug)]
19pub struct HttpFetchAction {
20    /// The URL to fetch.
21    pub url: String,
22    /// The options for the HTTP fetch request.
23    pub options: HttpFetchOptions,
24}
25
26/// Options for the HTTP fetch request, including method, headers, body, and timeout.
27/// This struct is serialized and sent to the VM for execution.
28#[derive(Serialize, Deserialize, Clone, Debug)]
29pub struct HttpFetchOptions {
30    /// The HTTP method to use for the request.
31    pub method: HttpFetchMethod,
32    /// Headers to include in the request.
33    pub headers: BTreeMap<String, String>,
34    /// The body of the request, if any.
35    pub body: Option<Bytes>,
36    /// Timeout for the request in milliseconds.
37    pub timeout_ms: Option<u32>,
38}
39
40impl Default for HttpFetchOptions {
41    fn default() -> Self {
42        HttpFetchOptions {
43            method: HttpFetchMethod::Get,
44            headers: BTreeMap::new(),
45            body: None,
46            timeout_ms: Some(2_000),
47        }
48    }
49}
50
51/// Represents the HTTP methods that can be used in an HTTP fetch request.
52/// This enum is serialized and sent to the VM for execution.
53/// It represents the various [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
54/// that can be used in an HTTP fetch request.
55#[derive(Serialize, Deserialize, Clone, Debug)]
56#[allow(missing_docs)]
57pub enum HttpFetchMethod {
58    Options,
59    Get,
60    Post,
61    Put,
62    Delete,
63    Head,
64    Trace,
65    Connect,
66    Patch,
67}
68
69impl HttpFetchMethod {
70    /// Returns the string representation, in all caps, of the HTTP method.
71    pub fn as_str(&self) -> &str {
72        match self {
73            HttpFetchMethod::Options => "OPTIONS",
74            HttpFetchMethod::Get => "GET",
75            HttpFetchMethod::Post => "POST",
76            HttpFetchMethod::Put => "PUT",
77            HttpFetchMethod::Delete => "DELETE",
78            HttpFetchMethod::Head => "HEAD",
79            HttpFetchMethod::Trace => "TRACE",
80            HttpFetchMethod::Connect => "CONNECT",
81            HttpFetchMethod::Patch => "PATCH",
82        }
83    }
84}
85
86/// Represents the response from an HTTP fetch request.
87/// This struct is serialized and the result is returned to the caller.
88#[derive(Serialize, Deserialize, Clone, Debug)]
89pub struct HttpFetchResponse {
90    /// HTTP Status code
91    pub status: u16,
92
93    /// Response headers
94    pub headers: BTreeMap<String, String>,
95
96    /// Response body in bytes
97    pub bytes: Vec<u8>,
98
99    /// The final URL that was resolved
100    pub url: String,
101
102    /// The byte length of the response
103    pub content_length: usize,
104}
105
106impl HttpFetchResponse {
107    /// Returns `true` if the status code is in the 2xx range.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use seda_sdk_rs::http::HttpFetchResponse;
113    /// let response = HttpFetchResponse {
114    ///     status: 200,
115    ///     headers: Default::default(),
116    ///     bytes: Vec::new(),
117    ///     url: "https://api.example.com/data".to_string(),
118    ///     content_length: 0,
119    /// };
120    /// assert!(response.is_ok());
121    /// ```
122    pub fn is_ok(&self) -> bool {
123        self.status >= 200 && self.status <= 299
124    }
125
126    /// Converts a [`PromiseStatus`] into an [`HttpFetchResponse`], treating rejections as errors.
127    ///
128    /// # Errors
129    ///
130    /// Fails if the `PromiseStatus` is not a `Fulfilled` variant or if the deserialization fails.
131    pub fn from_promise(promise_status: PromiseStatus) -> Self {
132        match promise_status {
133            PromiseStatus::Rejected(error) => error.try_into().unwrap(),
134            _ => promise_status.parse().unwrap(),
135        }
136    }
137}
138
139impl ToBytes for HttpFetchResponse {
140    fn to_bytes(self) -> Bytes {
141        serde_json::to_vec(&self).unwrap().to_bytes()
142    }
143}
144
145impl FromBytes for HttpFetchResponse {
146    fn from_bytes(bytes: &[u8]) -> Result<Self> {
147        serde_json::from_slice(bytes).map_err(Into::into)
148    }
149
150    fn from_bytes_vec(bytes: Vec<u8>) -> Result<Self> {
151        serde_json::from_slice(&bytes).map_err(Into::into)
152    }
153}
154
155impl TryFrom<Vec<u8>> for HttpFetchResponse {
156    type Error = serde_json::Error;
157
158    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
159        serde_json::from_slice(&value)
160    }
161}
162
163/// Performs an HTTP fetch request with the given URL and options.
164/// This wraps the unsafe FFI call to the VM's `http_fetch` function.
165///
166/// # Panics
167///
168/// Panics if the serialization of the [`HttpFetchAction`] fails or if the deserialization of the response fails.
169/// We expect these to never happen in practice, as the SDK is designed to ensure valid inputs.
170///
171/// # Examples
172///
173/// ```no_run
174/// use seda_sdk_rs::{bytes::ToBytes, http::{http_fetch, HttpFetchMethod, HttpFetchOptions}};
175/// use std::collections::BTreeMap;
176///
177/// // Basic GET request
178/// let response = http_fetch("https://api.example.com/data", None);
179/// if response.is_ok() {
180///     println!("Status: {}", response.status);
181///     println!("Body length: {}", response.content_length);
182/// }
183///
184/// // POST request with JSON payload
185/// let mut headers = BTreeMap::new();
186/// headers.insert("Content-Type".to_string(), "application/json".to_string());
187///
188/// let options = HttpFetchOptions {
189///     method: HttpFetchMethod::Post,
190///     headers,
191///     body: Some(serde_json::to_vec(&serde_json::json!({"temperature": 25.5, "unit": "celsius"})).unwrap().to_bytes()),
192///     timeout_ms: Some(5_000),
193/// };
194///
195/// let response = http_fetch("https://weather-api.example.com/update", Some(options));
196///
197/// // Handle the response
198/// if response.is_ok() {
199///     // Access response data
200///     println!("Status code: {}", response.status);
201///     println!("Final URL: {}", response.url);
202///     println!("Response size: {}", response.content_length);
203///
204///     // Process response headers
205///     if let Some(content_type) = response.headers.get("content-type") {
206///         println!("Content-Type: {}", content_type);
207///     }
208///
209///     // Process response body
210///     if !response.bytes.is_empty() {
211///         // Convert bytes to string if it's UTF-8 encoded
212///         if let Ok(body_text) = String::from_utf8(response.bytes.clone()) {
213///             println!("Response body: {}", body_text);
214///         }
215///     }
216/// } else {
217///     println!("Request failed with status: {}", response.status);
218/// }
219/// ```
220pub fn http_fetch<URL: ToString>(url: URL, options: Option<HttpFetchOptions>) -> HttpFetchResponse {
221    let http_action = HttpFetchAction {
222        url: url.to_string(),
223        options: options.unwrap_or_default(),
224    };
225
226    let action = serde_json::to_string(&http_action).unwrap();
227    let result_length = unsafe { super::raw::http_fetch(action.as_ptr(), action.len() as u32) };
228    let mut result_data_ptr = vec![0; result_length as usize];
229
230    unsafe {
231        super::raw::call_result_write(result_data_ptr.as_mut_ptr(), result_length);
232    }
233
234    let promise_status: PromiseStatus =
235        serde_json::from_slice(&result_data_ptr).expect("Could not deserialize http_fetch");
236
237    HttpFetchResponse::from_promise(promise_status)
238}