Skip to content

Commit

Permalink
Upgrade workspace + Axum 0.7
Browse files Browse the repository at this point in the history
  • Loading branch information
oscartbeaumont committed Mar 17, 2024
1 parent d636266 commit 45d3821
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 34 deletions.
22 changes: 11 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ bit-vec = ["specta/bit-vec"]
bson = ["specta/bson"]

[dependencies]
specta = { version = "1.0.0", features = ["serde", "typescript"] }
httpz = { version = "0.0.4", optional = true } # TODO: Move back to crates.io release
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.38"
futures = "0.3.26"
tokio = { version = "1.26.0", features = ["sync", "rt", "macros"] }
tauri = { version = "1.2.4", optional = true }
tracing = { version = "0.1.37", optional = true }
specta = { version = "1.0.5", features = ["serde", "typescript"] }
httpz = { version = "0.0.6", optional = true } # TODO: Move back to crates.io release
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
thiserror = "1.0.58"
futures = "0.3.30"
tokio = { version = "1.36.0", features = ["sync", "rt", "macros"] }
tauri = { version = "1.6.1", optional = true }
tracing = { version = "0.1.40", optional = true }

[dev-dependencies]
async-stream = "0.3.4"
async-stream = "0.3.5"

[workspace]
members = ["./examples", "./examples/axum"]
members = ["./crates/*", "./examples", "./examples/axum", "crates/axum"]
16 changes: 16 additions & 0 deletions crates/axum/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "rspc-axum"
description = "Axum adapter for rspc"
version = "0.0.1"
authors = ["Oscar Beaumont <[email protected]>"]
edition = "2021"
license = "MIT"
repository = "https://github.com/oscartbeaumont/rspc"
documentation = "https://docs.rs/rspc-axum/latest/rspc-axum"
keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"]
categories = ["web-programming", "asynchronous"]

[dependencies]
axum = "0.7.4"
httpz = { version = "0.0.6", features = ["axum"] }
rspc = { version = "0.1.3", path = "../.." }
146 changes: 146 additions & 0 deletions crates/axum/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! rspc-axum: Axum integration for [rspc](https://rspc.dev).
use std::sync::Arc;

use axum::{
body::{to_bytes, Body},
extract::State,
http::{HeaderMap, StatusCode},
routing::{on, MethodFilter},
Router,
};
use httpz::{Endpoint, HttpEndpoint, HttpResponse, Server};

fn method_interop(method: httpz::http::Method) -> axum::http::Method {
match 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!(),
}
}

fn method_interop2(method: axum::http::Method) -> httpz::http::Method {
match method {
axum::http::Method::OPTIONS => httpz::http::Method::OPTIONS,
axum::http::Method::GET => httpz::http::Method::GET,
axum::http::Method::POST => httpz::http::Method::POST,
axum::http::Method::PUT => httpz::http::Method::PUT,
axum::http::Method::DELETE => httpz::http::Method::DELETE,
axum::http::Method::HEAD => httpz::http::Method::HEAD,
axum::http::Method::TRACE => httpz::http::Method::TRACE,
axum::http::Method::CONNECT => httpz::http::Method::CONNECT,
axum::http::Method::PATCH => httpz::http::Method::PATCH,
_ => unreachable!(),
}
}

fn status_interop(status: httpz::http::StatusCode) -> axum::http::StatusCode {
axum::http::StatusCode::from_u16(status.as_u16()).expect("unreachable")
}

fn headers_interop(headers: httpz::http::HeaderMap) -> axum::http::HeaderMap {
let mut new_headers = axum::http::HeaderMap::new();
for (key, value) in headers.iter() {
new_headers.insert(
axum::http::HeaderName::from_bytes(key.as_str().as_bytes()).expect("unreachable"),
axum::http::HeaderValue::from_bytes(value.as_bytes()).expect("unreachable"),
);
}
new_headers
}

fn headers_interop2(headers: axum::http::HeaderMap) -> httpz::http::HeaderMap {
let mut new_headers = httpz::http::HeaderMap::new();
for (key, value) in headers.iter() {
new_headers.insert(
httpz::http::HeaderName::from_bytes(key.as_str().as_bytes()).expect("unreachable"),
httpz::http::HeaderValue::from_bytes(value.as_bytes()).expect("unreachable"),
);
}
new_headers
}

