From 83d513ef362f6cc17bd18d4c53bd9db7ebea3450 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 19:42:53 -0500 Subject: [PATCH] add salvo intigration --- .../.gitignore | 0 .../Cargo.toml | 2 +- .../src/main.rs | 4 +- .../examples/salvo-hello-world/.gitignore | 2 + .../examples/salvo-hello-world/Cargo.toml | 20 +++ .../examples/salvo-hello-world/src/main.rs | 68 ++++++++ packages/server/src/adapters/salvo_adapter.rs | 161 ++++++++++++++++-- 7 files changed, 238 insertions(+), 19 deletions(-) rename packages/server/examples/{hello-world => axum-hello-world}/.gitignore (100%) rename packages/server/examples/{hello-world => axum-hello-world}/Cargo.toml (95%) rename packages/server/examples/{hello-world => axum-hello-world}/src/main.rs (95%) create mode 100644 packages/server/examples/salvo-hello-world/.gitignore create mode 100644 packages/server/examples/salvo-hello-world/Cargo.toml create mode 100644 packages/server/examples/salvo-hello-world/src/main.rs diff --git a/packages/server/examples/hello-world/.gitignore b/packages/server/examples/axum-hello-world/.gitignore similarity index 100% rename from packages/server/examples/hello-world/.gitignore rename to packages/server/examples/axum-hello-world/.gitignore diff --git a/packages/server/examples/hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml similarity index 95% rename from packages/server/examples/hello-world/Cargo.toml rename to packages/server/examples/axum-hello-world/Cargo.toml index 846c03ea4..54aaf17d3 100644 --- a/packages/server/examples/hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hello-world" +name = "axum-hello-world" version = "0.1.0" edition = "2021" diff --git a/packages/server/examples/hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs similarity index 95% rename from packages/server/examples/hello-world/src/main.rs rename to packages/server/examples/axum-hello-world/src/main.rs index 2fd6b1bde..4d4ab5e78 100644 --- a/packages/server/examples/hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -1,5 +1,5 @@ //! Run with: -//! +//! //! ```sh //! dioxus build --features web //! cargo run --features ssr @@ -24,7 +24,7 @@ fn main() { .serve( axum::Router::new() .serve_dioxus_application( - ServeConfig::new(app).head(r#"Hello World!"#), + ServeConfig::new(app, ()).head(r#"Hello World!"#), ) .into_make_service(), ) diff --git a/packages/server/examples/salvo-hello-world/.gitignore b/packages/server/examples/salvo-hello-world/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/salvo-hello-world/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml new file mode 100644 index 000000000..1ebe56dfc --- /dev/null +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "salvo-hello-world" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-server = { path = "../../" } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" +tracing-subscriber = "0.3.16" +tracing = "0.1.37" +salvo = { version = "0.37.9", optional = true } + +[features] +ssr = ["salvo", "tokio", "dioxus-server/ssr", "dioxus-server/salvo"] +web = ["dioxus-web"] diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs new file mode 100644 index 000000000..cd8b80c51 --- /dev/null +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -0,0 +1,68 @@ +//! Run with: +//! +//! ```sh +//! dioxus build --features web +//! cargo run --features ssr +//! ``` + +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + use salvo::prelude::*; + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let router = Router::new().serve_dioxus_application( + ServeConfig::new(app, ()).head(r#"Hello World!"#), + ); + Server::new(TcpListener::bind("127.0.0.1:8080")) + .serve(router) + .await; + }); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 2c8912a57..20edc9364 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -1,25 +1,154 @@ -use futures_util::{SinkExt, StreamExt}; -use salvo::ws::{Message, WebSocket}; +use std::{error::Error, sync::Arc}; -use crate::{LiveViewError, LiveViewSocket}; +use hyper::{http::HeaderValue, StatusCode}; +use salvo::{ + async_trait, handler, serve_static::StaticDir, Depot, FlowCtrl, Handler, Request, Response, + Router, +}; +use server_fn::{Payload, ServerFunctionRegistry}; +use tokio::task::spawn_blocking; -/// Convert a salvo websocket into a LiveViewSocket -/// -/// This is required to launch a LiveView app using the warp web framework -pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket { - ws.map(transform_rx) - .with(transform_tx) - .sink_map_err(|_| LiveViewError::SendingFailed) +use crate::{ + dioxus_ssr_html, + serve::ServeConfig, + server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, +}; + +pub trait DioxusRouterExt { + fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn serve_dioxus_application( + self, + cfg: ServeConfig

