Skip to content

Commit cfcec30

Browse files
committed
Improve the builtin Request/Response types
Signed-off-by: Ryan Levick <[email protected]>
1 parent e4cb2c5 commit cfcec30

File tree

5 files changed

+331
-56
lines changed

5 files changed

+331
-56
lines changed

examples/variables-rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use spin_sdk::{
66
/// This endpoint returns the config value specified by key.
77
#[http_component]
88
fn get(req: Request) -> anyhow::Result<Response> {
9-
if req.path_and_query.contains("dotenv") {
9+
if req.path().contains("dotenv") {
1010
let val = variables::get("dotenv").expect("Failed to acquire dotenv from spin.toml");
1111
return Ok(Response::new(200, val));
1212
}

sdk/rust/macro/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ pub fn http_component(_attr: TokenStream, item: TokenStream) -> TokenStream {
112112

113113
async fn handle_response<R: ::spin_sdk::http::IntoResponse>(response_out: ::spin_sdk::http::ResponseOutparam, resp: R) {
114114
let mut response = ::spin_sdk::http::IntoResponse::into_response(resp);
115-
let body = std::mem::take(&mut response.body);
115+
let body = std::mem::take(response.body_mut());
116116
let response = ::std::convert::Into::into(response);
117117
if let Err(e) = ::spin_sdk::http::ResponseOutparam::set_with_body(response_out, response, body).await {
118118
eprintln!("Could not set `ResponseOutparam`: {e}");

sdk/rust/src/http.rs

Lines changed: 241 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,132 @@ pub use super::wit::wasi::http::types::*;
1515
/// is no need for streaming bodies.
1616
pub struct Request {
1717
/// The method of the request
18-
pub method: Method,
19-
/// The path together with the query string
20-
pub path_and_query: String,
18+
method: Method,
19+
/// The uri for the request
20+
///
21+
/// The first item is set to `None` if the supplied uri is malformed
22+
uri: (Option<hyperium::Uri>, String),
2123
/// The request headers
22-
pub headers: Vec<(String, Vec<u8>)>,
24+
headers: Vec<(String, String)>,
2325
/// The request body as bytes
24-
pub body: Vec<u8>,
26+
body: Vec<u8>,
27+
}
28+
29+
impl Request {
30+
fn new(method: Method, uri: impl Into<String>) -> Self {
31+
Self {
32+
method,
33+
uri: Self::parse_uri(uri.into()),
34+
headers: Vec::new(),
35+
body: Vec::new(),
36+
}
37+
}
38+
39+
/// The request method
40+
pub fn method(&self) -> &Method {
41+
&self.method
42+
}
43+
44+
/// The request uri
45+
pub fn uri(&self) -> &str {
46+
&self.uri.1
47+
}
48+
49+
/// The request uri path
50+
pub fn path(&self) -> &str {
51+
self.uri.0.as_ref().map(|u| u.path()).unwrap_or_default()
52+
}
53+
54+
/// The request uri query
55+
pub fn query(&self) -> &str {
56+
self.uri
57+
.0
58+
.as_ref()
59+
.and_then(|u| u.query())
60+
.unwrap_or_default()
61+
}
62+
63+
/// The request headers
64+
pub fn headers(&self) -> &[(String, String)] {
65+
&self.headers
66+
}
67+
68+
/// The request headers
69+
pub fn headers_mut(&mut self) -> &mut Vec<(String, String)> {
70+
&mut self.headers
71+
}
72+
73+
/// The request body
74+
pub fn body(&self) -> &[u8] {
75+
&self.body
76+
}
77+
78+
/// The request body
79+
pub fn body_mut(&mut self) -> &mut Vec<u8> {
80+
&mut self.body
81+
}
82+
83+
/// Consume this type and return its body
84+
pub fn into_body(self) -> Vec<u8> {
85+
self.body
86+
}
87+
88+
/// Create a request builder
89+
pub fn builder() -> RequestBuilder {
90+
RequestBuilder::new(Method::Get, "/")
91+
}
92+
93+
fn parse_uri(uri: String) -> (Option<hyperium::Uri>, String) {
94+
(
95+
hyperium::Uri::try_from(&uri)
96+
.or_else(|_| hyperium::Uri::try_from(&format!("http://{uri}")))
97+
.ok(),
98+
uri,
99+
)
100+
}
101+
}
102+
103+
/// A request builder
104+
pub struct RequestBuilder {
105+
request: Request,
106+
}
107+
108+
impl RequestBuilder {
109+
/// Create a new `RequestBuilder`
110+
pub fn new(method: Method, uri: impl Into<String>) -> Self {
111+
Self {
112+
request: Request::new(method, uri.into()),
113+
}
114+
}
115+
116+
/// Set the method
117+
pub fn method(&mut self, method: Method) -> &mut Self {
118+
self.request.method = method;
119+
self
120+
}
121+
122+
/// Set the uri
123+
pub fn uri(&mut self, uri: impl Into<String>) -> &mut Self {
124+
self.request.uri = Request::parse_uri(uri.into());
125+
self
126+
}
127+
128+
/// Set the headers
129+
pub fn headers(&mut self, headers: impl conversions::IntoHeaders) -> &mut Self {
130+
self.request.headers = headers.into_headers();
131+
self
132+
}
133+
134+
/// Set the body
135+
pub fn body(&mut self, body: impl conversions::IntoBody) -> &mut Self {
136+
self.request.body = body.into_body();
137+
self
138+
}
139+
140+
/// Build the `Request`
141+
pub fn build(&mut self) -> Request {
142+
std::mem::replace(&mut self.request, Request::new(Method::Get, "/"))
143+
}
25144
}
26145

27146
/// A unified response object that can represent both outgoing and incoming responses.
@@ -30,38 +149,94 @@ pub struct Request {
30149
/// is no need for streaming bodies.
31150
pub struct Response {
32151
/// The status of the response
33-
pub status: StatusCode,
152+
status: StatusCode,
34153
/// The response headers
35-
pub headers: Vec<(String, Vec<u8>)>,
154+
headers: Vec<(String, String)>,
36155
/// The body of the response as bytes
37-
pub body: Vec<u8>,
156+
body: Vec<u8>,
38157
}
39158

40159
impl Response {
41160
/// Create a new response from a status and optional headers and body
42-
pub fn new<S: conversions::IntoStatusCode, B: conversions::IntoBody>(
43-
status: S,
44-
body: B,
45-
) -> Self {
161+
pub fn new(status: impl conversions::IntoStatusCode, body: impl conversions::IntoBody) -> Self {
46162
Self {
47163
status: status.into_status_code(),
48-
headers: Default::default(),
164+
headers: Vec::new(),
49165
body: body.into_body(),
50166
}
51167
}
52168

53-
/// Create a new response from a status and optional headers and body
54-
pub fn new_with_headers<S: conversions::IntoStatusCode, B: conversions::IntoBody>(
55-
status: S,
56-
headers: Vec<(String, Vec<u8>)>,
57-
body: B,
58-
) -> Self {
59-
Self {
60-
status: status.into_status_code(),
61-
headers,
62-
body: body.into_body(),
169+
/// The response status
170+
pub fn status(&self) -> &StatusCode {
171+
&self.status
172+
}
173+
174+
/// The response headers
175+
///
176+
/// Technically headers do not have to be utf8 encoded but this type assumes they are.
177+
/// If you know you'll be dealing with non-utf8 encoded headers, reach more a more powerful
178+
/// response type like that found in the `http` crate.
179+
pub fn headers(&self) -> &[(String, String)] {
180+
&self.headers
181+
}
182+
183+
/// The response headers
184+
pub fn headers_mut(&mut self) -> &mut Vec<(String, String)> {
185+
&mut self.headers
186+
}
187+
188+
/// The response body
189+
pub fn body(&self) -> &[u8] {
190+
&self.body
191+
}
192+
193+
/// The response body
194+
pub fn body_mut(&mut self) -> &mut Vec<u8> {
195+
&mut self.body
196+
}
197+
198+
/// Consume this type and return its body
199+
pub fn into_body(self) -> Vec<u8> {
200+
self.body
201+
}
202+
203+
fn builder() -> ResponseBuilder {
204+
ResponseBuilder::new(200)
205+
}
206+
}
207+
208+
struct ResponseBuilder {
209+
response: Response,
210+
}
211+
212+
impl ResponseBuilder {
213+
pub fn new(status: impl conversions::IntoStatusCode) -> Self {
214+
ResponseBuilder {
215+
response: Response::new(status, Vec::new()),
63216
}
64217
}
218+
219+
/// Set the status
220+
pub fn status(&mut self, status: impl conversions::IntoStatusCode) -> &mut Self {
221+
self.response.status = status.into_status_code();
222+
self
223+
}
224+
225+
/// Set the headers
226+
pub fn headers(&mut self, headers: impl conversions::IntoHeaders) -> &mut Self {
227+
self.response.headers = headers.into_headers();
228+
self
229+
}
230+
231+
/// Set the body
232+
pub fn body(&mut self, body: impl conversions::IntoBody) -> &mut Self {
233+
self.response.body = body.into_body();
234+
self
235+
}
236+
237+
pub fn build(&mut self) -> Response {
238+
std::mem::replace(&mut self.response, Response::new(200, Vec::new()))
239+
}
65240
}
66241

67242
impl std::hash::Hash for Method {
@@ -99,6 +274,23 @@ impl std::fmt::Display for Method {
99274
}
100275

101276
impl IncomingRequest {
277+
/// The incoming request Uri
278+
pub fn uri(&self) -> String {
279+
let scheme_and_authority =
280+
if let (Some(scheme), Some(authority)) = (self.scheme(), self.authority()) {
281+
let scheme = match &scheme {
282+
Scheme::Http => "http://",
283+
Scheme::Https => "https://",
284+
Scheme::Other(s) => s.as_str(),
285+
};
286+
format!("{scheme}{authority}")
287+
} else {
288+
String::new()
289+
};
290+
let path_and_query = self.path_with_query().unwrap_or_default();
291+
format!("{scheme_and_authority}{path_and_query}")
292+
}
293+
102294
/// Return a `Stream` from which the body of the specified request may be read.
103295
///
104296
/// # Panics
@@ -287,3 +479,29 @@ pub mod responses {
287479
Response::new(400, msg.map(|m| m.into_bytes()))
288480
}
289481
}
482+
483+
#[cfg(test)]
484+
mod tests {
485+
use super::*;
486+
487+
#[test]
488+
fn request_uri_parses() {
489+
let uri = "/hello?world=1";
490+
let req = Request::new(Method::Get, uri);
491+
assert_eq!(req.uri(), uri);
492+
assert_eq!(req.path(), "/hello");
493+
assert_eq!(req.query(), "world=1");
494+
495+
let uri = "http://localhost:3000/hello?world=1";
496+
let req = Request::new(Method::Get, uri);
497+
assert_eq!(req.uri(), uri);
498+
assert_eq!(req.path(), "/hello");
499+
assert_eq!(req.query(), "world=1");
500+
501+
let uri = "localhost:3000/hello?world=1";
502+
let req = Request::new(Method::Get, uri);
503+
assert_eq!(req.uri(), uri);
504+
assert_eq!(req.path(), "/hello");
505+
assert_eq!(req.query(), "world=1");
506+
}
507+
}

0 commit comments

Comments
 (0)