mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
getting started on docs
This commit is contained in:
parent
1d1de4ac38
commit
0a9cdba22e
4 changed files with 242 additions and 15 deletions
|
@ -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<CustErr> {
|
||||
/// The type of a request sent by this client.
|
||||
type Request: ClientReq<CustErr> + Send;
|
||||
/// The type of a response received by this client.
|
||||
type Response: ClientRes<CustErr> + Send;
|
||||
|
||||
/// Sends the request and receives a response.
|
||||
fn send(
|
||||
req: Self::Request,
|
||||
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + 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<CustErr> Client<CustErr> 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};
|
||||
|
|
|
@ -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<CustErr, T, Request> FromReq<CustErr, Request, Json> for T
|
||||
/// where
|
||||
/// // require the Request implement `Req`
|
||||
/// Request: Req<CustErr> + Send + 'static,
|
||||
/// // require that the type can be deserialized with `serde`
|
||||
/// T: DeserializeOwned,
|
||||
/// {
|
||||
/// async fn from_req(
|
||||
/// req: Request,
|
||||
/// ) -> Result<Self, ServerFnError<CustErr>> {
|
||||
/// // 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::<Self>(&string_data)
|
||||
/// .map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait FromReq<CustErr, Request, Encoding>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
/// Attempts to deserialize the request.
|
||||
fn from_req(
|
||||
req: Request,
|
||||
) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send;
|
||||
|
|
|
@ -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<Vec<Posts>, 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 `<form method="post">`.
|
||||
//!
|
||||
//! 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<T, ServerFnError>`.** 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]: <https://docs.rs/server_fn/latest/server_fn/attr.server.html>
|
||||
//! [`serde_qs`]: <https://docs.rs/serde_qs/latest/serde_qs/>
|
||||
//! [`cbor`]: <https://docs.rs/cbor/latest/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<usize, ServerFnError> {
|
||||
/// 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<usize, ServerFnError> {
|
||||
/// 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<Arc<dyn Layer<Self::ServerRequest, Self::ServerResponse>>> {
|
||||
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<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn run_on_server(
|
||||
req: Self::ServerRequest,
|
||||
) -> impl Future<Output = Self::ServerResponse> + Send {
|
||||
|
@ -100,6 +243,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn run_on_client(
|
||||
self,
|
||||
) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send
|
||||
|
@ -113,6 +257,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn run_on_client_with_req(
|
||||
req: <Self::Client as Client<Self::Error>>::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<Req, Res> = Vec<Arc<dyn Layer<Req, Res>>>;
|
||||
|
||||
/// 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<Req, Res> {
|
||||
path: &'static str,
|
||||
method: Method,
|
||||
|
@ -189,6 +335,7 @@ pub struct ServerFnTraitObj<Req, Res> {
|
|||
}
|
||||
|
||||
impl<Req, Res> ServerFnTraitObj<Req, Res> {
|
||||
/// 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<Req, Res> ServerFnTraitObj<Req, Res> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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<Req, Res> Clone for ServerFnTraitObj<Req, Res> {
|
|||
type LazyServerFnMap<Req, Res> =
|
||||
Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>;
|
||||
|
||||
// Axum integration
|
||||
/// Axum integration.
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum {
|
||||
use crate::{
|
||||
|
@ -255,6 +404,9 @@ pub mod axum {
|
|||
Response<Body>,
|
||||
> = initialize_server_fn_map!(Request<Body>, Response<Body>);
|
||||
|
||||
/// 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<T>()
|
||||
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<Item = (&'static str, Method)> {
|
||||
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<Body>) -> Response<Body> {
|
||||
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<BoxedService<Request<Body>, Response<Body>>> {
|
||||
|
@ -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<T>()
|
||||
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<Item = (&'static str, Method)> {
|
||||
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<BoxedService<ActixRequest, ActixResponse>> {
|
||||
|
|
|
@ -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<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
// allowed: not in a public API, and pretty straightforward
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) static REDIRECT_HOOK: OnceLock<RedirectHook> = 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)
|
||||
|
|
Loading…
Reference in a new issue