Skip to content
This repository was archived by the owner on Jun 7, 2024. It is now read-only.

Commit de643b2

Browse files
authored
Merge pull request #9 from wacker-dev/request
Add more convenience methods to the RequestBuilder
2 parents 0a76f18 + ff7b785 commit de643b2

File tree

6 files changed

+261
-51
lines changed

6 files changed

+261
-51
lines changed

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ rustdoc-args = ["--cfg", "docsrs"]
1818
anyhow = "1.0.83"
1919
wasi = "0.13.0"
2020
url = "2.5.0"
21-
serde = { version = "1.0.201", optional = true }
21+
serde = "1.0.201"
2222
serde_json = { version = "1.0.117", optional = true }
23+
serde_urlencoded = "0.7.1"
2324

2425
[features]
25-
json = ["dep:serde", "dep:serde_json"]
26+
json = ["dep:serde_json"]

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ making it easier to send http(s) requests in WASI components.
66
```rust
77
let resp = Client::new()
88
.post("https://httpbin.org/post")
9-
.body("hello".as_bytes())
109
.connect_timeout(Duration::from_secs(5))
11-
.send()
12-
.unwrap();
10+
.send()?;
1311

1412
println!("status code: {}", resp.status());
1513
```

src/lib.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,23 @@
44
//! making it easier to send http(s) requests in WASI components.
55
//!
66
//! ```
7+
//! # use anyhow::Result;
78
//! # use std::time::Duration;
89
//! # use wasi_http_client::Client;
9-
//! # fn run() {
10+
//! # fn run() -> Result<()> {
1011
//! let resp = Client::new()
1112
//! .post("https://httpbin.org/post")
12-
//! .body("hello".as_bytes())
1313
//! .connect_timeout(Duration::from_secs(5))
14-
//! .send()
15-
//! .unwrap();
14+
//! .send()?;
1615
//!
1716
//! println!("status code: {}", resp.status());
17+
//! # Ok(())
1818
//! # }
1919
//! ```
2020
2121
mod client;
2222
mod request;
2323
mod response;
2424

25-
pub use self::client::Client;
26-
pub use self::request::RequestBuilder;
27-
pub use self::response::Response;
25+
pub use self::{client::Client, request::RequestBuilder, response::Response};
2826
pub use wasi::http::types::Method;

src/request.rs

Lines changed: 239 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,291 @@
11
use crate::Response;
2-
use anyhow::{anyhow, Result};
2+
use anyhow::{anyhow, Error, Result};
3+
use serde::Serialize;
34
use std::time::Duration;
45
use url::Url;
56
use wasi::http::{
67
outgoing_handler,
7-
types::{FieldValue, Headers, Method, OutgoingBody, OutgoingRequest, RequestOptions, Scheme},
8+
types::{
9+
FieldKey, FieldValue, Headers, Method, OutgoingBody, OutgoingRequest, RequestOptions,
10+
Scheme,
11+
},
812
};
913

1014
pub struct RequestBuilder {
11-
method: Method,
12-
url: String,
13-
headers: Headers,
14-
body: Vec<u8>,
15-
connect_timeout: Option<u64>,
15+
// all errors generated while building the request will be deferred and returned when `send` the request.
16+
request: Result<Request>,
1617
}
1718

