dioxus/packages/fullstack/server-macro/src/lib.rs

144 lines
6.2 KiB
Rust
Raw Normal View History

2023-03-30 15:34:13 +00:00
use proc_macro::TokenStream;
2023-07-07 18:03:59 +00:00
use quote::{ToTokens, __private::TokenStream as TokenStream2};
2023-03-30 15:34:13 +00:00
use server_fn_macro::*;
2023-07-07 18:03:59 +00:00
use syn::{parse::Parse, spanned::Spanned, ItemFn};
2023-03-30 15:34:13 +00:00
2023-05-02 15:15:34 +00:00
/// Declares that a function is a [server function](dioxus_fullstack). This means that
2023-03-28 18:35:17 +00:00
/// 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 its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
2023-04-28 01:25:16 +00:00
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
2023-05-02 16:05:21 +00:00
/// Defaults to `"Url"`. If you want to use this server function
2023-04-28 01:25:16 +00:00
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
2023-03-28 18:35:17 +00:00
///
/// The server function itself can take any number of arguments, each of which should be serializable
2023-05-02 15:15:34 +00:00
/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html),
2023-03-28 18:35:17 +00:00
/// 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
2023-05-02 15:15:34 +00:00
/// # use dioxus_fullstack::prelude::*; use serde::{Serialize, Deserialize};
2023-03-28 18:35:17 +00:00
/// # #[derive(Serialize, Deserialize)]
/// # pub struct Post { }
/// #[server(ReadPosts, "/api")]
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database
2023-04-01 22:00:12 +00:00
/// todo!()
2023-03-28 18:35:17 +00:00
/// }
/// ```
///
/// Note the following:
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
2023-04-01 22:00:12 +00:00
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
2023-03-28 18:35:17 +00:00
/// 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/).
2023-05-02 15:15:34 +00:00
/// - **The [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function
/// can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html). This scope can be used to inject dependencies like the HTTP request
2023-03-28 18:35:17 +00:00
/// 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 {
2023-07-07 18:03:59 +00:00
// before we pass this off to the server function macro, we apply extractors and middleware
let mut function: syn::ItemFn = match syn::parse(s).map_err(|e| e.to_compile_error()) {
Ok(f) => f,
Err(e) => return e.into(),
};
// find all arguments with the #[extract] attribute
let mut extractors: Vec<Extractor> = vec![];
function.sig.inputs = function
.sig
.inputs
.into_iter()
.filter(|arg| {
if let Ok(extractor) = syn::parse2(arg.clone().into_token_stream()) {
extractors.push(extractor);
false
} else {
true
}
})
.collect();
let ItemFn {
attrs,
vis,
sig,
block,
} = function;
let mapped_body = quote::quote! {
#(#attrs)*
#vis #sig {
#(#extractors)*
#block
}
};
2023-03-28 18:35:17 +00:00
match server_macro_impl(
args.into(),
2023-07-07 18:03:59 +00:00
mapped_body,
syn::parse_quote!(::dioxus_fullstack::prelude::ServerFnTraitObj),
2023-07-07 00:54:05 +00:00
None,
2023-05-02 15:15:34 +00:00
Some(syn::parse_quote!(::dioxus_fullstack::prelude::server_fn)),
2023-03-28 18:35:17 +00:00
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
2023-07-07 18:03:59 +00:00
struct Extractor {
pat: syn::PatType,
}
impl ToTokens for Extractor {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let pat = &self.pat;
tokens.extend(quote::quote! {
let #pat = ::dioxus_fullstack::prelude::extract_server_context().await?;
});
}
}
impl Parse for Extractor {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let arg: syn::FnArg = input.parse()?;
match arg {
syn::FnArg::Typed(mut pat_type) => {
let mut contains_extract = false;
pat_type.attrs.retain(|attr| {
let is_extract = attr.path().is_ident("extract");
if is_extract {
contains_extract = true;
}
!is_extract
});
if !contains_extract {
return Err(syn::Error::new(
pat_type.span(),
"expected an argument with the #[extract] attribute",
));
}
Ok(Extractor { pat: pat_type })
}
_ => Err(syn::Error::new(arg.span(), "expected a typed argument")),
}
}
}