feat: optional named arguments for #[server] macro (#1904)

This commit is contained in:
safx 2023-10-20 05:07:43 +09:00 committed by GitHub
parent 4a83ffca6f
commit 9a70898b09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 299 additions and 18 deletions

View File

@ -34,6 +34,7 @@ typed-builder = "0.16"
trybuild = "1"
leptos = { path = "../leptos" }
insta = "1.29"
serde = "1"
[features]
csr = []

View File

@ -840,12 +840,12 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// 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. All of these arguments are optional.
/// 1. A type name that will be used to identify and register the server function
/// 1. **`name`**: 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
/// 2. **`prefix`**: 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.)
/// 3. **`encoding`**: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. **`endpoint`**: 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`
@ -856,6 +856,10 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// // `/api/hello2349232342342` (hash based on location in source)
/// #[server]
/// pub async fn hello_world() /* ... */
///
/// // The server function accepts keyword parameters
/// #[server(endpoint = "my_endpoint")]
/// pub async fn hello_leptos() /* ... */
/// ```
///
/// The server function itself can take any number of arguments, each of which should be serializable

View File

@ -4,7 +4,7 @@ use proc_macro2::Literal;
use quote::{ToTokens, __private::TokenStream as TokenStream2};
use syn::{
parse::{Parse, ParseStream},
Ident, ItemFn, Token,
Ident, ItemFn, LitStr, Token,
};
pub fn server_impl(
@ -48,6 +48,10 @@ pub fn server_impl(
if args.prefix.is_none() {
args.prefix = Some(Literal::string("/api"));
}
// default to "Url" if no encoding given
if args.encoding.is_none() {
args.encoding = Some(Literal::string("Url"));
}
match server_fn_macro::server_macro_impl(
quote::quote!(#args),
@ -63,11 +67,8 @@ pub fn server_impl(
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>,
}
@ -89,21 +90,110 @@ impl ToTokens for ServerFnArgs {
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()?;
let mut struct_name: Option<Ident> = None;
let mut prefix: Option<Literal> = None;
let mut encoding: Option<Literal> = None;
let mut fn_path: Option<Literal> = None;
let mut use_key_and_value = false;
let mut arg_pos = 0;
while !input.is_empty() {
arg_pos += 1;
let lookahead = input.lookahead1();
if lookahead.peek(Ident) {
let key_or_value: Ident = input.parse()?;
let lookahead = input.lookahead1();
if lookahead.peek(Token![=]) {
input.parse::<Token![=]>()?;
let key = key_or_value;
use_key_and_value = true;
if key == "name" {
if struct_name.is_some() {
return Err(syn::Error::new(
key.span(),
"keyword argument repeated: name",
));
}
struct_name = Some(input.parse()?);
} else if key == "prefix" {
if prefix.is_some() {
return Err(syn::Error::new(
key.span(),
"keyword argument repeated: prefix",
));
}
prefix = Some(input.parse()?);
} else if key == "encoding" {
if encoding.is_some() {
return Err(syn::Error::new(
key.span(),
"keyword argument repeated: encoding",
));
}
encoding = Some(input.parse()?);
} else if key == "endpoint" {
if fn_path.is_some() {
return Err(syn::Error::new(
key.span(),
"keyword argument repeated: endpoint",
));
}
fn_path = Some(input.parse()?);
} else {
return Err(lookahead.error());
}
} else {
let value = key_or_value;
if use_key_and_value {
return Err(syn::Error::new(
value.span(),
"positional argument follows keyword argument",
));
}
if arg_pos == 1 {
struct_name = Some(value)
} else {
return Err(syn::Error::new(
value.span(),
"expected string literal",
));
}
}
} else if lookahead.peek(LitStr) {
let value: Literal = input.parse()?;
if use_key_and_value {
return Err(syn::Error::new(
value.span(),
"positional argument follows keyword argument",
));
}
match arg_pos {
1 => return Err(lookahead.error()),
2 => prefix = Some(value),
3 => encoding = Some(value),
4 => fn_path = Some(value),
_ => {
return Err(syn::Error::new(
value.span(),
"unexpected extra argument",
))
}
}
} else {
return Err(lookahead.error());
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
struct_name,
_comma,
prefix,
_comma2,
encoding,
_comma3,
fn_path,
})
}

View File

@ -0,0 +1,96 @@
#[cfg(test)]
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
use leptos::{server, server_fn::Encoding, ServerFnError};
#[test]
fn server_default() {
#[server]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(MyServerAction::PREFIX, "/api");
assert_eq!(&MyServerAction::URL[0..16], "my_server_action");
assert_eq!(MyServerAction::ENCODING, Encoding::Url);
}
#[test]
fn server_full_legacy() {
#[server(FooBar, "/foo/bar", "Cbor", "my_path")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(FooBar::PREFIX, "/foo/bar");
assert_eq!(FooBar::URL, "my_path");
assert_eq!(FooBar::ENCODING, Encoding::Cbor);
}
#[test]
fn server_all_keywords() {
#[server(endpoint = "my_path", encoding = "Cbor", prefix = "/foo/bar", name = FooBar)]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(FooBar::PREFIX, "/foo/bar");
assert_eq!(FooBar::URL, "my_path");
assert_eq!(FooBar::ENCODING, Encoding::Cbor);
}
#[test]
fn server_mix() {
#[server(FooBar, endpoint = "my_path")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(FooBar::PREFIX, "/api");
assert_eq!(FooBar::URL, "my_path");
assert_eq!(FooBar::ENCODING, Encoding::Url);
}
#[test]
fn server_name() {
#[server(name = FooBar)]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(FooBar::PREFIX, "/api");
assert_eq!(&FooBar::URL[0..16], "my_server_action");
assert_eq!(FooBar::ENCODING, Encoding::Url);
}
#[test]
fn server_prefix() {
#[server(prefix = "/foo/bar")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(MyServerAction::PREFIX, "/foo/bar");
assert_eq!(&MyServerAction::URL[0..16], "my_server_action");
assert_eq!(MyServerAction::ENCODING, Encoding::Url);
}
#[test]
fn server_encoding() {
#[server(encoding = "GetJson")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(MyServerAction::PREFIX, "/api");
assert_eq!(&MyServerAction::URL[0..16], "my_server_action");
assert_eq!(MyServerAction::ENCODING, Encoding::GetJSON);
}
#[test]
fn server_endpoint() {
#[server(endpoint = "/path/to/my/endpoint")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(MyServerAction::PREFIX, "/api");
assert_eq!(MyServerAction::URL, "/path/to/my/endpoint");
assert_eq!(MyServerAction::ENCODING, Encoding::Url);
}
}
}

View File

@ -3,4 +3,5 @@ fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/component.rs");
t.compile_fail("tests/ui/component_absolute.rs");
t.compile_fail("tests/ui/server.rs");
}

View File

@ -0,0 +1,42 @@
use leptos::*;
#[server(endpoint = "my_path", FooBar)]
pub async fn positional_argument_follows_keyword_argument() -> Result<(), ServerFnError> {
Ok(())
}
#[server(endpoint = "first", endpoint = "second")]
pub async fn keyword_argument_repeated() -> Result<(), ServerFnError> {
Ok(())
}
#[server(Foo, Bar)]
pub async fn expected_string_literal() -> Result<(), ServerFnError> {
Ok(())
}
#[server(Foo, Bar, bazz)]
pub async fn expected_string_literal_2() -> Result<(), ServerFnError> {
Ok(())
}
#[server("Foo")]
pub async fn expected_identifier() -> Result<(), ServerFnError> {
Ok(())
}
#[server(Foo Bar)]
pub async fn expected_comma() -> Result<(), ServerFnError> {
Ok(())
}
#[server(FooBar, "/foo/bar", "Cbor", "my_path", "extra")]
pub async fn unexpected_extra_argument() -> Result<(), ServerFnError> {
Ok(())
}
#[server(encoding = "wrong")]
pub async fn encoding_not_found() -> Result<(), ServerFnError> {
Ok(())
}
fn main() {}

View File

@ -0,0 +1,47 @@
error: positional argument follows keyword argument
--> tests/ui/server.rs:3:32
|
3 | #[server(endpoint = "my_path", FooBar)]
| ^^^^^^
error: keyword argument repeated: endpoint
--> tests/ui/server.rs:8:30
|
8 | #[server(endpoint = "first", endpoint = "second")]
| ^^^^^^^^
error: expected string literal
--> tests/ui/server.rs:13:15
|
13 | #[server(Foo, Bar)]
| ^^^
error: expected string literal
--> tests/ui/server.rs:17:15
|
17 | #[server(Foo, Bar, bazz)]
| ^^^
error: expected identifier
--> tests/ui/server.rs:22:10
|
22 | #[server("Foo")]
| ^^^^^
error: expected `,`
--> tests/ui/server.rs:27:14
|
27 | #[server(Foo Bar)]
| ^^^
error: unexpected extra argument
--> tests/ui/server.rs:32:49
|
32 | #[server(FooBar, "/foo/bar", "Cbor", "my_path", "extra")]
| ^^^^^^^
error: Encoding Not Found
--> tests/ui/server.rs:37:21
|
37 | #[server(encoding = "wrong")]
| ^^^^^^^