1819
impl RequestBuilder {
19-
pub fn new(method: Method, url: &str) -> Self {
20+
pub(crate) fn new(method: Method, url: &str) -> Self {
2021
Self {
21-
method,
22-
url: url.to_string(),
23-
headers: Headers::new(),
24-
body: vec![],
25-
connect_timeout: None,
22+
request: Url::parse(url)
23+
.map_or_else(|e| Err(Error::new(e)), |url| Ok(Request::new(method, url))),
2624
}
2725
}
2826

29-
pub fn header(self, key: &str, value: &str) -> Result<Self> {
30-
self.headers
31-
.set(&key.to_string(), &[FieldValue::from(value)])?;
32-
Ok(self)
27+
/// Add a header to the Request.
28+
///
29+
/// ```
30+
/// # use anyhow::Result;
31+
/// # use wasi_http_client::Client;
32+
/// # fn run() -> Result<()> {
33+
/// let resp = Client::new().get("https://httpbin.org/get")
34+
/// .header("Content-Type", "application/json")
35+
/// .send()?;
36+
/// # Ok(())
37+
/// # }
38+
/// ```
39+
pub fn header<K, V>(mut self, key: K, value: V) -> Self
40+
where
41+
K: Into<FieldKey>,
42+
V: Into<FieldValue>,
43+
{
44+
let mut err = None;
45+
if let Ok(ref mut req) = self.request {
46+
if let Err(e) = req.headers.set(&key.into(), &[value.into()]) {
47+
err = Some(e);
48+
}
49+
}
50+
if let Some(e) = err {
51+
self.request = Err(e.into());
52+
}
53+
self
3354
}
3455

56+
/// Add a set of headers to the Request.
57+
///
58+
/// Existing headers will be overwritten.
59+
///
60+
/// ```
61+
/// # use anyhow::Result;
62+
/// # use wasi_http_client::Client;
63+
/// # fn run() -> Result<()> {
64+
/// let resp = Client::new().get("https://httpbin.org/get")
65+
/// .headers([("Content-Type", "application/json"), ("Accept", "*/*")])
66+
/// .send()?;
67+
/// # Ok(())
68+
/// # }
69+
/// ```
70+
pub fn headers<K, V, I>(mut self, headers: I) -> Self
71+
where
72+
K: Into<FieldKey>,
73+
V: Into<FieldValue>,
74+
I: IntoIterator<Item = (K, V)>,
75+
{
76+
let mut err = None;
77+
if let Ok(ref mut req) = self.request {
78+
let entries: Vec<(FieldKey, FieldValue)> = headers
79+
.into_iter()
80+
.map(|(k, v)| (k.into(), v.into()))
81+
.collect();
82+
match Headers::from_list(&entries) {
83+
Ok(fields) => req.headers = fields,
84+
Err(e) => err = Some(e),
85+
}
86+
}
87+
if let Some(e) = err {
88+
self.request = Err(e.into());
89+
}
90+
self
91+
}
92+
93+
/// Modify the query string of the Request URL.
94+
///
95+
/// ```
96+
/// # use anyhow::Result;
97+
/// # use wasi_http_client::Client;
98+
/// # fn run() -> Result<()> {
99+
/// let resp = Client::new().get("https://httpbin.org/get")
100+
/// .query(&[("a", "b"), ("c", "d")])
101+
/// .send()?;
102+
/// # Ok(())
103+
/// # }
104+
/// ```
105+
pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Self {
106+
let mut err = None;
107+
if let Ok(ref mut req) = self.request {
108+
let mut pairs = req.url.query_pairs_mut();
109+
let serializer = serde_urlencoded::Serializer::new(&mut pairs);
110+
if let Err(e) = query.serialize(serializer) {
111+
err = Some(e);
112+
}
113+
}
114+
if let Some(e) = err {
115+
self.request = Err(e.into());
116+
}
117+
self
118+
}
119+
120+
/// Set the request body.
121+
///
122+
/// ```
123+
/// # use anyhow::Result;
124+
/// # use wasi_http_client::Client;
125+
/// # fn run() -> Result<()> {
126+
/// let resp = Client::new().post("https://httpbin.org/post")
127+
/// .body("hello".as_bytes())
128+
/// .send()?;
129+
/// # Ok(())
130+
/// # }
131+
/// ```
35132
pub fn body(mut self, body: &[u8]) -> Self {
36-
self.body = Vec::from(body);
133+
if let Ok(ref mut req) = self.request {
134+
req.body = Some(body.into());
135+
}
136+
self
137+
}
138+
139+
/// Send a JSON body.
140+
///
141+
/// # Optional
142+
///
143+
/// This requires the `json` feature enabled.
144+
///
145+
/// ```
146+
/// # use anyhow::Result;
147+
/// # use std::collections::HashMap;
148+
/// # use wasi_http_client::Client;
149+
/// # fn run() -> Result<()> {
150+
/// let resp = Client::new().post("https://httpbin.org/post")
151+
/// .json(&HashMap::from([("data", "hello")]))
152+
/// .send()?;
153+
/// # Ok(())
154+
/// # }
155+
/// ```
156+
#[cfg(feature = "json")]
157+
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
158+
pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self {
159+
let mut err = None;
160+
if let Ok(ref mut req) = self.request {
161+
if let Err(e) = req
162+
.headers
163+
.set(&"Content-Type".to_string(), &["application/json".into()])
164+
{
165+
err = Some(e.into());
166+
}
167+
match serde_json::to_vec(json) {
168+
Ok(data) => req.body = Some(data),
169+
Err(e) => err = Some(e.into()),
170+
}
171+
}
172+
if let Some(e) = err {
173+
self.request = Err(e);
174+
}
175+
self
176+
}
177+
178+
/// Send a form body.
179+
///
180+
/// ```
181+
/// # use anyhow::Result;
182+
/// # use wasi_http_client::Client;
183+
/// # fn run() -> Result<()> {
184+
/// let resp = Client::new().post("https://httpbin.org/post")
185+
/// .form(&[("a", "b"), ("c", "d")])
186+
/// .send()?;
187+
/// # Ok(())
188+
/// # }
189+
/// ```
190+
pub fn form<T: Serialize + ?Sized>(mut self, form: &T) -> Self {
191+
let mut err = None;
192+
if let Ok(ref mut req) = self.request {
193+
if let Err(e) = req.headers.set(
194+
&"Content-Type".to_string(),
195+
&["application/x-www-form-urlencoded".into()],
196+
) {
197+
err = Some(e.into());
198+
}
199+
match serde_urlencoded::to_string(form) {
200+
Ok(data) => req.body = Some(data.into()),
201+
Err(e) => err = Some(e.into()),
202+
}
203+
}
204+
if let Some(e) = err {
205+
self.request = Err(e);
206+
}
37207
self
38208
}
39209

210+
/// Set the timeout for the initial connect to the HTTP Server.
211+
///
212+
/// ```
213+
/// # use anyhow::Result;
214+
/// # use std::time::Duration;
215+
/// # use wasi_http_client::Client;
216+
/// # fn run() -> Result<()> {
217+
/// let resp = Client::new().post("https://httpbin.org/post")
218+
/// .connect_timeout(Duration::from_secs(5))
219+
/// .send()?;
220+
/// # Ok(())
221+
/// # }
222+
/// ```
40223
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
41-
self.connect_timeout = Some(timeout.as_nanos() as u64);
224+
if let Ok(ref mut req) = self.request {
225+
req.connect_timeout = Some(timeout.as_nanos() as u64);
226+
}
42227
self
43228
}
44229

230+
/// Send the Request, returning a [`Response`].
45231
pub fn send(self) -> Result<Response> {
232+
match self.request {
233+
Ok(req) => req.send(),
234+
Err(e) => Err(e),
235+
}
236+
}
237+
}
238+
239+
struct Request {
240+
method: Method,
241+
url: Url,
242+
headers: Headers,
243+
body: Option<Vec<u8>>,
244+
connect_timeout: Option<u64>,
245+
}
246+
247+
impl Request {
248+
fn new(method: Method, url: Url) -> Self {
249+
Self {
250+
method,
251+
url,
252+
headers: Headers::new(),
253+
body: None,
254+
connect_timeout: None,
255+
}
256+
}
257+
258+
fn send(self) -> Result<Response> {
46259
let req = OutgoingRequest::new(self.headers);
47260
req.set_method(&self.method)
48261
.map_err(|()| anyhow!("failed to set method"))?;
49262

50-
let url = Url::parse(self.url.as_str())?;
51-
let scheme = match url.scheme() {
263+
let scheme = match self.url.scheme() {
52264
"http" => Scheme::Http,
53265
"https" => Scheme::Https,
54266
other => Scheme::Other(other.to_string()),
55267
};
56268
req.set_scheme(Some(&scheme))
57269
.map_err(|()| anyhow!("failed to set scheme"))?;
58270

59-
req.set_authority(Some(url.authority()))
271+
req.set_authority(Some(self.url.authority()))
60272
.map_err(|()| anyhow!("failed to set authority"))?;
61273

62-
let path = match url.query() {
63-
Some(query) => format!("{}?{query}", url.path()),
64-
None => url.path().to_string(),
274+
let path = match self.url.query() {
275+
Some(query) => format!("{}?{query}", self.url.path()),
276+
None => self.url.path().to_string(),
65277
};
66278
req.set_path_with_query(Some(&path))
67279
.map_err(|()| anyhow!("failed to set path_with_query"))?;
68280

69281
let outgoing_body = req
70282
.body()
71283
.map_err(|_| anyhow!("outgoing request write failed"))?;
72-
if !self.body.is_empty() {
284+
if let Some(body) = self.body {
73285
let request_body = outgoing_body
74286
.write()
75287
.map_err(|_| anyhow!("outgoing request write failed"))?;
76-
request_body.blocking_write_and_flush(&self.body)?;
288+
request_body.blocking_write_and_flush(&body)?;
77289
}
78290
OutgoingBody::finish(outgoing_body, None)?;
79291

0 commit comments

Comments
 (0)