diff --git a/Cargo.toml b/Cargo.toml index 374ea43c..2c74307b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rspc" description = "A blazing fast and easy to use TRPC server for Rust." -version = "0.1.3" +version = "0.1.4" authors = ["Oscar Beaumont "] edition = "2021" license = "MIT" @@ -41,6 +41,9 @@ mac_address = ["specta/mac_address"] bit-vec = ["specta/bit-vec"] bson = ["specta/bson"] +# Internal (doesn't follow semver so don't use directly) +internal_axum_07 = ["dep:axum"] + [dependencies] specta = { version = "1.0.5", features = ["serde", "typescript"] } httpz = { version = "0.0.6", optional = true } # TODO: Move back to crates.io release @@ -52,6 +55,10 @@ tokio = { version = "1.36.0", features = ["sync", "rt", "macros"] } tauri = { version = "1.6.1", optional = true } tracing = { version = "0.1.40", optional = true } + +# TODO: This is temporary. It won't stay in the core long term. +axum = { version = "0.7.4", optional = true } + [dev-dependencies] async-stream = "0.3.5" diff --git a/crates/axum/Cargo.toml b/crates/axum/Cargo.toml index dc2e8dbc..f4fa2be6 100644 --- a/crates/axum/Cargo.toml +++ b/crates/axum/Cargo.toml @@ -13,4 +13,4 @@ categories = ["web-programming", "asynchronous"] [dependencies] axum = "0.7.4" httpz = { version = "0.0.6", features = ["axum"] } -rspc = { version = "0.1.3", path = "../.." } +rspc = { version = "0.1.4", path = "../..", features = ["internal_axum_07"] } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 2c6c16d2..eaad857b 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -6,13 +6,21 @@ publish = false [dependencies] rspc = { path = "../", features = ["axum"] } +rspc-axum = { path = "../crates/axum" } async-stream = "0.3.5" axum = "0.7.4" chrono = { version = "0.4.35", features = ["serde"] } serde = { version = "1.0.197", features = ["derive"] } time = "0.3.34" -tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros", "time", "sync"], default-features = false } -tower-cookies = "0.10.0" -tower-http = { version = "0.5.2", default-features = false, features = ["cors"] } +tokio = { version = "1.36.0", features = [ + "rt-multi-thread", + "macros", + "time", + "sync", +], default-features = false } +tower-cookies = { version = "0.10.0", features = ["axum-core"] } +tower-http = { version = "0.5.2", default-features = false, features = [ + "cors", +] } uuid = { version = "1.7.0", features = ["v4", "serde"] } serde_json = "1.0.114" diff --git a/examples/src/bin/axum.rs b/examples/src/bin/axum.rs index 16d0312d..2eecf62c 100644 --- a/examples/src/bin/axum.rs +++ b/examples/src/bin/axum.rs @@ -36,24 +36,22 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - router - .endpoint(|mut req: Request| { - // Official rspc API - println!("Client requested operation '{}'", req.uri().path()); + rspc_axum::endpoint(router.endpoint(|mut req: Request| { + // Official rspc API + println!("Client requested operation '{}'", req.uri().path()); - // Deprecated Axum extractors - this API will be removed in the future - // The first generic is the Axum extractor and the second is the type of your Axum state. - // If the state generic is wrong you will get a **RUNTIME** error so be careful! - // TODO: Be aware these will NOT work for websockets. If this is a problem for you open an issue on GitHub! - let path = req - .deprecated_extract::, ()>() - .expect("I got the Axum state type wrong!") - .unwrap(); - println!("Client requested operation '{}'", path.0); + // Deprecated Axum extractors - this API will be removed in the future + // The first generic is the Axum extractor and the second is the type of your Axum state. + // If the state generic is wrong you will get a **RUNTIME** error so be careful! + // TODO: Be aware these will NOT work for websockets. If this is a problem for you open an issue on GitHub! + let path = req + .deprecated_extract::, ()>() + .expect("I got the Axum state type wrong!") + .unwrap(); + println!("Client requested operation '{}'", path.0); - () - }) - .axum(), + () + })), ) // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! .layer( @@ -65,8 +63,7 @@ async fn main() { let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 println!("listening on http://{}/rspc/version", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) .await .unwrap(); } diff --git a/examples/src/bin/cookies.rs b/examples/src/bin/cookies.rs index 958669df..ed905076 100644 --- a/examples/src/bin/cookies.rs +++ b/examples/src/bin/cookies.rs @@ -43,6 +43,7 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", + rspc_axum::endpoint( router .endpoint(|mut req: Request| { // TODO: This API is going to be replaced with a httpz cookie manager in the next release to deal with Axum's recent changes. @@ -52,7 +53,7 @@ async fn main() { .unwrap(); Ctx { cookies } }) - .axum(), + ), ) .layer(CookieManagerLayer::new()) // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! @@ -65,8 +66,7 @@ async fn main() { let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 println!("listening on http://{}/rspc/version", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) .await .unwrap(); } diff --git a/examples/src/bin/global_context.rs b/examples/src/bin/global_context.rs index c7d4fc8d..af25c445 100644 --- a/examples/src/bin/global_context.rs +++ b/examples/src/bin/global_context.rs @@ -32,12 +32,14 @@ async fn main() { // `Arc>`. This could be your database connecton or any other value. let count = Arc::new(AtomicU16::new(0)); - let app = axum::Router::new().nest("/rspc", router.endpoint(move || MyCtx { count }).axum()); + let app = axum::Router::new().nest( + "/rspc", + rspc_axum::endpoint(router.endpoint(move || MyCtx { count })), + ); let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 println!("listening on http://{}/rspc/hit", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) .await .unwrap(); } diff --git a/examples/src/bin/middleware.rs b/examples/src/bin/middleware.rs index efbe6519..61054248 100644 --- a/examples/src/bin/middleware.rs +++ b/examples/src/bin/middleware.rs @@ -108,11 +108,9 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - router - .endpoint(|| UnauthenticatedContext { - session_id: Some("abc".into()), // Change this line to control whether you are authenticated and can access the "another" query. - }) - .axum(), + rspc_axum::endpoint(router.endpoint(|| UnauthenticatedContext { + session_id: Some("abc".into()), // Change this line to control whether you are authenticated and can access the "another" query. + })), ) // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! .layer( @@ -124,8 +122,7 @@ async fn main() { let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 println!("listening on http://{}/rspc/version", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) .await .unwrap(); } diff --git a/src/integrations/httpz.rs b/src/integrations/httpz.rs index 1d0d25cd..3dd67248 100644 --- a/src/integrations/httpz.rs +++ b/src/integrations/httpz.rs @@ -1,3 +1,4 @@ +use axum::http::request::Parts; use futures::{SinkExt, StreamExt}; use httpz::{ axum::axum::extract::FromRequestParts, @@ -221,7 +222,7 @@ impl Request { /// This methods allows using Axum extractors. /// This was previously supported but in Axum 0.6 it's not typesafe anymore so we are going to remove this API. // TODO: Remove this API once rspc's official cookie API is more stabilised. - #[cfg(feature = "axum")] + #[cfg(all(not(feature = "internal_axum_07"), feature = "axum"))] pub fn deprecated_extract(&mut self) -> Option> where E: FromRequestParts, @@ -240,6 +241,63 @@ impl Request { resp })) } + + /// This methods allows using Axum extractors. + /// This was previously supported but in Axum 0.6 it's not typesafe anymore so we are going to remove this API. + // TODO: Remove this API once rspc's official cookie API is more stabilised. + #[cfg(all(feature = "internal_axum_07", feature = "axum"))] + pub fn deprecated_extract(&mut self) -> Option> + where + E: axum::extract::FromRequestParts, + S: Clone + Send + Sync + 'static, + { + let parts = self.0.parts_mut(); + + let state = parts + .extensions + .remove::>()?; + + // This is bad but it's a temporary API so I don't care. + Some(futures::executor::block_on(async { + let (mut new_parts, _) = axum::http::Request::new(()).into_parts(); + new_parts.method = match parts.method { + httpz::http::Method::OPTIONS => axum::http::Method::OPTIONS, + httpz::http::Method::GET => axum::http::Method::GET, + httpz::http::Method::POST => axum::http::Method::POST, + httpz::http::Method::PUT => axum::http::Method::PUT, + httpz::http::Method::DELETE => axum::http::Method::DELETE, + httpz::http::Method::HEAD => axum::http::Method::HEAD, + httpz::http::Method::TRACE => axum::http::Method::TRACE, + httpz::http::Method::CONNECT => axum::http::Method::CONNECT, + httpz::http::Method::PATCH => axum::http::Method::PATCH, + _ => unreachable!(), + }; + new_parts.uri = axum::http::Uri::try_from(parts.uri.to_string()).expect("unreachable"); + new_parts.version = match parts.version { + httpz::http::Version::HTTP_10 => axum::http::Version::HTTP_10, + httpz::http::Version::HTTP_11 => axum::http::Version::HTTP_11, + httpz::http::Version::HTTP_2 => axum::http::Version::HTTP_2, + httpz::http::Version::HTTP_3 => axum::http::Version::HTTP_3, + _ => unreachable!(), + }; + for (key, value) in parts.headers.iter() { + new_parts.headers.insert( + axum::http::HeaderName::from_bytes(key.as_str().as_bytes()) + .expect("unreachable"), + axum::http::HeaderValue::from_bytes(value.as_bytes()).expect("unreachable"), + ); + } + // parts.extensions: Extensions, // TODO: This can't be converted. + + let resp = >::from_request_parts( + &mut new_parts, + &state.0, + ) + .await; + new_parts.extensions.insert(state); + resp + })) + } } impl Router