use std::convert::TryFrom;
use crate::{
cf::Cf, error::Error, headers::Headers, http::Method, ByteStream, FormData, RequestInit, Result,
};
use serde::de::DeserializeOwned;
use std::borrow::Cow;
use url::{form_urlencoded::Parse, Url};
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use worker_sys::ext::RequestExt;
#[derive(Debug)]
pub struct Request {
method: Method,
path: String,
headers: Headers,
cf: Option<Cf>,
edge_request: web_sys::Request,
body_used: bool,
immutable: bool,
}
impl From<web_sys::Request> for Request {
fn from(req: web_sys::Request) -> Self {
Self {
method: req.method().into(),
path: Url::parse(&req.url())
.map(|u| u.path().into())
.unwrap_or_else(|_| {
let u = req.url();
if !u.starts_with('/') {
return "/".to_string() + &u;
}
u
}),
headers: Headers(req.headers()),
cf: req.cf().map(Into::into),
edge_request: req,
body_used: false,
immutable: true,
}
}
}
impl TryFrom<Request> for web_sys::Request {
type Error = Error;
fn try_from(req: Request) -> Result<Self> {
req.inner().clone().map_err(Error::from)
}
}
impl TryFrom<&Request> for web_sys::Request {
type Error = Error;
fn try_from(req: &Request) -> Result<Self> {
req.inner().clone().map_err(Error::from)
}
}
impl Request {
pub fn new(uri: &str, method: Method) -> Result<Self> {
web_sys::Request::new_with_str_and_init(
uri,
web_sys::RequestInit::new().method(method.as_ref()),
)
.map(|req| {
let mut req: Request = req.into();
req.immutable = false;
req
})
.map_err(|e| {
Error::JsError(
e.as_string()
.unwrap_or_else(|| "invalid URL or method for Request".to_string()),
)
})
}
pub fn new_with_init(uri: &str, init: &RequestInit) -> Result<Self> {
web_sys::Request::new_with_str_and_init(uri, &init.into())
.map(|req| {
let mut req: Request = req.into();
req.immutable = false;
req
})
.map_err(|e| {
Error::JsError(
e.as_string()
.unwrap_or_else(|| "invalid URL or options for Request".to_string()),
)
})
}
pub async fn json<B: DeserializeOwned>(&mut self) -> Result<B> {
if !self.body_used {
self.body_used = true;
return JsFuture::from(self.edge_request.json()?)
.await
.map_err(|e| {
Error::JsError(
e.as_string()
.unwrap_or_else(|| "failed to get JSON for body value".into()),
)
})
.and_then(|val| serde_wasm_bindgen::from_value(val).map_err(Error::from));
}
Err(Error::BodyUsed)
}
pub async fn text(&mut self) -> Result<String> {
if !self.body_used {
self.body_used = true;
return JsFuture::from(self.edge_request.text()?)
.await
.map(|val| val.as_string().unwrap())
.map_err(|e| {
Error::JsError(
e.as_string()
.unwrap_or_else(|| "failed to get text for body value".into()),
)
});
}
Err(Error::BodyUsed)
}
pub async fn bytes(&mut self) -> Result<Vec<u8>> {
if !self.body_used {
self.body_used = true;
return JsFuture::from(self.edge_request.array_buffer()?)
.await
.map(|val| js_sys::Uint8Array::new(&val).to_vec())
.map_err(|e| {
Error::JsError(
e.as_string()
.unwrap_or_else(|| "failed to read array buffer from request".into()),
)
});
}
Err(Error::BodyUsed)
}
pub async fn form_data(&mut self) -> Result<FormData> {
if !self.body_used {
self.body_used = true;
return JsFuture::from(self.edge_request.form_data()?)
.await
.map(|val| val.into())
.map_err(|e| {
Error::JsError(
e.as_string()
.unwrap_or_else(|| "failed to get form data from request".into()),
)
});
}
Err(Error::BodyUsed)
}
pub fn stream(&mut self) -> Result<ByteStream> {
if self.body_used {
return Err(Error::BodyUsed);
}
self.body_used = true;
let stream = self
.edge_request
.body()
.ok_or_else(|| Error::RustError("no body for request".into()))?;
let stream = wasm_streams::ReadableStream::from_raw(stream.dyn_into().unwrap());
Ok(ByteStream {
inner: stream.into_stream(),
})
}
pub fn headers(&self) -> &Headers {
&self.headers
}
pub fn headers_mut(&mut self) -> Result<&mut Headers> {
if self.immutable {
return Err(Error::JsError(
"Cannot get a mutable reference to an immutable headers object.".into(),
));
}
Ok(&mut self.headers)
}
pub fn cf(&self) -> &Cf {
self.cf.as_ref().unwrap()
}
pub fn method(&self) -> Method {
self.method.clone()
}
pub fn path(&self) -> String {
self.path.clone()
}
pub fn path_mut(&mut self) -> Result<&mut String> {
if self.immutable {
return Err(Error::JsError(
"Cannot get a mutable reference to an immutable path.".into(),
));
}
Ok(&mut self.path)
}
pub fn url(&self) -> Result<Url> {
let url = self.edge_request.url();
url.parse()
.map_err(|e| Error::RustError(format!("failed to parse Url from {e}: {url}")))
}
#[allow(clippy::should_implement_trait)]
pub fn clone(&self) -> Result<Self> {
self.edge_request
.clone()
.map(|req| req.into())
.map_err(Error::from)
}
pub fn clone_mut(&self) -> Result<Self> {
let mut req: Request = web_sys::Request::new_with_request(&self.edge_request)?.into();
req.immutable = false;
Ok(req)
}
pub fn inner(&self) -> &web_sys::Request {
&self.edge_request
}
}
pub trait UrlExt {
fn param<'a>(&'a self, key: &'a str) -> Option<Cow<'a, str>> {
self.param_iter(key).next()
}
fn param_iter<'a>(&'a self, key: &'a str) -> ParamIter<'a>;
}
impl UrlExt for Url {
fn param_iter<'a>(&'a self, key: &'a str) -> ParamIter<'a> {
ParamIter {
inner: self.query_pairs(),
key,
}
}
}
pub struct ParamIter<'a> {
inner: Parse<'a>,
key: &'a str,
}
impl<'a> Iterator for ParamIter<'a> {
type Item = Cow<'a, str>;
fn next(&mut self) -> Option<Self::Item> {
let key = self.key;
Some(self.inner.find(|(k, _)| k == key)?.1)
}
}
#[test]
fn url_param_works() {
let url = Url::parse("https://example.com/foo.html?a=foo&b=bar&a=baz").unwrap();
assert_eq!(url.param("a").as_deref(), Some("foo"));
assert_eq!(url.param("b").as_deref(), Some("bar"));
assert_eq!(url.param("c").as_deref(), None);
let mut a_values = url.param_iter("a");
assert_eq!(a_values.next().as_deref(), Some("foo"));
assert_eq!(a_values.next().as_deref(), Some("baz"));
assert_eq!(a_values.next(), None);
}
#[test]
fn clone_mut_works() {
let req = Request::new(
"https://example.com/foo.html?a=foo&b=bar&a=baz",
crate::Method::Get,
)
.unwrap();
assert!(!req.immutable);
let mut_req = req.clone_mut().unwrap();
assert!(mut_req.immutable);
}