wasm_runner_sdk/
response.rs

1//! HTTP response types and the IntoResponse trait.
2
3use crate::abi;
4use serde::Serialize;
5
6/// HTTP status code.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub struct StatusCode(u16);
9
10impl StatusCode {
11    // 2xx Success
12    pub const OK: StatusCode = StatusCode(200);
13    pub const CREATED: StatusCode = StatusCode(201);
14    pub const ACCEPTED: StatusCode = StatusCode(202);
15    pub const NO_CONTENT: StatusCode = StatusCode(204);
16
17    // 3xx Redirection
18    pub const MOVED_PERMANENTLY: StatusCode = StatusCode(301);
19    pub const FOUND: StatusCode = StatusCode(302);
20    pub const SEE_OTHER: StatusCode = StatusCode(303);
21    pub const NOT_MODIFIED: StatusCode = StatusCode(304);
22    pub const TEMPORARY_REDIRECT: StatusCode = StatusCode(307);
23    pub const PERMANENT_REDIRECT: StatusCode = StatusCode(308);
24
25    // 4xx Client Errors
26    pub const BAD_REQUEST: StatusCode = StatusCode(400);
27    pub const UNAUTHORIZED: StatusCode = StatusCode(401);
28    pub const FORBIDDEN: StatusCode = StatusCode(403);
29    pub const NOT_FOUND: StatusCode = StatusCode(404);
30    pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(405);
31    pub const CONFLICT: StatusCode = StatusCode(409);
32    pub const GONE: StatusCode = StatusCode(410);
33    pub const UNPROCESSABLE_ENTITY: StatusCode = StatusCode(422);
34    pub const TOO_MANY_REQUESTS: StatusCode = StatusCode(429);
35
36    // 5xx Server Errors
37    pub const INTERNAL_SERVER_ERROR: StatusCode = StatusCode(500);
38    pub const NOT_IMPLEMENTED: StatusCode = StatusCode(501);
39    pub const BAD_GATEWAY: StatusCode = StatusCode(502);
40    pub const SERVICE_UNAVAILABLE: StatusCode = StatusCode(503);
41    pub const GATEWAY_TIMEOUT: StatusCode = StatusCode(504);
42
43    /// Creates a status code from a raw u16 value.
44    pub const fn from_u16(code: u16) -> Self {
45        StatusCode(code)
46    }
47
48    /// Returns the raw status code value.
49    pub const fn as_u16(&self) -> u16 {
50        self.0
51    }
52
53    /// Returns true if this is a success status (2xx).
54    pub const fn is_success(&self) -> bool {
55        self.0 >= 200 && self.0 < 300
56    }
57
58    /// Returns true if this is a redirection status (3xx).
59    pub const fn is_redirection(&self) -> bool {
60        self.0 >= 300 && self.0 < 400
61    }
62
63    /// Returns true if this is a client error status (4xx).
64    pub const fn is_client_error(&self) -> bool {
65        self.0 >= 400 && self.0 < 500
66    }
67
68    /// Returns true if this is a server error status (5xx).
69    pub const fn is_server_error(&self) -> bool {
70        self.0 >= 500 && self.0 < 600
71    }
72}
73
74impl From<u16> for StatusCode {
75    fn from(code: u16) -> Self {
76        StatusCode(code)
77    }
78}
79
80impl std::fmt::Display for StatusCode {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{}", self.0)
83    }
84}
85
86/// An HTTP response that can be sent to the client.
87#[derive(Debug, Clone)]
88pub struct Response {
89    status: StatusCode,
90    headers: Vec<(String, String)>,
91    body: Vec<u8>,
92}
93
94impl Response {
95    /// Creates a new response with the given status code.
96    pub fn new(status: StatusCode) -> Self {
97        Self {
98            status,
99            headers: Vec::new(),
100            body: Vec::new(),
101        }
102    }
103
104    /// Creates a 200 OK response.
105    pub fn ok() -> Self {
106        Self::new(StatusCode::OK)
107    }
108
109    /// Creates a response with the given status code.
110    pub fn status(status: impl Into<StatusCode>) -> Self {
111        Self::new(status.into())
112    }
113
114    /// Sets the status code.
115    pub fn with_status(mut self, status: impl Into<StatusCode>) -> Self {
116        self.status = status.into();
117        self
118    }
119
120    /// Adds a header to the response.
121    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
122        self.headers.push((name.into(), value.into()));
123        self
124    }
125
126    /// Sets the Content-Type header.
127    pub fn with_content_type(self, content_type: impl Into<String>) -> Self {
128        self.with_header("content-type", content_type)
129    }
130
131    /// Sets the response body from bytes.
132    pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
133        self.body = body.into();
134        self
135    }
136
137    /// Sets the response body from a string.
138    pub fn with_text(self, text: impl Into<String>) -> Self {
139        let text = text.into();
140        self.with_content_type("text/plain; charset=utf-8")
141            .with_body(text.into_bytes())
142    }
143
144    /// Sets the response body as HTML.
145    pub fn with_html(self, html: impl Into<String>) -> Self {
146        let html = html.into();
147        self.with_content_type("text/html; charset=utf-8")
148            .with_body(html.into_bytes())
149    }
150
151    /// Sets the response body as JSON.
152    pub fn with_json<T: Serialize>(self, value: &T) -> Result<Self, serde_json::Error> {
153        let json = serde_json::to_vec(value)?;
154        Ok(self
155            .with_content_type("application/json")
156            .with_body(json))
157    }
158
159    /// Sets a cookie on the response.
160    pub fn with_cookie(
161        mut self,
162        name: impl Into<String>,
163        value: impl Into<String>,
164        options: Option<&str>,
165    ) -> Self {
166        let name = name.into();
167        let value = value.into();
168        let cookie_value = match options {
169            Some(opts) => format!("{}={}; {}", name, value, opts),
170            None => format!("{}={}", name, value),
171        };
172        self.headers.push(("set-cookie".to_string(), cookie_value));
173        self
174    }
175
176    /// Returns the status code.
177    pub fn status_code(&self) -> StatusCode {
178        self.status
179    }
180
181    /// Returns the headers.
182    pub fn headers(&self) -> &[(String, String)] {
183        &self.headers
184    }
185
186    /// Returns the body.
187    pub fn body(&self) -> &[u8] {
188        &self.body
189    }
190
191    /// Sends this response to the host.
192    pub fn send(self) {
193        unsafe {
194            abi::response_create(self.status.0 as i32);
195
196            for (name, value) in &self.headers {
197                abi::response_set_header(
198                    name.as_ptr(),
199                    name.len() as i32,
200                    value.as_ptr(),
201                    value.len() as i32,
202                );
203            }
204
205            if !self.body.is_empty() {
206                abi::response_write_body(self.body.as_ptr(), self.body.len() as i32);
207            }
208
209            abi::response_send();
210        }
211    }
212}
213
214/// Trait for types that can be converted into an HTTP response.
215///
216/// This is the core trait that enables ergonomic handlers - any type that
217/// implements `IntoResponse` can be returned from a handler function.
218pub trait IntoResponse {
219    /// Converts this value into a Response.
220    fn into_response(self) -> Response;
221}
222
223// Response itself implements IntoResponse
224impl IntoResponse for Response {
225    fn into_response(self) -> Response {
226        self
227    }
228}
229
230// Strings become text/plain responses
231impl IntoResponse for String {
232    fn into_response(self) -> Response {
233        Response::ok().with_text(self)
234    }
235}
236
237impl IntoResponse for &str {
238    fn into_response(self) -> Response {
239        Response::ok().with_text(self)
240    }
241}
242
243// Byte vectors become application/octet-stream
244impl IntoResponse for Vec<u8> {
245    fn into_response(self) -> Response {
246        Response::ok()
247            .with_content_type("application/octet-stream")
248            .with_body(self)
249    }
250}
251
252impl IntoResponse for &[u8] {
253    fn into_response(self) -> Response {
254        Response::ok()
255            .with_content_type("application/octet-stream")
256            .with_body(self.to_vec())
257    }
258}
259
260// StatusCode alone becomes an empty response with that status
261impl IntoResponse for StatusCode {
262    fn into_response(self) -> Response {
263        Response::new(self)
264    }
265}
266
267// (StatusCode, T) tuples set the status code
268impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
269    fn into_response(self) -> Response {
270        let (status, body) = self;
271        body.into_response().with_status(status)
272    }
273}
274
275// (StatusCode, headers, T) tuples
276impl<T: IntoResponse> IntoResponse for (StatusCode, Vec<(String, String)>, T) {
277    fn into_response(self) -> Response {
278        let (status, headers, body) = self;
279        let mut response = body.into_response().with_status(status);
280        for (name, value) in headers {
281            response = response.with_header(name, value);
282        }
283        response
284    }
285}
286
287// Result types: Ok returns the success response, Err returns the error response
288impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
289    fn into_response(self) -> Response {
290        match self {
291            Ok(v) => v.into_response(),
292            Err(e) => e.into_response(),
293        }
294    }
295}
296
297// Option: Some returns the value, None returns 404
298impl<T: IntoResponse> IntoResponse for Option<T> {
299    fn into_response(self) -> Response {
300        match self {
301            Some(v) => v.into_response(),
302            None => Response::new(StatusCode::NOT_FOUND).with_text("Not Found"),
303        }
304    }
305}
306
307// Unit type returns 200 OK with no body
308impl IntoResponse for () {
309    fn into_response(self) -> Response {
310        Response::new(StatusCode::OK)
311    }
312}
313
314/// A JSON response wrapper.
315///
316/// Wraps any serializable type and serializes it as JSON.
317#[derive(Debug, Clone)]
318pub struct Json<T>(pub T);
319
320impl<T: Serialize> IntoResponse for Json<T> {
321    fn into_response(self) -> Response {
322        match serde_json::to_vec(&self.0) {
323            Ok(body) => Response::ok()
324                .with_content_type("application/json")
325                .with_body(body),
326            Err(e) => Response::new(StatusCode::INTERNAL_SERVER_ERROR)
327                .with_text(format!("JSON serialization error: {}", e)),
328        }
329    }
330}
331
332/// An HTML response wrapper.
333#[derive(Debug, Clone)]
334pub struct Html<T>(pub T);
335
336impl<T: Into<String>> IntoResponse for Html<T> {
337    fn into_response(self) -> Response {
338        Response::ok().with_html(self.0)
339    }
340}
341
342/// A redirect response.
343#[derive(Debug, Clone)]
344pub struct Redirect {
345    status: StatusCode,
346    location: String,
347}
348
349impl Redirect {
350    /// Creates a 302 Found redirect.
351    pub fn to(location: impl Into<String>) -> Self {
352        Self {
353            status: StatusCode::FOUND,
354            location: location.into(),
355        }
356    }
357
358    /// Creates a 301 Moved Permanently redirect.
359    pub fn permanent(location: impl Into<String>) -> Self {
360        Self {
361            status: StatusCode::MOVED_PERMANENTLY,
362            location: location.into(),
363        }
364    }
365
366    /// Creates a 303 See Other redirect.
367    pub fn see_other(location: impl Into<String>) -> Self {
368        Self {
369            status: StatusCode::SEE_OTHER,
370            location: location.into(),
371        }
372    }
373
374    /// Creates a 307 Temporary Redirect.
375    pub fn temporary(location: impl Into<String>) -> Self {
376        Self {
377            status: StatusCode::TEMPORARY_REDIRECT,
378            location: location.into(),
379        }
380    }
381}
382
383impl IntoResponse for Redirect {
384    fn into_response(self) -> Response {
385        Response::new(self.status).with_header("location", self.location)
386    }
387}