wasm_runner_sdk/
request.rs

1//! HTTP request types and accessors.
2
3use crate::abi;
4use std::collections::HashMap;
5
6/// HTTP method enum.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum Method {
9    Get,
10    Post,
11    Put,
12    Delete,
13    Patch,
14    Head,
15    Options,
16    Other,
17}
18
19impl Method {
20    /// Parse a method string into a Method enum.
21    pub fn from_str(s: &str) -> Self {
22        match s.to_uppercase().as_str() {
23            "GET" => Method::Get,
24            "POST" => Method::Post,
25            "PUT" => Method::Put,
26            "DELETE" => Method::Delete,
27            "PATCH" => Method::Patch,
28            "HEAD" => Method::Head,
29            "OPTIONS" => Method::Options,
30            _ => Method::Other,
31        }
32    }
33
34    /// Returns true if this method typically has a request body.
35    pub fn has_body(&self) -> bool {
36        matches!(self, Method::Post | Method::Put | Method::Patch)
37    }
38}
39
40impl std::fmt::Display for Method {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        let s = match self {
43            Method::Get => "GET",
44            Method::Post => "POST",
45            Method::Put => "PUT",
46            Method::Delete => "DELETE",
47            Method::Patch => "PATCH",
48            Method::Head => "HEAD",
49            Method::Options => "OPTIONS",
50            Method::Other => "OTHER",
51        };
52        write!(f, "{}", s)
53    }
54}
55
56/// Represents an incoming HTTP request.
57///
58/// Provides high-level access to request data without exposing unsafe code.
59/// Data is lazily loaded from the host on first access.
60pub struct Request {
61    method: Option<Method>,
62    method_str: Option<String>,
63    path: Option<String>,
64    path_segments: Option<Vec<String>>,
65    query: Option<HashMap<String, String>>,
66    query_all: Option<Vec<(String, String)>>,
67    headers: Option<HashMap<String, String>>,
68    headers_all: Option<Vec<(String, String)>>,
69    cookies: Option<HashMap<String, String>>,
70    body: Option<Vec<u8>>,
71}
72
73impl Request {
74    /// Creates a new Request, loading data from the host.
75    pub(crate) fn new() -> Self {
76        // Signal to host that we're accepting a request
77        unsafe { abi::accept_request() };
78
79        Self {
80            method: None,
81            method_str: None,
82            path: None,
83            path_segments: None,
84            query: None,
85            query_all: None,
86            headers: None,
87            headers_all: None,
88            cookies: None,
89            body: None,
90        }
91    }
92
93    /// Returns the HTTP method as an enum.
94    pub fn method(&mut self) -> Method {
95        if self.method.is_none() {
96            let method_str = self.method_str();
97            self.method = Some(Method::from_str(&method_str));
98        }
99        self.method.unwrap()
100    }
101
102    /// Returns the HTTP method as a string.
103    pub fn method_str(&mut self) -> String {
104        if self.method_str.is_none() {
105            self.method_str = Some(abi::read_string_from_host(|ptr, len| unsafe {
106                abi::request_get_method(ptr, len)
107            }));
108        }
109        self.method_str.clone().unwrap_or_default()
110    }
111
112    /// Returns the request path (e.g., "/api/users/123").
113    pub fn path(&mut self) -> &str {
114        if self.path.is_none() {
115            self.path = Some(abi::read_string_from_host(|ptr, len| unsafe {
116                abi::request_get_path(ptr, len)
117            }));
118        }
119        self.path.as_deref().unwrap_or("/")
120    }
121
122    /// Returns the path split into segments.
123    /// Example: "/api/users/123" -> ["api", "users", "123"]
124    pub fn path_segments(&mut self) -> &[String] {
125        if self.path_segments.is_none() {
126            let count = unsafe { abi::request_path_segment_count() };
127            let mut segments = Vec::with_capacity(count.max(0) as usize);
128
129            for i in 0..count {
130                let segment = abi::read_string_from_host(|ptr, len| unsafe {
131                    abi::request_path_segment(i, ptr, len)
132                });
133                if !segment.is_empty() {
134                    segments.push(segment);
135                }
136            }
137
138            self.path_segments = Some(segments);
139        }
140        self.path_segments.as_deref().unwrap_or(&[])
141    }
142
143    /// Returns a specific path segment by index.
144    /// Example: for "/api/users/123", segment(1) returns Some("users")
145    pub fn path_segment(&mut self, index: usize) -> Option<&str> {
146        self.path_segments().get(index).map(|s| s.as_str())
147    }
148
149    /// Returns all query parameters as a map.
150    /// If a key appears multiple times, only the last value is kept.
151    pub fn query(&mut self) -> &HashMap<String, String> {
152        if self.query.is_none() {
153            let pairs = self.query_all();
154            let mut map = HashMap::new();
155            for (k, v) in pairs {
156                map.insert(k.clone(), v.clone());
157            }
158            self.query = Some(map);
159        }
160        self.query.as_ref().unwrap()
161    }
162
163    /// Returns all query parameters as a list of key-value pairs.
164    /// Preserves duplicate keys.
165    pub fn query_all(&mut self) -> &[(String, String)] {
166        if self.query_all.is_none() {
167            let data = abi::read_bytes_from_host(|ptr, len| unsafe {
168                abi::request_query_all(ptr, len)
169            });
170            self.query_all = Some(abi::deserialize_pairs(&data));
171        }
172        self.query_all.as_deref().unwrap_or(&[])
173    }
174
175    /// Gets a query parameter by name.
176    pub fn query_param(&mut self, name: &str) -> Option<&str> {
177        self.query().get(name).map(|s| s.as_str())
178    }
179
180    /// Returns all headers as a map.
181    /// Header names are lowercase.
182    pub fn headers(&mut self) -> &HashMap<String, String> {
183        if self.headers.is_none() {
184            let pairs = self.headers_all();
185            let mut map = HashMap::new();
186            for (k, v) in pairs {
187                map.insert(k.clone(), v.clone());
188            }
189            self.headers = Some(map);
190        }
191        self.headers.as_ref().unwrap()
192    }
193
194    /// Returns all headers as a list of key-value pairs.
195    pub fn headers_all(&mut self) -> &[(String, String)] {
196        if self.headers_all.is_none() {
197            let data = abi::read_bytes_from_host(|ptr, len| unsafe {
198                abi::request_header_all(ptr, len)
199            });
200            self.headers_all = Some(abi::deserialize_pairs(&data));
201        }
202        self.headers_all.as_deref().unwrap_or(&[])
203    }
204
205    /// Gets a header by name (case-insensitive).
206    pub fn header(&mut self, name: &str) -> Option<&str> {
207        let name_lower = name.to_lowercase();
208        self.headers().get(&name_lower).map(|s| s.as_str())
209    }
210
211    /// Returns the Content-Type header value.
212    pub fn content_type(&mut self) -> Option<&str> {
213        self.header("content-type")
214    }
215
216    /// Returns all cookies as a map.
217    pub fn cookies(&mut self) -> &HashMap<String, String> {
218        if self.cookies.is_none() {
219            let data = abi::read_bytes_from_host(|ptr, len| unsafe {
220                abi::request_cookie_all(ptr, len)
221            });
222            let pairs = abi::deserialize_pairs(&data);
223            let mut map = HashMap::new();
224            for (k, v) in pairs {
225                map.insert(k, v);
226            }
227            self.cookies = Some(map);
228        }
229        self.cookies.as_ref().unwrap()
230    }
231
232    /// Gets a cookie by name.
233    pub fn cookie(&mut self, name: &str) -> Option<&str> {
234        self.cookies().get(name).map(|s| s.as_str())
235    }
236
237    /// Returns the request body as bytes.
238    pub fn body(&mut self) -> &[u8] {
239        if self.body.is_none() {
240            // Read body in chunks
241            let mut body = Vec::new();
242            let mut offset = 0i32;
243            let chunk_size = 4096i32;
244
245            loop {
246                let mut chunk = vec![0u8; chunk_size as usize];
247                let read = unsafe {
248                    abi::request_read_body(offset, chunk.as_mut_ptr(), chunk_size)
249                };
250
251                if read <= 0 {
252                    break;
253                }
254
255                chunk.truncate(read as usize);
256                body.extend_from_slice(&chunk);
257                offset += read;
258
259                if read < chunk_size {
260                    break;
261                }
262            }
263
264            self.body = Some(body);
265        }
266        self.body.as_deref().unwrap_or(&[])
267    }
268
269    /// Returns the request body as a string.
270    /// Returns an error if the body is not valid UTF-8.
271    pub fn body_string(&mut self) -> Result<String, std::string::FromUtf8Error> {
272        String::from_utf8(self.body().to_vec())
273    }
274
275    /// Parses the request body as JSON.
276    pub fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> Result<T, serde_json::Error> {
277        serde_json::from_slice(self.body())
278    }
279}
280
281impl Default for Request {
282    fn default() -> Self {
283        Self::new()
284    }
285}