feat: optional named arguments for #[server] macro (#1904)
This commit is contained in:
parent
4a83ffca6f
commit
9a70898b09
|
@ -34,6 +34,7 @@ typed-builder = "0.16"
|
|||
trybuild = "1"
|
||||
leptos = { path = "../leptos" }
|
||||
insta = "1.29"
|
||||
serde = "1"
|
||||
|
||||
[features]
|
||||
csr = []
|
||||
|
|
|
@ -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 it’s registered
|
||||
/// 2. **`prefix`**: A URL prefix at which the function will be mounted when it’s 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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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() {}
|
|
@ -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")]
|
||||
| ^^^^^^^
|
Loading…
Reference in New Issue