From 013f5dbb56137a1c943671cc6d6739aa6782fa8c Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Thu, 11 Jan 2024 17:26:08 -0500 Subject: [PATCH] getting started on docs --- server_fn/src/client.rs | 15 +++- server_fn/src/codec/mod.rs | 55 ++++++++++-- server_fn/src/lib.rs | 175 +++++++++++++++++++++++++++++++++++-- server_fn/src/redirect.rs | 12 +++ 4 files changed, 242 insertions(+), 15 deletions(-) diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index cd3599e63f..b4bbdb95fc 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -1,16 +1,26 @@ use crate::{error::ServerFnError, request::ClientReq, response::ClientRes}; use std::future::Future; +/// A client defines a pair of request/response types and the logic to send +/// and receive them. +/// +/// This trait is implemented for things like a browser `fetch` request or for +/// the `reqwest` trait. It should almost never be necessary to implement it +/// yourself, unless you’re trying to use an alternative HTTP crate on the client side. pub trait Client { + /// The type of a request sent by this client. type Request: ClientReq + Send; + /// The type of a response received by this client. type Response: ClientRes + Send; + /// Sends the request and receives a response. fn send( req: Self::Request, ) -> impl Future>> + Send; } -#[cfg(feature = "browser")] +#[cfg(any(feature = "browser", doc))] +/// Implements [`Client`] for a `fetch` request in the browser. pub mod browser { use super::Client; use crate::{ @@ -20,6 +30,7 @@ pub mod browser { use send_wrapper::SendWrapper; use std::future::Future; + /// Implements [`Client`] for a `fetch` request in the browser. pub struct BrowserClient; impl Client for BrowserClient { @@ -42,7 +53,7 @@ pub mod browser { } } -#[cfg(feature = "reqwest")] +#[cfg(any(feature = "reqwest", doc))] pub mod reqwest { use super::Client; use crate::{error::ServerFnError, request::reqwest::CLIENT}; diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index f4c6fa60d1..af3f06313e 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -1,31 +1,47 @@ +//! The serialization/deserialization process for server functions consists of a series of steps, +//! each of which is represented by a different trait: +//! 1. [`IntoReq`]: The client serializes the [`ServerFn`] argument type into an HTTP request. +//! 2. The [`Client`] sends the request to the server. +//! 3. [`FromReq`]: The server deserializes the HTTP request back into the [`ServerFn`] type. +//! 4. The server calls calls [`ServerFn::run_body`] on the data. +//! 5. [`IntoRes`]: The server serializes the [`ServerFn::Output`] type into an HTTP response. +//! 6. The server integration applies any middleware from [`ServerFn::middlewares`] and responds to the request. +//! 7. [`FromRes`]: The client deserializes the response back into the [`ServerFn::Output`] type. +//! +//! Rather than a limited number of encodings, this crate allows you to define server functions that +//! mix and match the input encoding and output encoding. To define a new encoding, you simply implement +//! an input combination ([`IntoReq`] and [`FromReq`]) and/or an output encoding ([`IntoRes`] and [`FromRes`]). +//! This genuinely is an and/or: while some encodings can be used for both input and output ([`Json`], [`Cbor`], [`Rkyv`]), +//! others can only be used for input ([`GetUrl`], [`MultipartData`]) or only output ([`ByteStream`], [`StreamingText`]). + #[cfg(feature = "cbor")] mod cbor; -#[cfg(feature = "cbor")] +#[cfg(any(feature = "cbor", doc))] pub use cbor::*; #[cfg(feature = "json")] mod json; -#[cfg(feature = "json")] +#[cfg(any(feature = "json", doc))] pub use json::*; #[cfg(feature = "serde-lite")] mod serde_lite; -#[cfg(feature = "serde-lite")] +#[cfg(any(feature = "serde-lite", doc))] pub use serde_lite::*; #[cfg(feature = "rkyv")] mod rkyv; -#[cfg(feature = "rkyv")] +#[cfg(any(feature = "rkyv", doc))] pub use rkyv::*; #[cfg(feature = "url")] mod url; -#[cfg(feature = "url")] +#[cfg(any(feature = "url", doc))] pub use url::*; #[cfg(feature = "multipart")] mod multipart; -#[cfg(feature = "multipart")] +#[cfg(any(feature = "multipart", doc))] pub use multipart::*; mod stream; @@ -34,10 +50,37 @@ use futures::Future; use http::Method; pub use stream::*; +/// Deserializes an HTTP request into the data type. +/// +/// Implementations use the methods of the [`Req`](crate::Req) trait to access whatever is +/// needed from the request. +/// +/// For example, here’s the implementation for [`Json`]. +/// +/// ```rust +/// impl FromReq for T +/// where +/// // require the Request implement `Req` +/// Request: Req + Send + 'static, +/// // require that the type can be deserialized with `serde` +/// T: DeserializeOwned, +/// { +/// async fn from_req( +/// req: Request, +/// ) -> Result> { +/// // try to convert the body of the request into a `String` +/// let string_data = req.try_into_string().await?; +/// // deserialize the data +/// serde_json::from_str::(&string_data) +/// .map_err(|e| ServerFnError::Args(e.to_string())) +/// } +/// } +/// ``` pub trait FromReq where Self: Sized, { + /// Attempts to deserialize the request. fn from_req( req: Request, ) -> impl Future>> + Send; diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 9314eba8b3..7d777badf9 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -1,10 +1,117 @@ +#![forbid(unsafe_code)] +// uncomment this if you want to feel pain +//#![deny(missing_docs)] + +//! # Server Functions +//! +//! This package is based on a simple idea: sometimes it’s useful to write functions +//! that will only run on the server, and call them from the client. +//! +//! If you’re creating anything beyond a toy app, you’ll need to do this all the time: +//! reading from or writing to a database that only runs on the server, running expensive +//! computations using libraries you don’t want to ship down to the client, accessing +//! APIs that need to be called from the server rather than the client for CORS reasons +//! or because you need a secret API key that’s stored on the server and definitely +//! shouldn’t be shipped down to a user’s browser. +//! +//! Traditionally, this is done by separating your server and client code, and by setting +//! up something like a REST API or GraphQL API to allow your client to fetch and mutate +//! data on the server. This is fine, but it requires you to write and maintain your code +//! in multiple separate places (client-side code for fetching, server-side functions to run), +//! as well as creating a third thing to manage, which is the API contract between the two. +//! +//! This package provides two simple primitives that allow you instead to write co-located, +//! isomorphic server functions. (*Co-located* means you can write them in your app code so +//! that they are “located alongside” the client code that calls them, rather than separating +//! the client and server sides. *Isomorphic* means you can call them from the client as if +//! you were simply calling a function; the function call has the “same shape” on the client +//! as it does on the server.) +//! +//! ### `#[server]` +//! +//! The [`#[server]`][server] macro allows you to annotate a function to +//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your +//! crate that is enabled). +//! +//! **Important**: Before calling a server function on a non-web platform, you must set the server URL by calling [`set_server_url`]. +//! +//! ```rust,ignore +//! #[server] +//! async fn read_posts(how_many: usize, query: String) -> Result, ServerFnError> { +//! // do some server-only work here to access the database +//! let posts = ...; +//! Ok(posts) +//! } +//! +//! // call the function +//! # #[tokio::main] +//! # async fn main() { +//! async { +//! let posts = read_posts(3, "my search".to_string()).await; +//! log::debug!("posts = {posts:#?}"); +//! } +//! # } +//! ``` +//! +//! If you call this function from the client, it will serialize the function arguments and `POST` +//! them to the server as if they were the URL-encoded inputs in `
`. +//! +//! Here’s what you need to remember: +//! - **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. [`ServerFnError`] can receive generic errors. +//! - **Server functions are part of the public API of your application.** A server function is an +//! ad hoc HTTP API endpoint, not a magic formula. Any server function can be accessed by any HTTP +//! client. You should take care to sanitize any data being returned from the function to ensure it +//! does not leak data that should exist only on the server. +//! - **Server functions can’t be generic.** Because each server function creates a separate API endpoint, +//! it is difficult to monomorphize. As a result, server functions cannot be generic (for now?) If you need to use +//! a generic function, you can define a generic inner function called by multiple concrete server functions. +//! - **Arguments and return types must be serializable.** We support a variety of different encodings, +//! but one way or another arguments need to be serialized to be sent to the server and deserialized +//! on the server, and the return type must be serialized on the server and deserialized on the client. +//! This means that the set of valid server function argument and return types is a subset of all +//! possible Rust argument and return types. (i.e., server functions are strictly more limited than +//! ordinary functions.) +//! +//! ## Server Function Encodings +//! +//! Server functions are designed to allow a flexible combination of input and output encodings, the set +//! of which can be found in the [`codec`] module. +//! +//! The serialization/deserialization process for server functions consists of a series of steps, +//! each of which is represented by a different trait: +//! 1. [`IntoReq`]: The client serializes the [`ServerFn`] argument type into an HTTP request. +//! 2. The [`Client`] sends the request to the server. +//! 3. [`FromReq`]: The server deserializes the HTTP request back into the [`ServerFn`] type. +//! 4. The server calls calls [`ServerFn::run_body`] on the data. +//! 5. [`IntoRes`]: The server serializes the [`ServerFn::Output`] type into an HTTP response. +//! 6. The server integration applies any middleware from [`ServerFn::middlewares`] and responds to the request. +//! 7. [`FromRes`]: The client deserializes the response back into the [`ServerFn::Output`] type. +//! +//! [server]: +//! [`serde_qs`]: +//! [`cbor`]: + +/// Implementations of the client side of the server function call. pub mod client; + +/// Encodings for arguments and results. pub mod codec; + #[macro_use] +/// Error types and utilities. pub mod error; +/// Types to add server middleware to a server function. pub mod middleware; +/// Utilities to allow client-side redirects. pub mod redirect; +/// Types and traits for for HTTP requests. pub mod request; +/// Types and traits for HTTP responses. pub mod response; #[cfg(feature = "actix")] @@ -35,6 +142,35 @@ use std::{fmt::Display, future::Future, pin::Pin, str::FromStr, sync::Arc}; #[doc(hidden)] pub use xxhash_rust; +/// Defines a function that runs only on the server, but can be called from the server or the client. +/// +/// The type for which `ServerFn` is implemented is actually the type of the arguments to the function, +/// while the function body itself is implemented in [`run_body`]. +/// +/// This means that `Self` here is usually a struct, in which each field is an argument to the function. +/// In other words, +/// ```rust,ignore +/// #[server] +/// pub async fn my_function(foo: String, bar: usize) -> Result { +/// Ok(foo.len() + bar) +/// } +/// ``` +/// should expand to +/// ```rust,ignore +/// #[derive(Serialize, Deserialize)] +/// pub struct MyFunction { +/// foo: String, +/// bar: usize +/// } +/// +/// impl ServerFn for MyFunction { +/// async fn run_body() -> Result { +/// Ok(foo.len() + bar) +/// } +/// +/// // etc. +/// } +/// ``` pub trait ServerFn where Self: Send @@ -45,6 +181,7 @@ where Self::InputEncoding, >, { + /// A unique path for the server function’s API endpoint, relative to the host, including its prefix. const PATH: &'static str; /// The type of the HTTP client that will send the request from the client side. @@ -79,17 +216,23 @@ where /// custom error type, this can be `NoCustomError` by default.) type Error: FromStr + Display; + /// Returns [`Self::PATH`]. + fn url() -> &'static str { + Self::PATH + } + /// Middleware that should be applied to this server function. fn middlewares( ) -> Vec>> { Vec::new() } - // The body of the server function. This will only run on the server. + /// The body of the server function. This will only run on the server. fn run_body( self, ) -> impl Future>> + Send; + #[doc(hidden)] fn run_on_server( req: Self::ServerRequest, ) -> impl Future + Send { @@ -100,6 +243,7 @@ where } } + #[doc(hidden)] fn run_on_client( self, ) -> impl Future>> + Send @@ -113,6 +257,7 @@ where } } + #[doc(hidden)] fn run_on_client_with_req( req: >::Request, redirect_hook: Option<&RedirectHook>, @@ -157,16 +302,13 @@ where Ok(res) } } - - fn url() -> &'static str { - Self::PATH - } } #[cfg(feature = "ssr")] #[doc(hidden)] pub use inventory; +/// Uses the `inventory` crate to initialize a map between paths and server functions. #[macro_export] macro_rules! initialize_server_fn_map { ($req:ty, $res:ty) => { @@ -179,8 +321,12 @@ macro_rules! initialize_server_fn_map { }; } +/// A list of middlewares that can be applied to a server function. pub type MiddlewareSet = Vec>>; +/// A trait object that allows multiple server functions that take the same +/// request type and return the same response type to be gathered into a single +/// collection. pub struct ServerFnTraitObj { path: &'static str, method: Method, @@ -189,6 +335,7 @@ pub struct ServerFnTraitObj { } impl ServerFnTraitObj { + /// Converts the relevant parts of a server function into a trait object. pub const fn new( path: &'static str, method: Method, @@ -203,10 +350,12 @@ impl ServerFnTraitObj { } } + /// The path of the server function. pub fn path(&self) -> &'static str { self.path } + /// The HTTP method the server function expects. pub fn method(&self) -> Method { self.method.clone() } @@ -238,7 +387,7 @@ impl Clone for ServerFnTraitObj { type LazyServerFnMap = Lazy>>; -// Axum integration +/// Axum integration. #[cfg(feature = "axum")] pub mod axum { use crate::{ @@ -255,6 +404,9 @@ pub mod axum { Response, > = initialize_server_fn_map!(Request, Response); + /// Explicitly register a server function. This is only necessary if you are + /// running the server in a WASM environment (or a rare environment that the + /// `inventory`). pub fn register_explicit() where T: ServerFn< @@ -273,12 +425,14 @@ pub mod axum { ); } + /// The set of all registered server function paths. pub fn server_fn_paths() -> impl Iterator { REGISTERED_SERVER_FUNCTIONS .iter() .map(|item| (item.path(), item.method())) } + /// An Axum handler that responds to a server function request. pub async fn handle_server_fn(req: Request) -> Response { let path = req.uri().path(); @@ -301,6 +455,7 @@ pub mod axum { } } + /// Returns the server function at the given path as a service that can be modified. pub fn get_server_fn_service( path: &str, ) -> Option, Response>> { @@ -315,7 +470,7 @@ pub mod axum { } } -// Actix integration +/// Actix integration. #[cfg(feature = "actix")] pub mod actix { use crate::{ @@ -335,6 +490,9 @@ pub mod actix { ActixResponse, > = initialize_server_fn_map!(ActixRequest, ActixResponse); + /// Explicitly register a server function. This is only necessary if you are + /// running the server in a WASM environment (or a rare environment that the + /// `inventory`). pub fn register_explicit() where T: ServerFn< @@ -353,12 +511,14 @@ pub mod actix { ); } + /// The set of all registered server function paths. pub fn server_fn_paths() -> impl Iterator { REGISTERED_SERVER_FUNCTIONS .iter() .map(|item| (item.path(), item.method())) } + /// An Actix handler that responds to a server function request. pub async fn handle_server_fn( req: HttpRequest, payload: Payload, @@ -391,6 +551,7 @@ pub mod actix { } } + /// Returns the server function at the given path as a service that can be modified. pub fn get_server_fn_service( path: &str, ) -> Option> { diff --git a/server_fn/src/redirect.rs b/server_fn/src/redirect.rs index 4eed28bab8..aa279d3936 100644 --- a/server_fn/src/redirect.rs +++ b/server_fn/src/redirect.rs @@ -1,19 +1,31 @@ use std::sync::OnceLock; +/// A custom header that can be set with any value to indicate +/// that the server function client should redirect to a new route. +/// +/// This is useful because it allows returning a value from the request, +/// while also indicating that a redirect should follow. This cannot be +/// done with an HTTP `3xx` status code, because the browser will follow +/// that redirect rather than returning the desired data. pub const REDIRECT_HEADER: &str = "serverfnredirect"; +/// A function that will be called if a server function returns a `3xx` status +/// or the [`REDIRECT_HEADER`]. pub type RedirectHook = Box; // allowed: not in a public API, and pretty straightforward #[allow(clippy::type_complexity)] pub(crate) static REDIRECT_HOOK: OnceLock = OnceLock::new(); +/// Sets a function that will be called if a server function returns a `3xx` status +/// or the [`REDIRECT_HEADER`]. Returns `Err(_)` if the hook has already been set. pub fn set_redirect_hook( hook: impl Fn(&str) + Send + Sync + 'static, ) -> Result<(), RedirectHook> { REDIRECT_HOOK.set(Box::new(hook)) } +/// Calls the hook that has been set by [`set_redirect_hook`] to redirect to `path`. pub fn call_redirect_hook(path: &str) { if let Some(hook) = REDIRECT_HOOK.get() { hook(path)