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

This commit is contained in:
Lucas Fernandes Nogueira 2022-09-28 13:52:18 -03:00 committed by GitHub
parent f98e1b128c
commit 1dd722c4a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 277 additions and 151 deletions

View File

@ -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
}

View File

@ -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());
}

View File

@ -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})`
})
}
<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>
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] }
}
]
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})`
})
container.appendChild(button)
.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: '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>

View File

@ -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!("{:?} {:?}", argument, state.inner());
dbg!(argument.ok_or(MyError::FooError))
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!("{:?} {:?}", 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,