From eccd75a1fe345516398467360eec849ba67f1672 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 4 Mar 2025 14:20:56 -0600 Subject: [PATCH] Switch the default server function codec to json (#3602) * Switch the default server function codec to json * test both input and output encoding --- .../playwright-tests/fullstack/src/main.rs | 19 ++++ packages/server-macro/src/lib.rs | 97 ++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/packages/playwright-tests/fullstack/src/main.rs b/packages/playwright-tests/fullstack/src/main.rs index 1b2feba66..c68f2b432 100644 --- a/packages/playwright-tests/fullstack/src/main.rs +++ b/packages/playwright-tests/fullstack/src/main.rs @@ -41,6 +41,7 @@ fn app() -> Element { Errors {} } OnMounted {} + DefaultServerFnCodec {} DocumentElements {} } } @@ -59,6 +60,15 @@ fn OnMounted() -> Element { } } +#[component] +fn DefaultServerFnCodec() -> Element { + let resource = use_server_future(|| get_server_data_empty_vec(Vec::new()))?; + let empty_vec = resource.unwrap().unwrap(); + assert!(empty_vec.is_empty()); + + rsx! {} +} + #[cfg(feature = "server")] async fn assert_server_context_provided() { let FromContext(i): FromContext = extract().await.unwrap(); @@ -79,6 +89,15 @@ async fn get_server_data() -> Result { Ok("Hello from the server!".to_string()) } +// Make sure the default codec work with empty data structures +// Regression test for https://github.com/DioxusLabs/dioxus/issues/2628 +#[server] +async fn get_server_data_empty_vec(empty_vec: Vec) -> Result, ServerFnError> { + assert_server_context_provided().await; + assert!(empty_vec.is_empty()); + Ok(Vec::new()) +} + #[server] async fn server_error() -> Result { assert_server_context_provided().await; diff --git a/packages/server-macro/src/lib.rs b/packages/server-macro/src/lib.rs index d3d50f164..ab8023fb4 100644 --- a/packages/server-macro/src/lib.rs +++ b/packages/server-macro/src/lib.rs @@ -6,8 +6,12 @@ //! See the [server_fn_macro] crate for more information. use proc_macro::TokenStream; +use quote::quote; use server_fn_macro::server_macro_impl; -use syn::__private::ToTokens; +use syn::{ + __private::ToTokens, + parse::{Parse, ParseStream}, +}; /// Declares that a function is a [server function](https://docs.rs/server_fn/). /// This means that its body will only run on the server, i.e., when the `ssr` @@ -143,6 +147,9 @@ use syn::__private::ToTokens; /// ``` #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { + // If there is no input codec, use json as the default + let args = default_json_codec(args); + match server_macro_impl( args.into(), s.into(), @@ -155,3 +162,91 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { Ok(s) => s.to_token_stream().into(), } } + +fn default_json_codec(args: TokenStream) -> TokenStream { + // Try to parse the args + if let Err(err) = syn::parse::(args.clone()) { + return err.to_compile_error().into(); + } + let Ok(args_metadata) = syn::parse::(args.clone()) else { + // If we fail to parse the args, forward them directly to the macro for diagnostics + return args; + }; + let mut new_tokens = args; + + // Make sure the args always end with a comma + if args_metadata.requires_trailing_comma { + let default_comma: TokenStream = quote! {,}.into(); + new_tokens.extend(default_comma); + } + + // If the user didn't override the input codec, default to Json + if !args_metadata.manual_input { + let default_input: TokenStream = quote! { + input = server_fn::codec::Json, + } + .into(); + new_tokens.extend(default_input); + } + + // If the user didn't override the output codec, default to Json + if !args_metadata.manual_output { + let default_output: TokenStream = quote! { + output = server_fn::codec::Json, + } + .into(); + new_tokens.extend(default_output); + } + + new_tokens +} + +struct ServerFnArgsMetadata { + manual_input: bool, + manual_output: bool, + requires_trailing_comma: bool, +} + +impl Parse for ServerFnArgsMetadata { + fn parse(input: ParseStream) -> syn::Result { + let mut manual_input = false; + let mut manual_output = false; + let mut requires_trailing_comma = false; + let mut take_comma = |input: &ParseStream| { + let comma: Option = input.parse().ok(); + requires_trailing_comma = comma.is_none(); + }; + + while !input.is_empty() { + // Ignore legacy ident and string args + if input.peek(syn::Ident) && !input.peek2(syn::Token![=]) { + input.parse::()?; + take_comma(&input); + continue; + } + if input.peek(syn::LitStr) && !input.peek2(syn::Token![=]) { + input.parse::()?; + take_comma(&input); + continue; + } + + let ident: syn::Ident = input.parse()?; + let _: syn::Token![=] = input.parse()?; + let _: syn::Expr = input.parse()?; + + if ident == "input" { + manual_input = true; + } else if ident == "output" { + manual_output = true; + } + + take_comma(&input); + } + + Ok(Self { + manual_input, + manual_output, + requires_trailing_comma, + }) + } +}