diff --git a/Cargo.toml b/Cargo.toml index e32940e18..3b36bb5e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "packages/rsx-rosetta", "packages/signals", "packages/hot-reload", + "packages/server", "docs/guide", ] diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml new file mode 100644 index 000000000..13704574b --- /dev/null +++ b/packages/server/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +server_fn = { version = "0.2.4", features = ["stable"] } + +# warp +warp = { version = "0.3.3", optional = true } + +# axum +axum = { version = "0.6.1", optional = true, features = ["ws"] } + +# salvo +salvo = { version = "0.37.7", optional = true, features = ["ws"] } +serde = "1.0.159" + +dioxus = { path = "../dioxus", version = "^0.3.0" } \ No newline at end of file diff --git a/packages/server/server_macro/Cargo.toml b/packages/server/server_macro/Cargo.toml new file mode 100644 index 000000000..6e8c69b46 --- /dev/null +++ b/packages/server/server_macro/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "server_macro" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/packages/server/server_macro/src/lib.rs b/packages/server/server_macro/src/lib.rs new file mode 100644 index 000000000..439a9e7b2 --- /dev/null +++ b/packages/server/server_macro/src/lib.rs @@ -0,0 +1,67 @@ +/// Declares that a function is a [server function](leptos_server). This means that +/// its body will only run on the server, i.e., when the `ssr` feature is enabled. +/// +/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features +/// are enabled), it will instead make a network request to the server. +/// +/// You can specify one, two, or three arguments to the server function: +/// 1. **Required**: A type name that will be used to identify and register the server function +/// (e.g., `MyServerFn`). +/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered +/// (e.g., `"/api"`). Defaults to `"/"`. +/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for +/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string). +/// Defaults to `"Url"`. If you want to use this server function to power a `
` that will +/// work without WebAssembly, the encoding must be `"Url"`. +/// +/// The server function itself can take any number of arguments, each of which should be serializable +/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope), +/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other +/// server-side context into the server function. +/// +/// ```ignore +/// # use leptos::*; use serde::{Serialize, Deserialize}; +/// # #[derive(Serialize, Deserialize)] +/// # pub struct Post { } +/// #[server(ReadPosts, "/api")] +/// pub async fn read_posts(how_many: u8, query: String) -> Result, ServerFnError> { +/// // do some work on the server to access the database +/// todo!() +/// } +/// ``` +/// +/// Note the following: +/// - You must **register** the server function by calling `T::register()` somewhere in your main function. +/// - **Server functions must be `async`.** Even if the work being done inside the function body +/// can run synchronously on the server, from the client’s perspective it involves an asynchronous +/// function call. +/// - **Server functions must return `Result`.** Even if the work being done +/// inside the function body can’t fail, the processes of serialization/deserialization and the +/// network call are fallible. +/// - **Return types must be [Serializable](leptos_reactive::Serializable).** +/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we +/// need to deserialize the result to return it to the client. +/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) +/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).** +/// They are serialized as an `application/x-www-form-urlencoded` +/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor` +/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). +/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function +/// can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request +/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client. +#[proc_macro_attribute] +pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { + let context = ServerContext { + ty: syn::parse_quote!(Scope), + path: syn::parse_quote!(::leptos::Scope), + }; + match server_macro_impl( + args.into(), + s.into(), + Some(context), + Some(syn::parse_quote!(::leptos::server_fn)), + ) { + Err(e) => e.to_compile_error().into(), + Ok(s) => s.to_token_stream().into(), + } +} diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs new file mode 100644 index 000000000..646cde38e --- /dev/null +++ b/packages/server/src/adapters/axum_adapter.rs @@ -0,0 +1,23 @@ +use crate::{LiveViewError, LiveViewSocket}; +use axum::extract::ws::{Message, WebSocket}; +use futures_util::{SinkExt, StreamExt}; + +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::Text(message)) +} diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs new file mode 100644 index 000000000..2c8912a57 --- /dev/null +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -0,0 +1,25 @@ +use futures_util::{SinkExt, StreamExt}; +use salvo::ws::{Message, WebSocket}; + +use crate::{LiveViewError, LiveViewSocket}; + +/// 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) +} + +fn transform_rx(message: Result) -> Result { + let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; + + let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; + + Ok(msg) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) +} diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs new file mode 100644 index 000000000..e5c821ce3 --- /dev/null +++ b/packages/server/src/adapters/warp_adapter.rs @@ -0,0 +1,28 @@ +use crate::{LiveViewError, LiveViewSocket}; +use futures_util::{SinkExt, StreamExt}; +use warp::ws::{Message, WebSocket}; + +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result { + // destructure the message into the buffer we got from warp + let msg = message + .map_err(|_| LiveViewError::SendingFailed)? + .into_bytes(); + + // transform it back into a string, saving us the allocation + let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; + + Ok(msg) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) +} diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs new file mode 100644 index 000000000..49b635fc2 --- /dev/null +++ b/packages/server/src/lib.rs @@ -0,0 +1,85 @@ +use dioxus::prelude::*; +use serde::{de::DeserializeOwned, Deserializer, Serialize, Serializer}; + +// We use deref specialization to make it possible to pass either a value that implements +pub trait SerializeToRemoteWrapper { + fn serialize_to_remote(&self, serializer: S) -> Result; +} + +impl SerializeToRemoteWrapper for &T { + fn serialize_to_remote( + &self, + serializer: S, + ) -> Result<::Ok, ::Error> { + self.serialize(serializer) + } +} + +impl SerializeToRemoteWrapper for &mut &S { + fn serialize_to_remote( + &self, + serializer: S2, + ) -> Result<::Ok, ::Error> { + (**self).serialize_to_remote(serializer) + } +} + +pub trait SerializeToRemote { + fn serialize_to_remote(&self, serializer: S) -> Result; +} + +impl SerializeToRemote for UseState { + fn serialize_to_remote( + &self, + serializer: S2, + ) -> Result<::Ok, ::Error> { + self.current().serialize(serializer) + } +} + +// We use deref specialization to make it possible to pass either a value that implements +pub trait DeserializeOnRemoteWrapper { + type Output; + + fn deserialize_on_remote<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result; +} + +impl DeserializeOnRemoteWrapper for &T { + type Output = T; + + fn deserialize_on_remote<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result { + T::deserialize(deserializer) + } +} + +impl DeserializeOnRemoteWrapper for &mut &D { + type Output = D::Output; + + fn deserialize_on_remote<'a, D2: Deserializer<'a>>( + deserializer: D2, + ) -> Result { + D::deserialize_on_remote(deserializer) + } +} + +pub trait DeserializeOnRemote { + type Output; + + fn deserialize_on_remote<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result; +} + +impl DeserializeOnRemote for UseState { + type Output = D; + + fn deserialize_on_remote<'a, D2: Deserializer<'a>>( + deserializer: D2, + ) -> Result { + D::deserialize(deserializer) + } +}