diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5a93bc2c..760b63bf 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -209,6 +209,26 @@ jobs: -p axum-macros --target armv5te-unknown-linux-musleabi + wasm32-unknown-unknown: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: wasm32-unknown-unknown + override: true + profile: minimal + - uses: Swatinem/rust-cache@v1 + - name: Check + uses: actions-rs/cargo@v1 + with: + command: check + args: > + --manifest-path ./examples/simple-router-wasm/Cargo.toml + --target wasm32-unknown-unknown + dependencies-are-sorted: runs-on: ubuntu-latest strategy: diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index a07b6715..9f6b60c4 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -33,9 +33,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 can be nested or merged into a `Router` with the same state type ([#1368]) - **changed:** `Router::nest` now only accepts `Router`s, the general-purpose `Service` nesting method has been renamed to `nest_service` ([#1368]) +- **added:** Support compiling to WASM. See the `simple-router-wasm` example + for more details ([#1382]) +- **breaking:** New `tokio` default feature needed for WASM support. If you + don't need WASM support but have `default_features = false` for other reasons + you likely need to re-enable the `tokio` feature ([#1382]) [#1368]: https://github.com/tokio-rs/axum/pull/1368 [#1371]: https://github.com/tokio-rs/axum/pull/1371 +[#1382]: https://github.com/tokio-rs/axum/pull/1382 [#1387]: https://github.com/tokio-rs/axum/pull/1387 [#1389]: https://github.com/tokio-rs/axum/pull/1389 [#1396]: https://github.com/tokio-rs/axum/pull/1396 diff --git a/axum/Cargo.toml b/axum/Cargo.toml index 69d3cf26..01a6ea3d 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" repository = "https://github.com/tokio-rs/axum" [features] -default = ["form", "http1", "json", "matched-path", "original-uri", "query", "tower-log"] +default = ["form", "http1", "json", "matched-path", "original-uri", "query", "tokio", "tower-log"] form = ["dep:serde_urlencoded"] http1 = ["hyper/http1"] http2 = ["hyper/http2"] @@ -22,8 +22,9 @@ matched-path = [] multipart = ["dep:multer"] original-uri = [] query = ["dep:serde_urlencoded"] +tokio = ["dep:tokio", "hyper/server", "hyper/tcp", "tower/make"] tower-log = ["tower/log"] -ws = ["dep:tokio-tungstenite", "dep:sha-1", "dep:base64"] +ws = ["tokio", "dep:tokio-tungstenite", "dep:sha-1", "dep:base64"] # Required for intra-doc links to resolve correctly __private_docs = ["tower/full", "tower-http/full"] @@ -36,7 +37,7 @@ bytes = "1.0" futures-util = { version = "0.3", default-features = false, features = ["alloc"] } http = "0.2.5" http-body = "0.4.4" -hyper = { version = "0.14.14", features = ["server", "tcp", "stream"] } +hyper = { version = "0.14.14", features = ["stream"] } itoa = "1.0.1" matchit = "0.6" memchr = "2.4.1" @@ -45,8 +46,7 @@ percent-encoding = "2.1" pin-project-lite = "0.2.7" serde = "1.0" sync_wrapper = "0.1.1" -tokio = { version = "1", features = ["time"] } -tower = { version = "0.4.13", default-features = false, features = ["util", "make"] } +tower = { version = "0.4.13", default-features = false, features = ["util"] } tower-http = { version = "0.3.0", features = ["util", "map-response-body"] } tower-layer = "0.3" tower-service = "0.3" @@ -60,6 +60,7 @@ serde_json = { version = "1.0", features = ["raw_value"], optional = true } serde_path_to_error = { version = "0.1.8", optional = true } serde_urlencoded = { version = "0.7", optional = true } sha-1 = { version = "0.10", optional = true } +tokio = { package = "tokio", version = "1.21", features = ["time"], optional = true } tokio-tungstenite = { version = "0.17.2", optional = true } [dev-dependencies] @@ -70,7 +71,7 @@ quickcheck_macros = "1.0" reqwest = { version = "0.11.11", default-features = false, features = ["json", "stream", "multipart"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.6.1", features = ["macros", "rt", "rt-multi-thread", "net", "test-util"] } +tokio = { package = "tokio", version = "1.21", features = ["macros", "rt", "rt-multi-thread", "net", "test-util"] } tokio-stream = "0.1" tracing = "0.1" uuid = { version = "1.0", features = ["serde", "v4"] } diff --git a/axum/src/ext_traits/service.rs b/axum/src/ext_traits/service.rs index b901a34b..e603d65f 100644 --- a/axum/src/ext_traits/service.rs +++ b/axum/src/ext_traits/service.rs @@ -1,4 +1,6 @@ -use crate::{extract::connect_info::IntoMakeServiceWithConnectInfo, routing::IntoMakeService}; +#[cfg(feature = "tokio")] +use crate::extract::connect_info::IntoMakeServiceWithConnectInfo; +use crate::routing::IntoMakeService; use tower_service::Service; /// Extension trait that adds additional methods to any [`Service`]. @@ -26,6 +28,7 @@ pub trait ServiceExt: Service + Sized { /// ["Rewriting request URI in middleware"]: crate::middleware#rewriting-request-uri-in-middleware /// [`Router`]: crate::Router /// [`ConnectInfo`]: crate::extract::connect_info::ConnectInfo + #[cfg(feature = "tokio")] fn into_make_service_with_connect_info(self) -> IntoMakeServiceWithConnectInfo; } @@ -37,6 +40,7 @@ where IntoMakeService::new(self) } + #[cfg(feature = "tokio")] fn into_make_service_with_connect_info(self) -> IntoMakeServiceWithConnectInfo { IntoMakeServiceWithConnectInfo::new(self) } diff --git a/axum/src/extract/mod.rs b/axum/src/extract/mod.rs index 698f8b17..9dde11f5 100644 --- a/axum/src/extract/mod.rs +++ b/axum/src/extract/mod.rs @@ -2,6 +2,7 @@ use http::header::{self, HeaderMap}; +#[cfg(feature = "tokio")] pub mod connect_info; pub mod path; pub mod rejection; @@ -23,7 +24,6 @@ pub use axum_macros::{FromRequest, FromRequestParts}; #[doc(inline)] #[allow(deprecated)] pub use self::{ - connect_info::ConnectInfo, host::Host, path::Path, raw_query::RawQuery, @@ -31,6 +31,10 @@ pub use self::{ state::State, }; +#[doc(inline)] +#[cfg(feature = "tokio")] +pub use self::connect_info::ConnectInfo; + #[doc(no_inline)] #[cfg(feature = "json")] pub use crate::Json; diff --git a/axum/src/extract/ws.rs b/axum/src/extract/ws.rs index 212abdad..4156a16c 100644 --- a/axum/src/extract/ws.rs +++ b/axum/src/extract/ws.rs @@ -72,7 +72,7 @@ //! If you need to read and write concurrently from a [`WebSocket`] you can use //! [`StreamExt::split`]: //! -//! ``` +//! ```rust,no_run //! use axum::{Error, extract::ws::{WebSocket, Message}}; //! use futures::{sink::SinkExt, stream::{StreamExt, SplitSink, SplitStream}}; //! diff --git a/axum/src/handler/mod.rs b/axum/src/handler/mod.rs index 4eb4c2b9..dd82b706 100644 --- a/axum/src/handler/mod.rs +++ b/axum/src/handler/mod.rs @@ -35,9 +35,11 @@ //! #![doc = include_str!("../docs/debugging_handler_type_errors.md")] +#[cfg(feature = "tokio")] +use crate::extract::connect_info::IntoMakeServiceWithConnectInfo; use crate::{ body::Body, - extract::{connect_info::IntoMakeServiceWithConnectInfo, FromRequest, FromRequestParts}, + extract::{FromRequest, FromRequestParts}, response::{IntoResponse, Response}, routing::IntoMakeService, }; @@ -318,6 +320,7 @@ pub trait HandlerWithoutStateExt: Handler { /// See [`WithState::into_make_service_with_connect_info`] for more details. /// /// [`MakeService`]: tower::make::MakeService + #[cfg(feature = "tokio")] fn into_make_service_with_connect_info( self, ) -> IntoMakeServiceWithConnectInfo, C>; @@ -335,6 +338,7 @@ where self.with_state(()).into_make_service() } + #[cfg(feature = "tokio")] fn into_make_service_with_connect_info( self, ) -> IntoMakeServiceWithConnectInfo, C> { diff --git a/axum/src/handler/with_state.rs b/axum/src/handler/with_state.rs index 71ca82bf..662b51b9 100644 --- a/axum/src/handler/with_state.rs +++ b/axum/src/handler/with_state.rs @@ -1,5 +1,7 @@ use super::{Handler, IntoService}; -use crate::{extract::connect_info::IntoMakeServiceWithConnectInfo, routing::IntoMakeService}; +#[cfg(feature = "tokio")] +use crate::extract::connect_info::IntoMakeServiceWithConnectInfo; +use crate::routing::IntoMakeService; use http::Request; use std::task::{Context, Poll}; use tower_service::Service; @@ -95,6 +97,7 @@ impl WithState { /// /// [`MakeService`]: tower::make::MakeService /// [`Router::into_make_service_with_connect_info`]: crate::routing::Router::into_make_service_with_connect_info + #[cfg(feature = "tokio")] pub fn into_make_service_with_connect_info( self, ) -> IntoMakeServiceWithConnectInfo, C> { diff --git a/axum/src/lib.rs b/axum/src/lib.rs index 171df9e2..97aa56b9 100644 --- a/axum/src/lib.rs +++ b/axum/src/lib.rs @@ -350,6 +350,7 @@ //! `matched-path` | Enables capturing of every request's router path and the [`MatchedPath`] extractor | Yes //! `multipart` | Enables parsing `multipart/form-data` requests with [`Multipart`] | No //! `original-uri` | Enables capturing of every request's original URI and the [`OriginalUri`] extractor | Yes +//! `tokio` | Enables `tokio` as a dependency and `axum::Server`, `SSE` and `extract::connect_info` types. | Yes //! `tower-log` | Enables `tower`'s `log` feature | Yes //! `ws` | Enables WebSockets support via [`extract::ws`] | No //! `form` | Enables the `Form` extractor | Yes @@ -461,6 +462,7 @@ pub use async_trait::async_trait; pub use headers; #[doc(no_inline)] pub use http; +#[cfg(feature = "tokio")] #[doc(no_inline)] pub use hyper::Server; diff --git a/axum/src/response/mod.rs b/axum/src/response/mod.rs index 5cf19c04..3bfcead3 100644 --- a/axum/src/response/mod.rs +++ b/axum/src/response/mod.rs @@ -5,6 +5,7 @@ use http::{header, HeaderValue}; mod redirect; +#[cfg(feature = "tokio")] pub mod sse; #[doc(no_inline)] @@ -28,7 +29,11 @@ pub use axum_core::response::{ }; #[doc(inline)] -pub use self::{redirect::Redirect, sse::Sse}; +pub use self::redirect::Redirect; + +#[doc(inline)] +#[cfg(feature = "tokio")] +pub use sse::Sse; /// An HTML response. /// diff --git a/axum/src/routing/method_routing.rs b/axum/src/routing/method_routing.rs index f7fb4042..fffb6339 100644 --- a/axum/src/routing/method_routing.rs +++ b/axum/src/routing/method_routing.rs @@ -1,10 +1,11 @@ //! Route to services and handlers based on HTTP methods. use super::IntoMakeService; +#[cfg(feature = "tokio")] +use crate::extract::connect_info::IntoMakeServiceWithConnectInfo; use crate::{ body::{Body, Bytes, HttpBody}, error_handling::{HandleError, HandleErrorLayer}, - extract::connect_info::IntoMakeServiceWithConnectInfo, handler::{Handler, IntoServiceStateInExtension}, http::{Method, Request, StatusCode}, response::Response, @@ -689,6 +690,7 @@ where /// /// [`MakeService`]: tower::make::MakeService /// [`Router::into_make_service_with_connect_info`]: crate::routing::Router::into_make_service_with_connect_info + #[cfg(feature = "tokio")] pub fn into_make_service_with_connect_info(self) -> IntoMakeServiceWithConnectInfo { IntoMakeServiceWithConnectInfo::new(self) } @@ -1186,6 +1188,7 @@ impl WithState { /// See [`MethodRouter::into_make_service_with_connect_info`] for more details. /// /// [`MakeService`]: tower::make::MakeService + #[cfg(feature = "tokio")] pub fn into_make_service_with_connect_info(self) -> IntoMakeServiceWithConnectInfo { IntoMakeServiceWithConnectInfo::new(self) } diff --git a/axum/src/routing/mod.rs b/axum/src/routing/mod.rs index cb20c9e3..f71dc0f4 100644 --- a/axum/src/routing/mod.rs +++ b/axum/src/routing/mod.rs @@ -1,9 +1,10 @@ //! Routing between [`Service`]s and handlers. use self::not_found::NotFound; +#[cfg(feature = "tokio")] +use crate::extract::connect_info::IntoMakeServiceWithConnectInfo; use crate::{ body::{Body, HttpBody}, - extract::connect_info::IntoMakeServiceWithConnectInfo, handler::{BoxedHandler, Handler}, util::try_downcast, Extension, @@ -547,6 +548,7 @@ where } #[doc = include_str!("../docs/routing/into_make_service_with_connect_info.md")] + #[cfg(feature = "tokio")] pub fn into_make_service_with_connect_info( self, ) -> IntoMakeServiceWithConnectInfo, C> { diff --git a/examples/simple-router-wasm/Cargo.toml b/examples/simple-router-wasm/Cargo.toml new file mode 100644 index 00000000..03de581a --- /dev/null +++ b/examples/simple-router-wasm/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "example-simple-router-wasm" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +# `default-features = false` to not depend on tokio features which don't support wasm +# you can still pull in tokio manually and only add features that tokio supports for wasm +axum = { path = "../../axum", default-features = false } +futures-executor = "0.3.21" +http = "0.2.7" +tower-service = "0.3.1" diff --git a/examples/simple-router-wasm/src/main.rs b/examples/simple-router-wasm/src/main.rs new file mode 100644 index 00000000..5b1e7907 --- /dev/null +++ b/examples/simple-router-wasm/src/main.rs @@ -0,0 +1,51 @@ +//! Run with +//! +//! ```not_rust +//! cd examples && cargo run -p example-simple-router-wasm +//! ``` +//! +//! This example shows what using axum in a wasm context might look like. This example should +//! always compile with `--target wasm32-unknown-unknown`. +//! +//! [`mio`](https://docs.rs/mio/latest/mio/index.html), tokio's IO layer, does not support the +//! `wasm32-unknown-unknown` target which is why this crate requires `default-features = false` +//! for axum. +//! +//! Most serverless runtimes expect an exported function that takes in a single request and returns +//! a single response, much like axum's `Handler` trait. In this example, the handler function is +//! `app` with `main` acting as the serverless runtime which originally receives the request and +//! calls the app function. +//! +//! We can use axum's routing, extractors, tower services, and everything else to implement +//! our serverless function, even though we are running axum in a wasm context. + +use axum::{ + response::{Html, Response}, + routing::get, + Router, +}; +use futures_executor::block_on; +use http::Request; +use tower_service::Service; + +fn main() { + let request: Request = Request::builder() + .uri("https://serverless.example/api/") + .body("Some Body Data".into()) + .unwrap(); + + let response: Response = block_on(app(request)); + assert_eq!(200, response.status()); +} + +#[allow(clippy::let_and_return)] +async fn app(request: Request) -> Response { + let mut router = Router::new().route("/api/", get(index)).into_service(); + + let response = router.call(request).await.unwrap(); + response +} + +async fn index() -> Html<&'static str> { + Html("

Hello, World!

") +}