diff --git a/Cargo.toml b/Cargo.toml index db351e8..496d48d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ quote = "1.0" [lib] proc-macro = true +path = "src/lib.rs" [badges.maintenance] status = "actively-developed" diff --git a/README.md b/README.md index c36b295..82aed99 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Maybe-Async Procedure Macro +![Maintenance](https://img.shields.io/badge/maintenance-activly--developed-brightgreen.svg) + +# maybe-async **Why bother writing similar code twice for blocking and async code?** @@ -24,16 +26,16 @@ those `async` and `await` when you need a blocking code. These procedural macros can be applied to the following codes: - trait item declaration -- trait implmentation +- trait implementation - function definition - struct definition **RECOMMENDATION**: Enable **resolver ver2** in your crate, which is introduced in Rust 1.51. If not, two crates in dependency with conflict -version (one async and another blocking) can fail complilation. +version (one async and another blocking) can fail compilation. -## Motivation +### Motivation The async/await language feature alters the async world of rust. Comparing with the map/and_then style, now the async code really resembles @@ -42,9 +44,9 @@ sync version code. In many crates, the async and sync version of crates shares the same API, but the minor difference that all async code must be awaited prevent the unification of async and sync code. In other words, we are forced to write -an async and an sync implementation repectively. +an async and a sync implementation respectively. -## Macros in Detail +### Macros in Detail `maybe-async` offers 4 set of attribute macros: `maybe_async`, `sync_impl`/`async_impl`, `must_be_sync`/`must_be_async`, and `test`. @@ -71,7 +73,7 @@ blocking code except for async/await keywords. And use feature gate maybe_async = "0.2" ``` - Wanna convert async code to sync? Add `maybe_async` to dependencies with + Want to convert async code to sync? Add `maybe_async` to dependencies with an `is_sync` feature gate. In this way, `maybe_async` is the same as `must_be_sync`: @@ -80,19 +82,31 @@ blocking code except for async/await keywords. And use feature gate maybe_async = { version = "0.2", features = ["is_sync"] } ``` - Not all async traits need futures that are `dyn Future + Send`. - To avoid having "Send" and "Sync" bounds placed on the async trait - methods, invoke the maybe_async macro as #[maybe_async(?Send)] on both - the trait and the impl blocks. + There are three usage variants for `maybe_async` attribute macros: + - `#[maybe_async]` or `#[maybe_async(Send)]` + + In this mode, `#[async_trait::async_trait]` is added to trait declarations and trait implementations + to support async fn in traits. + + - `#[maybe_async(?Send)]` + + Not all async traits need futures that are `dyn Future + Send`. + In this mode, `#[async_trait::async_trait(?Send)]` is added to trait declarations and trait implementations, + to avoid having "Send" and "Sync" bounds placed on the async trait + methods. + - `#[maybe_async(AFIT)]` + + AFIT is acronym for **a**sync **f**unction **i**n **t**rait, stabilized from rust 1.74 - `must_be_async` - **Keep async**. Add `async_trait` attribute macro for trait declaration - or implementation to bring async fn support in traits. + **Keep async**. - To avoid having "Send" and "Sync" bounds placed on the async trait - methods, invoke the maybe_async macro as #[must_be_async(?Send)]. + There are three usage variants for `must_be_async` attribute macros: + - `#[must_be_async]` or `#[must_be_async(Send)]` + - `#[must_be_async(?Send)]` + - `#[must_be_async(AFIT)]` - `must_be_sync` @@ -102,25 +116,26 @@ blocking code except for async/await keywords. And use feature gate - `sync_impl` - An sync implementation should on compile on blocking implementation and -must simply disappear when we want async version. + A sync implementation should compile on blocking implementation and + must simply disappear when we want async version. Although most of the API are almost the same, there definitely come to a point when the async and sync version should differ greatly. For example, a MongoDB client may use the same API for async and sync - verison, but the code to actually send reqeust are quite different. + version, but the code to actually send reqeust are quite different. Here, we can use `sync_impl` to mark a synchronous implementation, and a - sync implementation shoule disappear when we want async version. + sync implementation should disappear when we want async version. - `async_impl` An async implementation should on compile on async implementation and -must simply disappear when we want sync version. - - To avoid having "Send" and "Sync" bounds placed on the async trait - methods, invoke the maybe_async macro as #[async_impl(?Send)]. + must simply disappear when we want sync version. + There are three usage variants for `async_impl` attribute macros: + - `#[async_impl]` or `#[async_impl(Send)]` + - `#[async_impl(?Send)]` + - `#[async_impl(AFIT)]` - `test` @@ -128,15 +143,26 @@ must simply disappear when we want sync version. You can specify the condition to compile to sync test code and also the conditions to compile to async test code with given test - macro, e.x. `tokio::test`, `async_std::test` and etc. When only sync + macro, e.x. `tokio::test`, `async_std::test`, etc. When only sync condition is specified,the test code only compiles when sync condition is met. ```rust - #[maybe_async::test( + # #[maybe_async::maybe_async] + # async fn async_fn() -> bool { + # true + # } + + ##[maybe_async::test( feature="is_sync", - async(all(not(feature="is_sync"), feature="async_std"), async_std::test), - async(all(not(feature="is_sync"), feature="tokio"), tokio::test) + async( + all(not(feature="is_sync"), feature="async_std"), + async_std::test + ), + async( + all(not(feature="is_sync"), feature="tokio"), + tokio::test + ) )] async fn test_async_fn() { let res = async_fn().await; @@ -144,14 +170,14 @@ must simply disappear when we want sync version. } ``` -## What's Under the Hook +### What's Under the Hook `maybe-async` compiles your code in different way with the `is_sync` feature -gate. It remove all `await` and `async` keywords in your code under +gate. It removes all `await` and `async` keywords in your code under `maybe_async` macro and conditionally compiles codes under `async_impl` and `sync_impl`. -Here is an detailed example on what's going on whe the `is_sync` feature +Here is a detailed example on what's going on whe the `is_sync` feature gate set or not. ```rust @@ -252,13 +278,13 @@ fn maybe_async_fn() -> Result<(), ()> { } ``` -## Examples +### Examples -### rust client for services +#### rust client for services When implementing rust client for any services, like awz3. The higher level API of async and sync version is almost the same, such as creating or -deleting a bucket, retrieving an object and etc. +deleting a bucket, retrieving an object, etc. The example `service_client` is a proof of concept that `maybe_async` can actually free us from writing almost the same code for sync and async. We @@ -266,5 +292,7 @@ can toggle between a sync AWZ3 client and async one by `is_sync` feature gate when we add `maybe-async` to dependency. -# License -MIT \ No newline at end of file +## License +MIT + +License: MIT diff --git a/examples/service_client.rs b/examples/service_client.rs index fec6813..3db9d2b 100644 --- a/examples/service_client.rs +++ b/examples/service_client.rs @@ -9,7 +9,9 @@ type Method = String; /// InnerClient are used to actually send request, /// which differ a lot between sync and async. -#[maybe_async::maybe_async] +/// +/// Use native async function in trait +#[maybe_async::maybe_async(AFIT)] trait InnerClient { async fn request(method: Method, url: Url, data: String) -> Response; #[inline] @@ -38,7 +40,7 @@ impl InnerClient for ServiceClient { } /// Asynchronous implementation, only compiles when `is_sync` feature is off. -#[maybe_async::async_impl] +#[maybe_async::async_impl(AFIT)] impl InnerClient for ServiceClient { async fn request(method: Method, url: Url, data: String) -> Response { // your implementation for async, like use `reqwest::client` diff --git a/src/lib.rs b/src/lib.rs index 7e6c81f..6a8a6c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,3 @@ -//! -//! # Maybe-Async Procedure Macro -//! //! **Why bother writing similar code twice for blocking and async code?** //! //! [![Build Status](https://github.com/fMeow/maybe-async-rs/workflows/CI%20%28Linux%29/badge.svg?branch=main)](https://github.com/fMeow/maybe-async-rs/actions) @@ -25,13 +22,13 @@ //! //! These procedural macros can be applied to the following codes: //! - trait item declaration -//! - trait implmentation +//! - trait implementation //! - function definition //! - struct definition //! //! **RECOMMENDATION**: Enable **resolver ver2** in your crate, which is //! introduced in Rust 1.51. If not, two crates in dependency with conflict -//! version (one async and another blocking) can fail complilation. +//! version (one async and another blocking) can fail compilation. //! //! //! ## Motivation @@ -43,7 +40,7 @@ //! In many crates, the async and sync version of crates shares the same API, //! but the minor difference that all async code must be awaited prevent the //! unification of async and sync code. In other words, we are forced to write -//! an async and an sync implementation repectively. +//! an async and a sync implementation respectively. //! //! ## Macros in Detail //! @@ -72,7 +69,7 @@ //! maybe_async = "0.2" //! ``` //! -//! Wanna convert async code to sync? Add `maybe_async` to dependencies with +//! Want to convert async code to sync? Add `maybe_async` to dependencies with //! an `is_sync` feature gate. In this way, `maybe_async` is the same as //! `must_be_sync`: //! @@ -81,19 +78,31 @@ //! maybe_async = { version = "0.2", features = ["is_sync"] } //! ``` //! -//! Not all async traits need futures that are `dyn Future + Send`. -//! To avoid having "Send" and "Sync" bounds placed on the async trait -//! methods, invoke the maybe_async macro as #[maybe_async(?Send)] on both -//! the trait and the impl blocks. +//! There are three usage variants for `maybe_async` attribute macros: +//! - `#[maybe_async]` or `#[maybe_async(Send)]` +//! +//! In this mode, `#[async_trait::async_trait]` is added to trait declarations and trait implementations +//! to support async fn in traits. +//! +//! - `#[maybe_async(?Send)]` +//! +//! Not all async traits need futures that are `dyn Future + Send`. +//! In this mode, `#[async_trait::async_trait(?Send)]` is added to trait declarations and trait implementations, +//! to avoid having "Send" and "Sync" bounds placed on the async trait +//! methods. +//! +//! - `#[maybe_async(AFIT)]` //! +//! AFIT is acronym for **a**sync **f**unction **i**n **t**rait, stabilized from rust 1.74 //! //! - `must_be_async` //! -//! **Keep async**. Add `async_trait` attribute macro for trait declaration -//! or implementation to bring async fn support in traits. +//! **Keep async**. //! -//! To avoid having "Send" and "Sync" bounds placed on the async trait -//! methods, invoke the maybe_async macro as #[must_be_async(?Send)]. +//! There are three usage variants for `must_be_async` attribute macros: +//! - `#[must_be_async]` or `#[must_be_async(Send)]` +//! - `#[must_be_async(?Send)]` +//! - `#[must_be_async(AFIT)]` //! //! - `must_be_sync` //! @@ -103,25 +112,26 @@ //! //! - `sync_impl` //! -//! An sync implementation should on compile on blocking implementation and -//! must simply disappear when we want async version. +//! A sync implementation should compile on blocking implementation and +//! must simply disappear when we want async version. //! //! Although most of the API are almost the same, there definitely come to a //! point when the async and sync version should differ greatly. For //! example, a MongoDB client may use the same API for async and sync -//! verison, but the code to actually send reqeust are quite different. +//! version, but the code to actually send reqeust are quite different. //! //! Here, we can use `sync_impl` to mark a synchronous implementation, and a -//! sync implementation shoule disappear when we want async version. +//! sync implementation should disappear when we want async version. //! //! - `async_impl` //! //! An async implementation should on compile on async implementation and -//! must simply disappear when we want sync version. -//! -//! To avoid having "Send" and "Sync" bounds placed on the async trait -//! methods, invoke the maybe_async macro as #[async_impl(?Send)]. +//! must simply disappear when we want sync version. //! +//! There are three usage variants for `async_impl` attribute macros: +//! - `#[async_impl]` or `#[async_impl(Send)]` +//! - `#[async_impl(?Send)]` +//! - `#[async_impl(AFIT)]` //! //! - `test` //! @@ -129,7 +139,7 @@ //! //! You can specify the condition to compile to sync test code //! and also the conditions to compile to async test code with given test -//! macro, e.x. `tokio::test`, `async_std::test` and etc. When only sync +//! macro, e.x. `tokio::test`, `async_std::test`, etc. When only sync //! condition is specified,the test code only compiles when sync condition //! is met. //! @@ -159,11 +169,11 @@ //! ## What's Under the Hook //! //! `maybe-async` compiles your code in different way with the `is_sync` feature -//! gate. It remove all `await` and `async` keywords in your code under +//! gate. It removes all `await` and `async` keywords in your code under //! `maybe_async` macro and conditionally compiles codes under `async_impl` and //! `sync_impl`. //! -//! Here is an detailed example on what's going on whe the `is_sync` feature +//! Here is a detailed example on what's going on whe the `is_sync` feature //! gate set or not. //! //! ```rust @@ -270,7 +280,7 @@ //! //! When implementing rust client for any services, like awz3. The higher level //! API of async and sync version is almost the same, such as creating or -//! deleting a bucket, retrieving an object and etc. +//! deleting a bucket, retrieving an object, etc. //! //! The example `service_client` is a proof of concept that `maybe_async` can //! actually free us from writing almost the same code for sync and async. We @@ -299,22 +309,26 @@ use crate::{parse::Item, visit::AsyncAwaitRemoval}; mod parse; mod visit; +enum AsyncTraitMode { + Send, + NotSend, + Off, +} -fn convert_async(input: &mut Item, send: bool) -> TokenStream2 { - if send { - match input { - Item::Impl(item) => quote!(#[async_trait::async_trait]#item), - Item::Trait(item) => quote!(#[async_trait::async_trait]#item), - Item::Fn(item) => quote!(#item), - Item::Static(item) => quote!(#item), - } - } else { - match input { - Item::Impl(item) => quote!(#[async_trait::async_trait(?Send)]#item), - Item::Trait(item) => quote!(#[async_trait::async_trait(?Send)]#item), - Item::Fn(item) => quote!(#item), - Item::Static(item) => quote!(#item), - } +fn convert_async(input: &mut Item, async_trait_mode: AsyncTraitMode) -> TokenStream2 { + match input { + Item::Trait(item) => match async_trait_mode { + AsyncTraitMode::Send => quote!(#[async_trait::async_trait]#item), + AsyncTraitMode::NotSend => quote!(#[async_trait::async_trait(?Send)]#item), + AsyncTraitMode::Off => quote!(#item), + }, + Item::Impl(item) => match async_trait_mode { + AsyncTraitMode::Send => quote!(#[async_trait::async_trait]#item), + AsyncTraitMode::NotSend => quote!(#[async_trait::async_trait(?Send)]#item), + AsyncTraitMode::Off => quote!(#item), + }, + Item::Fn(item) => quote!(#item), + Item::Static(item) => quote!(#item), } } @@ -350,27 +364,35 @@ fn convert_sync(input: &mut Item) -> TokenStream2 { } } +fn async_mode(arg: &str) -> Result { + match arg { + "" | "Send" => Ok(AsyncTraitMode::Send), + "?Send" => Ok(AsyncTraitMode::NotSend), + // acronym for Async Function in Trait, + // TODO make AFIT as default in future release + "AFIT" => Ok(AsyncTraitMode::Off), + _ => Err(syn::Error::new( + Span::call_site(), + "Only accepts `Send`, `?Send` or `AFIT` (native async function in trait)", + )), + } +} + /// maybe_async attribute macro /// /// Can be applied to trait item, trait impl, functions and struct impls. #[proc_macro_attribute] pub fn maybe_async(args: TokenStream, input: TokenStream) -> TokenStream { - let send = match args.to_string().replace(" ", "").as_str() { - "" | "Send" => true, - "?Send" => false, - _ => { - return syn::Error::new(Span::call_site(), "Only accepts `Send` or `?Send`") - .to_compile_error() - .into(); - } + let mode = match async_mode(args.to_string().replace(" ", "").as_str()) { + Ok(m) => m, + Err(e) => return e.to_compile_error().into(), }; - let mut item = parse_macro_input!(input as Item); let token = if cfg!(feature = "is_sync") { convert_sync(&mut item) } else { - convert_async(&mut item, send) + convert_async(&mut item, mode) }; token.into() } @@ -378,17 +400,12 @@ pub fn maybe_async(args: TokenStream, input: TokenStream) -> TokenStream { /// convert marked async code to async code with `async-trait` #[proc_macro_attribute] pub fn must_be_async(args: TokenStream, input: TokenStream) -> TokenStream { - let send = match args.to_string().replace(" ", "").as_str() { - "" | "Send" => true, - "?Send" => false, - _ => { - return syn::Error::new(Span::call_site(), "Only accepts `Send` or `?Send`") - .to_compile_error() - .into(); - } + let mode = match async_mode(args.to_string().replace(" ", "").as_str()) { + Ok(m) => m, + Err(e) => return e.to_compile_error().into(), }; let mut item = parse_macro_input!(input as Item); - convert_async(&mut item, send).into() + convert_async(&mut item, mode).into() } /// convert marked async code to sync code @@ -419,21 +436,15 @@ pub fn sync_impl(_args: TokenStream, input: TokenStream) -> TokenStream { /// When `is_sync` is set, marked code is removed. #[proc_macro_attribute] pub fn async_impl(args: TokenStream, _input: TokenStream) -> TokenStream { - let send = match args.to_string().replace(" ", "").as_str() { - "" | "Send" => true, - "?Send" => false, - _ => { - return syn::Error::new(Span::call_site(), "Only accepts `Send` or `?Send`") - .to_compile_error() - .into(); - } + let mode = match async_mode(args.to_string().replace(" ", "").as_str()) { + Ok(m) => m, + Err(e) => return e.to_compile_error().into(), }; - let token = if cfg!(feature = "is_sync") { quote!() } else { let mut item = parse_macro_input!(_input as Item); - convert_async(&mut item, send) + convert_async(&mut item, mode) }; token.into() } diff --git a/tests/ui/01-maybe-async.rs b/tests/ui/01-maybe-async.rs index 33b8248..73796de 100644 --- a/tests/ui/01-maybe-async.rs +++ b/tests/ui/01-maybe-async.rs @@ -35,6 +35,17 @@ pub(crate) trait PubCrateTrait { } } +#[maybe_async(AFIT)] +trait AfitTrait { + fn sync_fn_afit() {} + + async fn declare_async_afit(&self); + + async fn async_fn_afit(&self) { + self.declare_async_afit().await + } +} + #[maybe_async] async fn async_fn() {} @@ -60,11 +71,24 @@ impl Trait for Struct { } } +#[maybe_async(AFIT)] +impl AfitTrait for Struct { + fn sync_fn_afit() {} + + async fn declare_async_afit(&self) {} + + async fn async_fn_afit(&self) { + async { self.declare_async_afit().await }.await + } +} + #[cfg(feature = "is_sync")] fn main() -> std::result::Result<(), ()> { let s = Struct; s.declare_async(); s.async_fn(); + s.declare_async_afit(); + s.async_fn_afit(); async_fn(); pub_async_fn(); Ok(()) @@ -76,6 +100,8 @@ async fn main() { let s = Struct; s.declare_async().await; s.async_fn().await; + s.declare_async_afit().await; + s.async_fn_afit().await; async_fn().await; pub_async_fn().await; } diff --git a/tests/ui/02-must-be-async.rs b/tests/ui/02-must-be-async.rs index cfe2285..e36fa49 100644 --- a/tests/ui/02-must-be-async.rs +++ b/tests/ui/02-must-be-async.rs @@ -42,6 +42,17 @@ pub(crate) trait PubCrateTrait { } } +#[maybe_async::maybe_async(AFIT)] +trait AfitTrait { + fn sync_fn_afit() {} + + async fn declare_async_afit(&self); + + async fn async_fn_afit(&self) { + self.declare_async_afit().await + } +} + #[cfg(not(feature = "is_sync"))] #[maybe_async::must_be_async] async fn async_fn() {} @@ -81,6 +92,17 @@ impl NotSendTrait for Struct { async { self.declare_async_not_send().await }.await } } +#[cfg(not(feature = "is_sync"))] +#[maybe_async::must_be_async(AFIT)] +impl AfitTrait for Struct { + fn sync_fn_afit() {} + + async fn declare_async_afit(&self) {} + + async fn async_fn_afit(&self) { + async { self.declare_async_afit().await }.await + } +} #[cfg(feature = "is_sync")] fn main() {} @@ -91,6 +113,8 @@ async fn main() { let s = Struct; s.declare_async().await; s.async_fn().await; + s.declare_async_afit().await; + s.async_fn_afit().await; async_fn().await; pub_async_fn().await; }