Browse Source

feat(command): add `rename_all` attribute, closes #4898 (#4903)

Lucas Fernandes Nogueira 2 năm trước cách đây
mục cha
commit
1dd722c4a7

+ 97 - 51
core/tauri-macros/src/command/wrapper.rs

@@ -2,39 +2,82 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+use heck::{ToLowerCamelCase, ToSnakeCase};
 use proc_macro::TokenStream;
 use proc_macro2::TokenStream as TokenStream2;
 use quote::{format_ident, quote};
 use syn::{
   ext::IdentExt,
-  parse::{Parse, ParseBuffer},
+  parse::{Parse, ParseStream},
   parse_macro_input,
   spanned::Spanned,
-  FnArg, Ident, ItemFn, Pat, Token, Visibility,
+  FnArg, Ident, ItemFn, Lit, Meta, Pat, Token, Visibility,
 };
 
+struct WrapperAttributes {
+  execution_context: ExecutionContext,
+  argument_case: ArgumentCase,
+}
+
+impl Parse for WrapperAttributes {
+  fn parse(input: ParseStream) -> syn::Result<Self> {
+    let mut wrapper_attributes = WrapperAttributes {
+      execution_context: ExecutionContext::Blocking,
+      argument_case: ArgumentCase::Camel,
+    };
+
+    loop {
+      match input.parse::<Meta>() {
+        Ok(Meta::List(_)) => {}
+        Ok(Meta::NameValue(v)) => {
+          if v.path.is_ident("rename_all") {
+            if let Lit::Str(s) = v.lit {
+              wrapper_attributes.argument_case = match s.value().as_str() {
+                "snake_case" => ArgumentCase::Snake,
+                "camelCase" => ArgumentCase::Camel,
+                _ => {
+                  return Err(syn::Error::new(
+                    s.span(),
+                    "expected \"camelCase\" or \"snake_case\"",
+                  ))
+                }
+              };
+            }
+          }
+        }
+        Ok(Meta::Path(p)) => {
+          if p.is_ident("async") {
+            wrapper_attributes.execution_context = ExecutionContext::Async;
+          } else {
+            return Err(syn::Error::new(p.span(), "expected `async`"));
+          }
+        }
+        Err(_e) => {
+          break;
+        }
+      }
+
+      let lookahead = input.lookahead1();
+      if lookahead.peek(Token![,]) {
+        input.parse::<Token![,]>()?;
+      }
+    }
+
+    Ok(wrapper_attributes)
+  }
+}
+
 /// The execution context of the command.
 enum ExecutionContext {
   Async,
   Blocking,
 }
 
-impl Parse for ExecutionContext {
-  fn parse(input: &ParseBuffer<'_>) -> syn::Result<Self> {
-    if input.is_empty() {
-      return Ok(Self::Blocking);
-    }
-
-    input
-      .parse::<Token![async]>()
-      .map(|_| Self::Async)
-      .map_err(|_| {
-        syn::Error::new(
-          input.span(),
-          "only a single item `async` is currently allowed",
-        )
-      })
-  }
+/// The case of each argument name.
+#[derive(Copy, Clone)]
+enum ArgumentCase {
+  Snake,
+  Camel,
 }
 
 /// The bindings we attach to `tauri::Invoke`.
@@ -61,14 +104,16 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
   };
 
   // body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
