diff options
Diffstat (limited to 'README.md')
-rw-r--r-- | README.md | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..1289a73 --- /dev/null +++ b/README.md @@ -0,0 +1,295 @@ +# maybe-async + +**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) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![Latest Version](https://img.shields.io/crates/v/maybe-async.svg)](https://crates.io/crates/maybe-async) +[![maybe-async](https://docs.rs/maybe-async/badge.svg)](https://docs.rs/maybe-async) + +When implementing both sync and async versions of API in a crate, most API +of the two version are almost the same except for some async/await keyword. + +`maybe-async` help unifying async and sync implementation by **procedural +macro**. +- Write async code with normal `async`, `await`, and let `maybe_async` + handles +those `async` and `await` when you need a blocking code. +- Switch between sync and async by toggling `is_sync` feature gate in + `Cargo.toml`. +- use `must_be_async` and `must_be_sync` to keep code in specified version +- use `async_impl` and `sync_impl` to only compile code block on specified + version +- A handy macro to unify unit test code is also provided. + +These procedural macros can be applied to the following codes: +- trait item declaration +- 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 compilation. + + +### 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 +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 a sync implementation respectively. + +### 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`. + +To use `maybe-async`, we must know which block of codes is only used on +blocking implementation, and which on async. These two implementation should +share the same function signatures except for async/await keywords, and use +`sync_impl` and `async_impl` to mark these implementation. + +Use `maybe_async` macro on codes that share the same API on both async and +blocking code except for async/await keywords. And use feature gate +`is_sync` in `Cargo.toml` to toggle between async and blocking code. + +- `maybe_async` + + Offers a unified feature gate to provide sync and async conversion on + demand by feature gate `is_sync`, with **async first** policy. + + Want to keep async code? add `maybe_async` in dependencies with default + features, which means `maybe_async` is the same as `must_be_async`: + + ```toml + [dependencies] + maybe_async = "0.2" + ``` + + 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`: + + ```toml + [dependencies] + maybe_async = { version = "0.2", features = ["is_sync"] } + ``` + + There are three usage variants for `maybe_async` attribute usage: + - `#[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 + + For compatibility reasons, the `async fn` in traits is supported via a verbose `AFIT` flag. This will become + the default mode for the next major release. + +- `must_be_async` + + **Keep async**. + + There are three usage variants for `must_be_async` attribute usage: + - `#[must_be_async]` or `#[must_be_async(Send)]` + - `#[must_be_async(?Send)]` + - `#[must_be_async(AFIT)]` + +- `must_be_sync` + + **Convert to sync code**. Convert the async code into sync code by + removing all `async move`, `async` and `await` keyword + + +- `sync_impl` + + 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 + 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 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. + + There are three usage variants for `async_impl` attribute usage: + - `#[async_impl]` or `#[async_impl(Send)]` + - `#[async_impl(?Send)]` + - `#[async_impl(AFIT)]` + +- `test` + + Handy macro to unify async and sync **unit and e2e test** code. + + 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`, etc. When only sync + condition is specified,the test code only compiles when sync condition + is met. + + ```rust + # #[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 fn test_async_fn() { + let res = async_fn().await; + assert_eq!(res, true); + } + ``` + +### What's Under the Hook + +`maybe-async` compiles your code in different way with the `is_sync` feature +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 a detailed example on what's going on whe the `is_sync` feature +gate set or not. + +```rust +#[maybe_async::maybe_async(AFIT)] +trait A { + async fn async_fn_name() -> Result<(), ()> { + Ok(()) + } + fn sync_fn_name() -> Result<(), ()> { + Ok(()) + } +} + +struct Foo; + +#[maybe_async::maybe_async(AFIT)] +impl A for Foo { + async fn async_fn_name() -> Result<(), ()> { + Ok(()) + } + fn sync_fn_name() -> Result<(), ()> { + Ok(()) + } +} + +#[maybe_async::maybe_async] +async fn maybe_async_fn() -> Result<(), ()> { + let a = Foo::async_fn_name().await?; + + let b = Foo::sync_fn_name()?; + Ok(()) +} +``` + +When `maybe-async` feature gate `is_sync` is **NOT** set, the generated code +is async code: + +```rust +// Compiled code when `is_sync` is toggled off. +trait A { + async fn maybe_async_fn_name() -> Result<(), ()> { + Ok(()) + } + fn sync_fn_name() -> Result<(), ()> { + Ok(()) + } +} + +struct Foo; + +impl A for Foo { + async fn maybe_async_fn_name() -> Result<(), ()> { + Ok(()) + } + fn sync_fn_name() -> Result<(), ()> { + Ok(()) + } +} + +async fn maybe_async_fn() -> Result<(), ()> { + let a = Foo::maybe_async_fn_name().await?; + let b = Foo::sync_fn_name()?; + Ok(()) +} +``` + +When `maybe-async` feature gate `is_sync` is set, all async keyword is +ignored and yields a sync version code: + +```rust +// Compiled code when `is_sync` is toggled on. +trait A { + fn maybe_async_fn_name() -> Result<(), ()> { + Ok(()) + } + fn sync_fn_name() -> Result<(), ()> { + Ok(()) + } +} + +struct Foo; + +impl A for Foo { + fn maybe_async_fn_name() -> Result<(), ()> { + Ok(()) + } + fn sync_fn_name() -> Result<(), ()> { + Ok(()) + } +} + +fn maybe_async_fn() -> Result<(), ()> { + let a = Foo::maybe_async_fn_name()?; + let b = Foo::sync_fn_name()?; + Ok(()) +} +``` + +### Examples + +#### 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, 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 +can toggle between a sync AWZ3 client and async one by `is_sync` feature +gate when we add `maybe-async` to dependency. + + +## License +MIT |