feat: make struct name and path optional for server functions (#1573)

This commit is contained in:
Greg Johnston 2023-08-24 10:22:35 -04:00 committed by GitHub
parent b98174db7a
commit 7306ecccbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 143 additions and 32 deletions

View file

@ -15,13 +15,12 @@ cfg_if! {
}
}
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
#[server]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server(AdjustServerCount, "/api")]
#[server]
pub async fn adjust_server_count(
delta: i32,
msg: String,
@ -33,7 +32,7 @@ pub async fn adjust_server_count(
Ok(new)
}
#[server(ClearServerCount, "/api")]
#[server]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
@ -147,6 +146,8 @@ pub fn Counter() -> impl IntoView {
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter() -> impl IntoView {
// these struct names are auto-generated by #[server]
// they are just the PascalCased versions of the function names
let adjust = create_server_action::<AdjustServerCount>();
let clear = create_server_action::<ClearServerCount>();

View file

@ -107,7 +107,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
#[server(DeleteTodo, "/api")]
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let pool = pool()?;

View file

@ -173,7 +173,7 @@ pub struct PostMetadata {
title: String,
}
#[server(ListPostMetadata, "/api")]
#[server]
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS
@ -185,7 +185,7 @@ pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
.collect())
}
#[server(GetPost, "/api")]
#[server]
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS.iter().find(|post| post.id == id).cloned())

View file

@ -173,7 +173,7 @@ pub struct PostMetadata {
title: String,
}
#[server(ListPostMetadata, "/api")]
#[server]
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS
@ -185,7 +185,7 @@ pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
.collect())
}
#[server(GetPost, "/api")]
#[server]
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS.iter().find(|post| post.id == id).cloned())

View file

@ -4,14 +4,14 @@ use leptos_router::*;
const WAIT_ONE_SECOND: u64 = 1;
const WAIT_TWO_SECONDS: u64 = 2;
#[server(FirstWaitFn "/api")]
#[server]
async fn first_wait_fn(seconds: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;
Ok(())
}
#[server(SecondWaitFn "/api")]
#[server]
async fn second_wait_fn(seconds: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;

View file

@ -62,7 +62,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
#[server(DeleteTodo, "/api")]
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View file

@ -79,7 +79,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
#[server(DeleteTodo, "/api")]
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View file

@ -79,7 +79,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
#[server(DeleteTodo, "/api")]
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View file

@ -8,7 +8,6 @@ use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use quote::ToTokens;
use rstml::{node::KeyedAttribute, parse};
use server_fn_macro::server_macro_impl;
use syn::parse_macro_input;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -34,6 +33,7 @@ mod params;
mod view;
use view::{client_template::render_template, render_view};
mod component;
mod server;
mod slot;
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
@ -760,17 +760,23 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// 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, three, or four 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*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
/// You can specify one, two, three, or four arguments to the server function. All of these arguments are optional.
/// 1. A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`). Defaults to a PascalCased version of the function name.
/// 2. A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/api"`.
/// 3. The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
///
/// ```rust,ignore
/// // will generate a server function at `/api-prefix/hello`
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
/// pub async fn my_server_fn_type() /* ... */
///
/// // will generate a server function with struct `HelloWorld` and path
/// // `/api/hello2349232342342` (hash based on location in source)
/// #[server]
/// pub async fn hello_world() /* ... */
/// ```
///
/// The server function itself can take any number of arguments, each of which should be serializable
@ -859,16 +865,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
#[proc_macro_attribute]
#[proc_macro_error]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match server_macro_impl(
args.into(),
s.into(),
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
None,
Some(syn::parse_quote!(::leptos::server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
server::server_impl(args, s)
}
/// Derives a trait that parses a map of string keys and values into a typed

109
leptos_macro/src/server.rs Normal file
View file

@ -0,0 +1,109 @@
use convert_case::{Case, Converter};
use proc_macro::TokenStream;
use proc_macro2::Literal;
use quote::{ToTokens, __private::TokenStream as TokenStream2};
use syn::{
parse::{Parse, ParseStream},
Ident, ItemFn, Token,
};
pub fn server_impl(
args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let function: syn::ItemFn =
match syn::parse(s).map_err(|e| e.to_compile_error()) {
Ok(f) => f,
Err(e) => return e.into(),
};
let ItemFn {
attrs,
vis,
sig,
block,
} = function;
// TODO apply middleware: https://github.com/leptos-rs/leptos/issues/1461
let mapped_body = quote::quote! {
#(#attrs)*
#vis #sig {
#block
}
};
let mut args: ServerFnArgs = match syn::parse(args) {
Ok(args) => args,
Err(e) => return e.to_compile_error().into(),
};
// default to PascalCase version of function name if no struct name given
if args.struct_name.is_none() {
let upper_cammel_case_name = Converter::new()
.from_case(Case::Snake)
.to_case(Case::UpperCamel)
.convert(sig.ident.to_string());
args.struct_name =
Some(Ident::new(&upper_cammel_case_name, sig.ident.span()));
}
// default to "/api" if no prefix given
if args.prefix.is_none() {
args.prefix = Some(Literal::string("/api"));
}
match server_fn_macro::server_macro_impl(
quote::quote!(#args),
mapped_body,
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
None,
Some(syn::parse_quote!(::leptos::server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
struct ServerFnArgs {
struct_name: Option<Ident>,
_comma: Option<Token![,]>,
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Option<Literal>,
_comma3: Option<Token![,]>,
fn_path: Option<Literal>,
}
impl ToTokens for ServerFnArgs {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let struct_name =
self.struct_name.as_ref().map(|s| quote::quote! { #s, });
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
tokens.extend(quote::quote! {
#struct_name
#prefix
#encoding
#fn_path
})
}
}
impl Parse for ServerFnArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let struct_name = input.parse()?;
let _comma = input.parse()?;
let prefix = input.parse()?;
let _comma2 = input.parse()?;
let encoding = input.parse()?;
let _comma3 = input.parse()?;
let fn_path = input.parse()?;
Ok(Self {
struct_name,
_comma,
prefix,
_comma2,
encoding,
_comma3,
fn_path,
})
}
}