diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 9c23119689..f8b98fc259 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -34,6 +34,7 @@ typed-builder = "0.16" trybuild = "1" leptos = { path = "../leptos" } insta = "1.29" +serde = "1" [features] csr = [] diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index a67247f7c3..d3eac61a58 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -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 diff --git a/leptos_macro/src/server.rs b/leptos_macro/src/server.rs index 7c2fcfc172..49832cb4b9 100644 --- a/leptos_macro/src/server.rs +++ b/leptos_macro/src/server.rs @@ -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, - _comma: Option, prefix: Option, - _comma2: Option, encoding: Option, - _comma3: Option, fn_path: Option, } @@ -89,21 +90,110 @@ impl ToTokens for ServerFnArgs { impl Parse for ServerFnArgs { fn parse(input: ParseStream) -> syn::Result { - 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 = None; + let mut prefix: Option = None; + let mut encoding: Option = None; + let mut fn_path: Option = 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::()?; + 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::()?; + } + } Ok(Self { struct_name, - _comma, prefix, - _comma2, encoding, - _comma3, fn_path, }) } diff --git a/leptos_macro/tests/server.rs b/leptos_macro/tests/server.rs new file mode 100644 index 0000000000..052d88d67a --- /dev/null +++ b/leptos_macro/tests/server.rs @@ -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); + } + } +} diff --git a/leptos_macro/tests/ui.rs b/leptos_macro/tests/ui.rs index 8e4cc29328..95f170b605 100644 --- a/leptos_macro/tests/ui.rs +++ b/leptos_macro/tests/ui.rs @@ -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"); } diff --git a/leptos_macro/tests/ui/server.rs b/leptos_macro/tests/ui/server.rs new file mode 100644 index 0000000000..d4ac950f28 --- /dev/null +++ b/leptos_macro/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() {} diff --git a/leptos_macro/tests/ui/server.stderr b/leptos_macro/tests/ui/server.stderr new file mode 100644 index 0000000000..0cfa2665e9 --- /dev/null +++ b/leptos_macro/tests/ui/server.stderr @@ -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")] + | ^^^^^^^