-  let body = syn::parse::<ExecutionContext>(attributes)
-    .map(|context| match function.sig.asyncness {
-      Some(_) => ExecutionContext::Async,
-      None => context,
+  let body = syn::parse::<WrapperAttributes>(attributes)
+    .map(|mut attrs| {
+      if function.sig.asyncness.is_some() {
+        attrs.execution_context = ExecutionContext::Async;
+      }
+      attrs
     })
-    .and_then(|context| match context {
-      ExecutionContext::Async => body_async(&function, &invoke),
-      ExecutionContext::Blocking => body_blocking(&function, &invoke),
+    .and_then(|attrs| match attrs.execution_context {
+      ExecutionContext::Async => body_async(&function, &invoke, attrs.argument_case),
+      ExecutionContext::Blocking => body_blocking(&function, &invoke, attrs.argument_case),
     })
     .unwrap_or_else(syn::Error::into_compile_error);
 
@@ -105,9 +150,9 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
 /// See the [`tauri::command`] module for all the items and traits that make this possible.
 ///
 /// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
-fn body_async(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2> {
+fn body_async(function: &ItemFn, invoke: &Invoke, case: ArgumentCase) -> syn::Result<TokenStream2> {
   let Invoke { message, resolver } = invoke;
-  parse_args(function, message).map(|args| {
+  parse_args(function, message, case).map(|args| {
     quote! {
       #resolver.respond_async_serialized(async move {
         let result = $path(#(#args?),*);
@@ -123,9 +168,13 @@ fn body_async(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2> {
 /// See the [`tauri::command`] module for all the items and traits that make this possible.
 ///
 /// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
-fn body_blocking(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2> {
+fn body_blocking(
+  function: &ItemFn,
+  invoke: &Invoke,
+  case: ArgumentCase,
+) -> syn::Result<TokenStream2> {
   let Invoke { message, resolver } = invoke;
-  let args = parse_args(function, message)?;
+  let args = parse_args(function, message, case)?;
 
   // the body of a `match` to early return any argument that wasn't successful in parsing.
   let match_body = quote!({
@@ -141,17 +190,26 @@ fn body_blocking(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2
 }
 
 /// Parse all arguments for the command wrapper to use from the signature of the command function.
-fn parse_args(function: &ItemFn, message: &Ident) -> syn::Result<Vec<TokenStream2>> {
+fn parse_args(
+  function: &ItemFn,
+  message: &Ident,
+  case: ArgumentCase,
+) -> syn::Result<Vec<TokenStream2>> {
   function
     .sig
     .inputs
     .iter()
-    .map(|arg| parse_arg(&function.sig.ident, arg, message))
+    .map(|arg| parse_arg(&function.sig.ident, arg, message, case))
     .collect()
 }
 
 /// Transform a [`FnArg`] into a command argument.
-fn parse_arg(command: &Ident, arg: &FnArg, message: &Ident) -> syn::Result<TokenStream2> {
+fn parse_arg(
+  command: &Ident,
+  arg: &FnArg,
+  message: &Ident,
+  case: ArgumentCase,
+) -> syn::Result<TokenStream2> {
   // we have no use for self arguments
   let mut arg = match arg {
     FnArg::Typed(arg) => arg.pat.as_ref().clone(),
@@ -185,9 +243,13 @@ fn parse_arg(command: &Ident, arg: &FnArg, message: &Ident) -> syn::Result<Token
     ));
   }
 
-  // snake_case -> camelCase
-  if key.as_str().contains('_') {
-    key = snake_case_to_camel_case(key.as_str());
+  match case {
+    ArgumentCase::Camel => {
+      key = key.to_lower_camel_case();
+    }
+    ArgumentCase::Snake => {
+      key = key.to_snake_case();
+    }
   }
 
   Ok(quote!(::tauri::command::CommandArg::from_command(
@@ -198,19 +260,3 @@ fn parse_arg(command: &Ident, arg: &FnArg, message: &Ident) -> syn::Result<Token
     }
   )))
 }
-
-/// Convert a snake_case string into camelCase, no underscores will be left.
-fn snake_case_to_camel_case(key: &str) -> String {
-  let mut camel = String::with_capacity(key.len());
-  let mut to_upper = false;
-
-  for c in key.chars() {
-    match c {
-      '_' => to_upper = true,
-      c if std::mem::take(&mut to_upper) => camel.push(c.to_ascii_uppercase()),
-      c => camel.push(c),
-    }
-  }
-
-  camel
-}

+ 4 - 4
examples/commands/commands.rs

@@ -17,11 +17,11 @@ pub fn message(_argument: String) {}
 pub fn resolver(_argument: String) {}
 
 #[command]
-pub fn simple_command(argument: String) {
-  println!("{}", argument);
+pub fn simple_command(the_argument: String) {
+  println!("{}", the_argument);
 }
 
 #[command]
-pub fn stateful_command(argument: Option<String>, state: State<'_, super::MyState>) {
-  println!("{:?} {:?}", argument, state.inner());
+pub fn stateful_command(the_argument: Option<String>, state: State<'_, super::MyState>) {
+  println!("{:?} {:?}", the_argument, state.inner());
 }

+ 73 - 61
examples/commands/index.html

@@ -1,68 +1,80 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Tauri</title>
-  </head>
 
-  <body>
-    <h1>Tauri Commands</h1>
-    <div>Response: <span id="response"></span></div>
-    <div>Without Args: <span id="response-optional"></span></div>
-    <div id="container"></div>
-    <script>
-      function runCommand(commandName, args, optional) {
-        const id = optional ? '#response-optional' : '#response'
-        const result = document.querySelector(id)
-        window.__TAURI__
-          .invoke(commandName, args)
-          .then((response) => {
-            result.innerText = `Ok(${response})`
-          })
-          .catch((error) => {
-            result.innerText = `Err(${error})`
-          })
-      }
-
-      const container = document.querySelector('#container')
-      const commands = [
-        { name: 'borrow_cmd' },
-        { name: 'window_label' },
-        { name: 'simple_command' },
-        { name: 'stateful_command' },
-        { name: 'async_simple_command' },
-        { name: 'future_simple_command' },
-        { name: 'async_stateful_command' },
-        { name: 'simple_command_with_result' },
-        { name: 'stateful_command_with_result' },
-        { name: 'async_simple_command_with_result' },
-        { name: 'future_simple_command_with_return' },
-        { name: 'future_simple_command_with_result' },
-        { name: 'async_stateful_command_with_result' },
-        { name: 'command_arguments_wild' },
-        {
-          name: 'command_arguments_struct',
-          args: { Person: { name: 'ferris', age: 6 } }
-        },
-        {
-          name: 'command_arguments_tuple_struct',
-          args: { InlinePerson: ['ferris', 6] }
-        }
-      ]
+<head>
+  <meta charset="UTF-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Tauri</title>
+</head>
 
-      for (const command of commands) {
-        const { name } = command
-        const args = command.args ?? { argument: 'value' }
-        const button = document.createElement('button')
-        button.innerHTML = `Run ${name}`
-        button.addEventListener('click', function () {
-          runCommand(name, args, false)
-          runCommand(name, Object.create(null), true)
+<body>
+  <h1>Tauri Commands</h1>
+  <div>Response: <span id="response"></span></div>
+  <div>Without Args: <span id="response-optional"></span></div>
+  <div id="container"></div>
+  <script>
+    function runCommand(commandName, args, optional) {
+      const id = optional ? '#response-optional' : '#response'
+      const result = document.querySelector(id)
+      window.__TAURI__
+        .invoke(commandName, args)
+        .then((response) => {
+          result.innerText = `Ok(${response})`
+        })
+        .catch((error) => {
+          result.innerText = `Err(${error})`
         })
-        container.appendChild(button)
+    }
+
+    const container = document.querySelector('#container')
+    const commands = [
+      { name: 'borrow_cmd' },
+      { name: 'window_label' },
+      { name: 'simple_command' },
+      { name: 'stateful_command' },
+      { name: 'async_simple_command' },
+      { name: 'async_simple_command_snake' },
+      { name: 'future_simple_command' },
+      { name: 'async_stateful_command' },
+      { name: 'simple_command_with_result' },
+      // snake
+      { name: 'future_simple_command_snake' },
+      { name: 'future_simple_command_with_return_snake' },
+      { name: 'future_simple_command_with_result_snake' },
+      { name: 'force_async_snake' },
+      { name: 'force_async_with_result_snake' },
+      { name: 'simple_command_with_result_snake' },
+      { name: 'stateful_command_with_result_snake' },
+      // state
+      { name: 'stateful_command_with_result' },
+      { name: 'async_simple_command_with_result' },
+      { name: 'future_simple_command_with_return' },
+      { name: 'future_simple_command_with_result' },
+      { name: 'async_stateful_command_with_result' },
+      { name: 'command_arguments_wild' },
+      {
+        name: 'command_arguments_struct',
+        args: { Person: { name: 'ferris', age: 6 } }
+      },
+      {
+        name: 'command_arguments_tuple_struct',
+        args: { InlinePerson: ['ferris', 6] }
       }
-    </script>
-  </body>
+    ]
+
+    for (const command of commands) {
+      const { name } = command
+      const args = command.args ?? { [name.endsWith('snake') ? 'the_argument' : 'theArgument']: 'value' }
+      const button = document.createElement('button')
+      button.innerHTML = `Run ${name}`
+      button.addEventListener('click', function () {
+        runCommand(name, args, false)
+        runCommand(name, Object.create(null), true)
+      })
+      container.appendChild(button)
+    }
+  </script>
+</body>
+
 </html>

+ 103 - 35
examples/commands/main.rs

@@ -36,88 +36,148 @@ fn window_label(window: Window) {
 // Async commands
 
 #[command]
-async fn async_simple_command(argument: String) {
-  println!("{}", argument);
+async fn async_simple_command(the_argument: String) {
+  println!("{}", the_argument);
+}
+
+#[command(rename_all = "snake_case")]
+async fn async_simple_command_snake(the_argument: String) {
+  println!("{}", the_argument);
 }
 
 #[command]
 async fn async_stateful_command(
-  argument: Option<String>,
+  the_argument: Option<String>,
   state: State<'_, MyState>,
 ) -> Result<(), ()> {
-  println!("{:?} {:?}", argument, state.inner());
+  println!("{:?} {:?}", the_argument, state.inner());
   Ok(())
 }
+// ------------------------ Raw future commands ------------------------
 
-// Raw future commands
 #[command(async)]
-fn future_simple_command(argument: String) -> impl std::future::Future<Output = ()> {
-  println!("{}", argument);
+fn future_simple_command(the_argument: String) -> impl std::future::Future<Output = ()> {
+  println!("{}", the_argument);
   std::future::ready(())
 }
 
 #[command(async)]
 fn future_simple_command_with_return(
-  argument: String,
+  the_argument: String,
 ) -> impl std::future::Future<Output = String> {
-  println!("{}", argument);
-  std::future::ready(argument)
+  println!("{}", the_argument);
+  std::future::ready(the_argument)
 }
 
 #[command(async)]
 fn future_simple_command_with_result(
-  argument: String,
+  the_argument: String,
 ) -> impl std::future::Future<Output = Result<String, ()>> {
-  println!("{}", argument);
-  std::future::ready(Ok(argument))
+  println!("{}", the_argument);
+  std::future::ready(Ok(the_argument))
 }
 
 #[command(async)]
-fn force_async(argument: String) -> String {
-  argument
+fn force_async(the_argument: String) -> String {
+  the_argument
 }
 
 #[command(async)]
-fn force_async_with_result(argument: &str) -> Result<&str, MyError> {
-  (!argument.is_empty())
-    .then(|| argument)
+fn force_async_with_result(the_argument: &str) -> Result<&str, MyError> {
+  (!the_argument.is_empty())
+    .then(|| the_argument)
+    .ok_or(MyError::FooError)
+}
+
+// ------------------------ Raw future commands - snake_case ------------------------
+
+#[command(async, rename_all = "snake_case")]
+fn future_simple_command_snake(the_argument: String) -> impl std::future::Future<Output = ()> {
+  println!("{}", the_argument);
+  std::future::ready(())
+}
+
+#[command(async, rename_all = "snake_case")]
+fn future_simple_command_with_return_snake(
+  the_argument: String,
+) -> impl std::future::Future<Output = String> {
+  println!("{}", the_argument);
+  std::future::ready(the_argument)
+}
+
+#[command(async, rename_all = "snake_case")]
+fn future_simple_command_with_result_snake(
+  the_argument: String,
+) -> impl std::future::Future<Output = Result<String, ()>> {
+  println!("{}", the_argument);
+  std::future::ready(Ok(the_argument))
+}
+
+#[command(async, rename_all = "snake_case")]
+fn force_async_snake(the_argument: String) -> String {
+  the_argument
+}
+
+#[command(rename_all = "snake_case", async)]
+fn force_async_with_result_snake(the_argument: &str) -> Result<&str, MyError> {
+  (!the_argument.is_empty())
+    .then(|| the_argument)
     .ok_or(MyError::FooError)
 }
 
 // ------------------------ Commands returning Result ------------------------
 
 #[command]
-fn simple_command_with_result(argument: String) -> Result<String, MyError> {
-  println!("{}", argument);
-  (!argument.is_empty())
-    .then(|| argument)
+fn simple_command_with_result(the_argument: String) -> Result<String, MyError> {
+  println!("{}", the_argument);
+  (!the_argument.is_empty())
+    .then(|| the_argument)
     .ok_or(MyError::FooError)
 }
 
 #[command]
 fn stateful_command_with_result(
-  argument: Option<String>,
+  the_argument: Option<String>,
+  state: State<'_, MyState>,
+) -> Result<String, MyError> {
+  println!("{:?} {:?}", the_argument, state.inner());
+  dbg!(the_argument.ok_or(MyError::FooError))
+}
+
+// ------------------------ Commands returning Result - snake_case ------------------------
+
+#[command(rename_all = "snake_case")]
+fn simple_command_with_result_snake(the_argument: String) -> Result<String, MyError> {
+  println!("{}", the_argument);
+  (!the_argument.is_empty())
+    .then(|| the_argument)
+    .ok_or(MyError::FooError)
+}
+
+#[command(rename_all = "snake_case")]
+fn stateful_command_with_result_snake(
+  the_argument: Option<String>,
   state: State<'_, MyState>,
 ) -> Result<String, MyError> {
-  println!("{:?} {:?}", argument, state.inner());
-  dbg!(argument.ok_or(MyError::FooError))
+  println!("{:?} {:?}", the_argument, state.inner());
+  dbg!(the_argument.ok_or(MyError::FooError))
 }
 
 // Async commands
 
 #[command]
-async fn async_simple_command_with_result(argument: String) -> Result<String, MyError> {
-  println!("{}", argument);
-  Ok(argument)
+async fn async_simple_command_with_result(the_argument: String) -> Result<String, MyError> {
+  println!("{}", the_argument);
+  Ok(the_argument)
 }
 
 #[command]
 async fn async_stateful_command_with_result(
-  argument: Option<String>,
+  the_argument: Option<String>,
   state: State<'_, MyState>,
 ) -> Result<String, MyError> {
-  println!("{:?} {:?}", argument, state.inner());
-  Ok(argument.unwrap_or_else(|| "".to_string()))
+  println!("{:?} {:?}", the_argument, state.inner());
+  Ok(the_argument.unwrap_or_else(|| "".to_string()))
 }
 
 // Non-Ident command function arguments
@@ -147,13 +207,13 @@ fn command_arguments_tuple_struct(InlinePerson(name, age): InlinePerson<'_>) {
 }
 
 #[command]
-fn borrow_cmd(argument: &str) -> &str {
-  argument
+fn borrow_cmd(the_argument: &str) -> &str {
+  the_argument
 }
 
 #[command]
-fn borrow_cmd_async(argument: &str) -> &str {
-  argument
+fn borrow_cmd_async(the_argument: &str) -> &str {
+  the_argument
 }
 
 fn main() {
@@ -180,6 +240,14 @@ fn main() {
       command_arguments_wild,
       command_arguments_struct,
       simple_command_with_result,
+      async_simple_command_snake,
+      future_simple_command_snake,
+      future_simple_command_with_return_snake,
+      future_simple_command_with_result_snake,
+      force_async_snake,
+      force_async_with_result_snake,
+      simple_command_with_result_snake,
+      stateful_command_with_result_snake,
       stateful_command_with_result,
       command_arguments_tuple_struct,
       async_simple_command_with_result,