Skip to content

Commit

Permalink
feat: optional named arguments for #[server] macro
Browse files Browse the repository at this point in the history
  • Loading branch information
safx committed Oct 17, 2023
1 parent be2d014 commit 4db7d17
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 18 deletions.
1 change: 1 addition & 0 deletions leptos_macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ typed-builder = "0.16"
trybuild = "1"
leptos = { path = "../leptos" }
insta = "1.29"
serde = "1"

[features]
csr = []
Expand Down
12 changes: 8 additions & 4 deletions leptos_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down
118 changes: 104 additions & 14 deletions leptos_macro/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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),
Expand All @@ -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>,
}

Expand All @@ -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,
})
}
Expand Down
96 changes: 96 additions & 0 deletions leptos_macro/tests/server.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
1 change: 1 addition & 0 deletions leptos_macro/tests/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
42 changes: 42 additions & 0 deletions leptos_macro/tests/ui/server.rs
Original file line number Diff line number Diff line change
@@ -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() {}
47 changes: 47 additions & 0 deletions leptos_macro/tests/ui/server.stderr
Original file line number Diff line number Diff line change
@@ -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")]
| ^^^^^^^

0 comments on commit 4db7d17

Please sign in to comment.