, + ) -> Self; } -fn transform_rx(message: Result) -> Result { - let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; +impl DioxusRouterExt for Router { + fn register_server_fns(self, server_fn_route: &'static str) -> Self { + let mut router = self; + for server_fn_path in DioxusServerFnRegistry::paths_registered() { + let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); + let full_route = format!("{server_fn_route}/{server_fn_path}"); + router = router.push(Router::with_path(&full_route).post(ServerFnHandler { + server_context: DioxusServerContext {}, + function: func, + })); + } + router + } - let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; + fn serve_dioxus_application( + self, + cfg: ServeConfig

, + ) -> Self { + // Serve the dist folder and the index.html file + let serve_dir = StaticDir::new(["dist"]); - Ok(msg) + self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) + .push(Router::with_path("/").get(SSRHandler { cfg })) + .push(Router::with_path("<**path>").get(serve_dir)) + } } -async fn transform_tx(message: String) -> Result { - Ok(Message::text(message)) +struct SSRHandler { + cfg: ServeConfig

, +} + +#[async_trait] +impl Handler for SSRHandler

{ + async fn handle( + &self, + _req: &mut Request, + _depot: &mut Depot, + res: &mut Response, + _flow: &mut FlowCtrl, + ) { + res.write_body(dioxus_ssr_html(&self.cfg)).unwrap(); + } +} + +struct ServerFnHandler { + server_context: DioxusServerContext, + function: Arc, +} + +#[handler] +impl ServerFnHandler { + async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) { + let Self { + server_context, + function, + } = self; + + let body = hyper::body::to_bytes(req.body_mut().unwrap()).await; + let Ok(body)=body else { + handle_error(body.err().unwrap(), res); + return; + }; + let headers = req.headers(); + + // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); + let function = function.clone(); + let server_context = server_context.clone(); + spawn_blocking({ + move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async move { + let resp = function(server_context, &body).await; + + resp_tx.send(resp).unwrap(); + }) + } + }); + let result = resp_rx.await.unwrap(); + + match result { + Ok(serialized) => { + // if this is Accept: application/json then send a serialized JSON response + let accept_header = headers.get("Accept").and_then(|value| value.to_str().ok()); + if accept_header == Some("application/json") + || accept_header + == Some( + "application/\ + x-www-form-urlencoded", + ) + || accept_header == Some("application/cbor") + { + res.set_status_code(StatusCode::OK); + } + + match serialized { + Payload::Binary(data) => { + res.headers_mut() + .insert("Content-Type", HeaderValue::from_static("application/cbor")); + res.write_body(data).unwrap(); + } + Payload::Url(data) => { + res.headers_mut().insert( + "Content-Type", + HeaderValue::from_static( + "application/\ + x-www-form-urlencoded", + ), + ); + res.render(data); + } + Payload::Json(data) => { + res.headers_mut() + .insert("Content-Type", HeaderValue::from_static("application/json")); + res.render(data); + } + } + } + Err(err) => handle_error(err, res), + } + } +} + +fn handle_error(error: impl Error + Send + Sync, res: &mut Response) { + let mut resp_err = Response::new(); + resp_err.set_status_code(StatusCode::INTERNAL_SERVER_ERROR); + resp_err.render(format!("Internal Server Error: {}", error)); + *res = resp_err; }