diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15fd067..a44c2ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,16 +6,14 @@ on: - 'release-**' pull_request: jobs: - ci: - name: Lint and test + lint: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@1.79.0 + uses: dtolnay/rust-toolchain@1.82.0 with: - targets: wasm32-wasip1 components: clippy, rustfmt - name: Re-vendor WIT run: | @@ -25,5 +23,21 @@ jobs: run: cargo fmt --all -- --check - name: cargo clippy run: cargo clippy --all-targets --all-features -- -D warnings + test: + needs: lint + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + rust: [ "1.81", "1.82" ] + targets: [ "wasm32-wasip1", "wasm32-wasip2" ] + name: Test on Rust ${{ matrix.rust }} + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain + with: + toolchain: ${{ matrix.rust }} + targets: ${{ matrix.targets }} - name: cargo test run: cargo test diff --git a/test-programs/artifacts/Cargo.toml b/test-programs/artifacts/Cargo.toml index 1191352..2850b44 100644 --- a/test-programs/artifacts/Cargo.toml +++ b/test-programs/artifacts/Cargo.toml @@ -7,6 +7,7 @@ publish = false [build-dependencies] anyhow.workspace = true cargo_metadata = "0.18.1" -wit-component = "0.208.1" +wit-component = "0.219.1" heck = "0.5.0" -wasi-preview1-component-adapter-provider = "23.0.1" +wasi-preview1-component-adapter-provider = "25.0.1" +version_check = "0.9.5" diff --git a/test-programs/artifacts/build.rs b/test-programs/artifacts/build.rs index d5f4112..dd17c39 100644 --- a/test-programs/artifacts/build.rs +++ b/test-programs/artifacts/build.rs @@ -10,11 +10,19 @@ fn main() -> Result<()> { let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); println!("cargo::rerun-if-changed=../src"); + println!("cargo::rerun-if-changed=../../waki"); + + let wasip2 = version_check::is_min_version("1.82.0").unwrap_or(false); + let wasi_target = if wasip2 { + "wasm32-wasip2" + } else { + "wasm32-wasip1" + }; let status = Command::new("cargo") .arg("build") .arg("--package=test-programs") - .arg("--target=wasm32-wasip1") + .arg(format!("--target={}", wasi_target)) .env("CARGO_TARGET_DIR", &out_dir) .env("CARGO_PROFILE_DEV_DEBUG", "1") .status()?; @@ -37,21 +45,26 @@ fn main() -> Result<()> { for target in targets { let camel = target.to_shouty_snake_case(); let wasm = out_dir - .join("wasm32-wasip1") + .join(wasi_target) .join("debug") .join(format!("{target}.wasm")); - let adapter = match target.as_str() { - s if s.starts_with("client_") => { - wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_COMMAND_ADAPTER - } - s if s.starts_with("server_") => { - wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_PROXY_ADAPTER - } - other => panic!("unknown type {other}"), + let path = if wasip2 { + wasm + } else { + compile_component( + &wasm, + match target.as_str() { + s if s.starts_with("client_") => { + wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_COMMAND_ADAPTER + } + s if s.starts_with("server_") => { + wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_PROXY_ADAPTER + } + other => panic!("unknown type {other}"), + }, + )? }; - - let path = compile_component(&wasm, adapter)?; generated_code += &format!("pub const {camel}_COMPONENT: &str = {path:?};\n"); } diff --git a/waki/Cargo.toml b/waki/Cargo.toml index 16c26fa..db77e81 100644 --- a/waki/Cargo.toml +++ b/waki/Cargo.toml @@ -19,9 +19,8 @@ waki-macros.workspace = true anyhow.workspace = true serde.workspace = true wit-bindgen = "0.34.0" -url = "2.5.2" +form_urlencoded = "1.2.1" http = "1.1.0" -serde_urlencoded = "0.7.1" serde_json = { version = "1.0.128", optional = true } mime = { version = "0.3.17", optional = true } mime_guess = { version = "2.0.5", optional = true } diff --git a/waki/src/common/request_and_response.rs b/waki/src/common/request_and_response.rs index 3972c58..72c6c51 100644 --- a/waki/src/common/request_and_response.rs +++ b/waki/src/common/request_and_response.rs @@ -7,6 +7,7 @@ use crate::{ }; use anyhow::{anyhow, Error, Result}; use serde::Serialize; +use std::borrow::Borrow; use std::collections::HashMap; macro_rules! impl_common_get_methods { @@ -68,7 +69,7 @@ macro_rules! impl_common_get_methods { /// Parse the body as form data. pub fn form(self) -> Result> { - Ok(serde_urlencoded::from_bytes(self.body()?.as_ref())?) + Ok(form_urlencoded::parse(self.body()?.as_ref()).into_owned().collect()) } /// Parse the body as multipart/form-data. @@ -223,23 +224,24 @@ macro_rules! impl_common_set_methods { /// # use waki::ResponseBuilder; /// # fn run() { /// # let r = ResponseBuilder::new(); - /// r.form(&[("a", "b"), ("c", "d")]); + /// r.form([("a", "b"), ("c", "d")]); /// # } /// ``` - pub fn form(mut self, form: &T) -> Self { - let mut err = None; + pub fn form(mut self, form: I) -> Self + where + K: AsRef, + V: AsRef, + I: IntoIterator, + I::Item: Borrow<(K, V)>, + { if let Ok(ref mut inner) = self.inner { inner.headers.insert( CONTENT_TYPE, "application/x-www-form-urlencoded".parse().unwrap(), ); - match serde_urlencoded::to_string(form) { - Ok(data) => inner.body = Body::Bytes(data.into()), - Err(e) => err = Some(e.into()), - } - } - if let Some(e) = err { - self.inner = Err(e); + let mut serializer = form_urlencoded::Serializer::new(String::new()); + serializer.extend_pairs(form); + inner.body = Body::Bytes(serializer.finish().into()) } self } diff --git a/waki/src/common/scheme.rs b/waki/src/common/scheme.rs index 614a57b..1cda07e 100644 --- a/waki/src/common/scheme.rs +++ b/waki/src/common/scheme.rs @@ -1,5 +1,4 @@ use crate::bindings::wasi::http::types::Scheme; -use std::fmt::{Display, Formatter, Result}; impl From<&str> for Scheme { fn from(s: &str) -> Self { @@ -11,12 +10,14 @@ impl From<&str> for Scheme { } } -impl Display for Scheme { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - f.write_str(match self { - Scheme::Http => "http", - Scheme::Https => "https", - Scheme::Other(s) => s, - }) +impl TryInto for Scheme { + type Error = http::uri::InvalidUri; + + fn try_into(self) -> Result { + match self { + Scheme::Http => Ok(http::uri::Scheme::HTTP), + Scheme::Https => Ok(http::uri::Scheme::HTTPS), + Scheme::Other(s) => s.as_str().try_into(), + } } } diff --git a/waki/src/request.rs b/waki/src/request.rs index 453d065..98f79c8 100644 --- a/waki/src/request.rs +++ b/waki/src/request.rs @@ -1,7 +1,7 @@ use crate::{ bindings::wasi::http::{ outgoing_handler, - types::{IncomingRequest, OutgoingBody, OutgoingRequest, RequestOptions, Scheme}, + types::{IncomingRequest, OutgoingBody, OutgoingRequest, RequestOptions}, }, body::Body, header::HeaderMap, @@ -9,10 +9,13 @@ use crate::{ }; use anyhow::{anyhow, Error, Result}; -use serde::Serialize; +use http::{ + uri::{Parts, PathAndQuery}, + Uri, +}; +use std::borrow::Borrow; use std::collections::HashMap; use std::time::Duration; -use url::Url; pub struct RequestBuilder { // all errors generated while building the request will be deferred and returned when `send` the request. @@ -20,32 +23,48 @@ pub struct RequestBuilder { } impl RequestBuilder { - pub fn new(method: Method, url: &str) -> Self { + pub fn new(method: Method, uri: &str) -> Self { Self { - inner: Url::parse(url) - .map_or_else(|e| Err(Error::new(e)), |url| Ok(Request::new(method, url))), + inner: uri.parse::().map_or_else( + |e| Err(Error::new(e)), + |uri| Ok(Request::new(method, uri.into_parts())), + ), } } - /// Modify the query string of the Request URL. + /// Modify the query string of the Request URI. /// /// ``` /// # use anyhow::Result; /// # use waki::Client; /// # fn run() -> Result<()> { /// let resp = Client::new().get("https://httpbin.org/get") - /// .query(&[("a", "b"), ("c", "d")]) + /// .query([("a", "b"), ("c", "d")]) /// .send()?; /// # Ok(()) /// # } /// ``` - pub fn query(mut self, query: &T) -> Self { + pub fn query(mut self, args: I) -> Self + where + K: AsRef, + V: AsRef, + I: IntoIterator, + I::Item: Borrow<(K, V)>, + { let mut err = None; if let Ok(ref mut req) = self.inner { - let mut pairs = req.url.query_pairs_mut(); - let serializer = serde_urlencoded::Serializer::new(&mut pairs); - if let Err(e) = query.serialize(serializer) { - err = Some(e.into()); + let (path, query) = match &req.uri.path_and_query { + Some(path_and_query) => ( + path_and_query.path(), + path_and_query.query().unwrap_or_default(), + ), + None => ("", ""), + }; + let mut serializer = form_urlencoded::Serializer::new(query.to_string()); + serializer.extend_pairs(args); + match PathAndQuery::try_from(format!("{}?{}", path, serializer.finish())) { + Ok(path_and_query) => req.uri.path_and_query = Some(path_and_query), + Err(e) => err = Some(e.into()), } } if let Some(e) = err { @@ -90,7 +109,7 @@ impl RequestBuilder { pub struct Request { method: Method, - url: Url, + uri: Parts, pub(crate) headers: HeaderMap, pub(crate) body: Body, connect_timeout: Option, @@ -101,16 +120,29 @@ impl TryFrom for Request { fn try_from(req: IncomingRequest) -> std::result::Result { let method = req.method(); - let url = Url::parse( - format!( - "{}://{}{}", - req.scheme().unwrap_or(Scheme::Http), - req.authority().unwrap_or("localhost".into()), - req.path_with_query().unwrap_or_default() - ) - .as_str(), - ) - .unwrap(); + + let mut parts = Parts::default(); + if let Some(scheme) = req.scheme() { + parts.scheme = Some( + scheme + .try_into() + .map_err(|_| ErrorCode::HttpRequestUriInvalid)?, + ); + } + if let Some(authority) = req.authority() { + parts.authority = Some( + authority + .try_into() + .map_err(|_| ErrorCode::HttpRequestUriInvalid)?, + ); + } + if let Some(path_with_query) = req.path_with_query() { + parts.path_and_query = Some( + path_with_query + .try_into() + .map_err(|_| ErrorCode::HttpRequestUriInvalid)?, + ); + } let headers = req .headers_map() @@ -121,7 +153,7 @@ impl TryFrom for Request { Ok(Self { method, - url, + uri: parts, headers, body: Body::Stream(incoming_body.into()), connect_timeout: None, @@ -130,23 +162,18 @@ impl TryFrom for Request { } impl Request { - pub fn new(method: Method, url: Url) -> Self { + pub fn new(method: Method, uri: Parts) -> Self { Self { method, - url, + uri, headers: HeaderMap::new(), body: Body::Bytes(vec![]), connect_timeout: None, } } - pub fn builder(method: Method, url: &str) -> RequestBuilder { - RequestBuilder::new(method, url) - } - - /// Get the full URL of the request. - pub fn url(&self) -> Url { - self.url.clone() + pub fn builder(method: Method, uri: &str) -> RequestBuilder { + RequestBuilder::new(method, uri) } /// Get the HTTP method of the request. @@ -156,32 +183,40 @@ impl Request { /// Get the path of the request. pub fn path(&self) -> &str { - self.url.path() + match &self.uri.path_and_query { + Some(path_and_query) => path_and_query.path(), + None => "", + } } /// Get the query string of the request. pub fn query(&self) -> HashMap { - let query_pairs = self.url.query_pairs(); - query_pairs.into_owned().collect() + match &self.uri.path_and_query { + Some(path_and_query) => { + let query_pairs = + form_urlencoded::parse(path_and_query.query().unwrap_or_default().as_bytes()); + query_pairs.into_owned().collect() + } + None => HashMap::default(), + } } fn send(self) -> Result { let req = OutgoingRequest::new(self.headers.try_into()?); req.set_method(&self.method) .map_err(|()| anyhow!("failed to set method"))?; - - req.set_scheme(Some(&self.url.scheme().into())) - .map_err(|()| anyhow!("failed to set scheme"))?; - - req.set_authority(Some(self.url.authority())) - .map_err(|()| anyhow!("failed to set authority"))?; - - let path = match self.url.query() { - Some(query) => format!("{}?{query}", self.url.path()), - None => self.url.path().to_string(), - }; - req.set_path_with_query(Some(&path)) - .map_err(|()| anyhow!("failed to set path_with_query"))?; + if let Some(scheme) = self.uri.scheme { + req.set_scheme(Some(&scheme.as_str().into())) + .map_err(|()| anyhow!("failed to set scheme"))?; + } + if let Some(authority) = self.uri.authority { + req.set_authority(Some(authority.as_str())) + .map_err(|()| anyhow!("failed to set authority"))?; + } + if let Some(path_and_query) = self.uri.path_and_query { + req.set_path_with_query(Some(path_and_query.as_str())) + .map_err(|()| anyhow!("failed to set path_with_query"))?; + } let options = RequestOptions::new(); options