pub fn endpoint<S>(mut endpoint: Endpoint<impl HttpEndpoint>) -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
let (url, methods) = endpoint.endpoint.register();
let endpoint = Arc::new(endpoint.endpoint);

let mut method_filter = None::<MethodFilter>;
for method in methods.as_ref().iter() {
#[allow(clippy::unwrap_used)] // TODO: Error handling
let new_filter = MethodFilter::try_from(method_interop(method.clone())).unwrap();
if let Some(filter) = method_filter {
method_filter = Some(filter.or(new_filter));
} else {
method_filter = Some(new_filter);
}
}

Router::<S>::new().route(
url.as_ref(),
on(
method_filter.expect("no methods specified"), // Unreachable because rspc specifies at least one method
|state: State<S>, request: axum::extract::Request<Body>| async move {
let (mut parts, body) = request.into_parts();
parts.extensions.insert(state);

// TODO: Should probs limit the size of the body
let body = match to_bytes(body, usize::MAX).await {
Ok(body) => body.to_vec(),
Err(err) => {
return (
StatusCode::BAD_REQUEST,
HeaderMap::new(),
err.to_string().as_bytes().to_vec(),
);
}
};

let (mut new_parts, _) = httpz::http::Request::new(()).into_parts();
new_parts.method = method_interop2(parts.method);
new_parts.uri =
httpz::http::Uri::try_from(parts.uri.to_string()).expect("unreachable");
new_parts.version = match parts.version {
axum::http::Version::HTTP_10 => httpz::http::Version::HTTP_10,
axum::http::Version::HTTP_11 => httpz::http::Version::HTTP_11,
axum::http::Version::HTTP_2 => httpz::http::Version::HTTP_2,
axum::http::Version::HTTP_3 => httpz::http::Version::HTTP_3,
_ => unreachable!(),
};
new_parts.headers = headers_interop2(parts.headers);
// new_parts.extensions = parts.extensions; // TODO: We can't interop this

match endpoint
.handler(httpz::Request::new(
httpz::http::Request::from_parts(new_parts, body),
Server::Axum,
))
.await
.into_response()
{
Ok(resp) => {
let (parts, body) = resp.into_parts();
(
status_interop(parts.status),
headers_interop(parts.headers),
body,
)
}
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
HeaderMap::new(),
err.to_string().as_bytes().to_vec(),
),
}
},
),
)
}
20 changes: 10 additions & 10 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ publish = false

[dependencies]
rspc = { path = "../", features = ["axum"] }
async-stream = "0.3.4"
axum = "0.6.10"
chrono = { version = "0.4.23", features = ["serde"] }
serde = { version = "1.0.152", features = ["derive"] }
time = "0.3.20"
tokio = { version = "1.26.0", features = ["rt-multi-thread", "macros", "time", "sync"], default-features = false }
tower-cookies = "0.9.0"
tower-http = { version = "0.4.0", default-features = false, features = ["cors"] }
uuid = { version = "1.3.0", features = ["v4", "serde"] }
serde_json = "1.0.93"
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"] }
uuid = { version = "1.7.0", features = ["v4", "serde"] }
serde_json = "1.0.114"
11 changes: 7 additions & 4 deletions examples/axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ publish = false

[dependencies]
rspc = { path = "../../", features = ["axum"] }
tokio = { version = "1.26.0", features = ["full"] }
async-stream = "0.3.4"
axum = { version = "0.6.10", features = ["ws"] }
tower-http = { version = "0.4.0", default-features = false, features = ["cors"] }
rspc-axum = { path = "../../crates/axum" }
tokio = { version = "1.36.0", features = ["full"] }
async-stream = "0.3.5"
axum = { version = "0.7.4", features = ["ws"] }
tower-http = { version = "0.5.2", default-features = false, features = [
"cors",
] }
14 changes: 5 additions & 9 deletions examples/axum/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,16 @@ async fn main() {
.route("/", get(|| async { "Hello 'rspc'!" }))
.nest(
"/rspc",
router
.clone()
.endpoint(|req: Request| {
println!("Client requested operation '{}'", req.uri().path());
Ctx {}
})
.axum(),
rspc_axum::endpoint(router.clone().endpoint(|req: Request| {
println!("Client requested operation '{}'", req.uri().path());
Ctx {}
})),
)
.layer(cors);

let addr = "[::]:4000".parse::<std::net::SocketAddr>().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();
}

0 comments on commit 45d3821

Please sign in to comment.