Browse Source

Switch the default server function codec to json (#3602)

* Switch the default server function codec to json

* test both input and output encoding
Evan Almloff 3 months ago
parent
commit
eccd75a1fe
2 changed files with 115 additions and 1 deletions
  1. 19 0
      packages/playwright-tests/fullstack/src/main.rs
  2. 96 1
      packages/server-macro/src/lib.rs

+ 19 - 0
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<u32> = extract().await.unwrap();
@@ -79,6 +89,15 @@ async fn get_server_data() -> Result<String, ServerFnError> {
     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<String>) -> Result<Vec<String>, ServerFnError> {
+    assert_server_context_provided().await;
+    assert!(empty_vec.is_empty());
+    Ok(Vec::new())
+}
+
 #[server]
 async fn server_error() -> Result<String, ServerFnError> {
     assert_server_context_provided().await;

+ 96 - 1
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::<ServerFnArgsMetadata>(args.clone()) {
+        return err.to_compile_error().into();
+    }
+    let Ok(args_metadata) = syn::parse::<ServerFnArgsMetadata>(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<Self> {
+        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<syn::Token![,]> = 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::<syn::Ident>()?;
+                take_comma(&input);
+                continue;
+            }
+            if input.peek(syn::LitStr) && !input.peek2(syn::Token![=]) {
+                input.parse::<syn::LitStr>()?;
+                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,
+        })
+    }
+}