1# maybe-async 2 3**Why bother writing similar code twice for blocking and async code?** 4 5[](https://github.com/fMeow/maybe-async-rs/actions) 6[](./LICENSE) 7[](https://crates.io/crates/maybe-async) 8[](https://docs.rs/maybe-async) 9 10When implementing both sync and async versions of API in a crate, most API 11of the two version are almost the same except for some async/await keyword. 12 13`maybe-async` help unifying async and sync implementation by **procedural 14macro**. 15- Write async code with normal `async`, `await`, and let `maybe_async` 16 handles 17those `async` and `await` when you need a blocking code. 18- Switch between sync and async by toggling `is_sync` feature gate in 19 `Cargo.toml`. 20- use `must_be_async` and `must_be_sync` to keep code in specified version 21- use `async_impl` and `sync_impl` to only compile code block on specified 22 version 23- A handy macro to unify unit test code is also provided. 24 25These procedural macros can be applied to the following codes: 26- trait item declaration 27- trait implementation 28- function definition 29- struct definition 30 31**RECOMMENDATION**: Enable **resolver ver2** in your crate, which is 32introduced in Rust 1.51. If not, two crates in dependency with conflict 33version (one async and another blocking) can fail compilation. 34 35 36### Motivation 37 38The async/await language feature alters the async world of rust. 39Comparing with the map/and_then style, now the async code really resembles 40sync version code. 41 42In many crates, the async and sync version of crates shares the same API, 43but the minor difference that all async code must be awaited prevent the 44unification of async and sync code. In other words, we are forced to write 45an async and a sync implementation respectively. 46 47### Macros in Detail 48 49`maybe-async` offers 4 set of attribute macros: `maybe_async`, 50`sync_impl`/`async_impl`, `must_be_sync`/`must_be_async`, and `test`. 51 52To use `maybe-async`, we must know which block of codes is only used on 53blocking implementation, and which on async. These two implementation should 54share the same function signatures except for async/await keywords, and use 55`sync_impl` and `async_impl` to mark these implementation. 56 57Use `maybe_async` macro on codes that share the same API on both async and 58blocking code except for async/await keywords. And use feature gate 59`is_sync` in `Cargo.toml` to toggle between async and blocking code. 60 61- `maybe_async` 62 63 Offers a unified feature gate to provide sync and async conversion on 64 demand by feature gate `is_sync`, with **async first** policy. 65 66 Want to keep async code? add `maybe_async` in dependencies with default 67 features, which means `maybe_async` is the same as `must_be_async`: 68 69 ```toml 70 [dependencies] 71 maybe_async = "0.2" 72 ``` 73 74 Want to convert async code to sync? Add `maybe_async` to dependencies with 75 an `is_sync` feature gate. In this way, `maybe_async` is the same as 76 `must_be_sync`: 77 78 ```toml 79 [dependencies] 80 maybe_async = { version = "0.2", features = ["is_sync"] } 81 ``` 82 83 There are three usage variants for `maybe_async` attribute usage: 84 - `#[maybe_async]` or `#[maybe_async(Send)]` 85 86 In this mode, `#[async_trait::async_trait]` is added to trait declarations and trait implementations 87 to support async fn in traits. 88 89 - `#[maybe_async(?Send)]` 90 91 Not all async traits need futures that are `dyn Future + Send`. 92 In this mode, `#[async_trait::async_trait(?Send)]` is added to trait declarations and trait implementations, 93 to avoid having "Send" and "Sync" bounds placed on the async trait 94 methods. 95 96 - `#[maybe_async(AFIT)]` 97 98 AFIT is acronym for **a**sync **f**unction **i**n **t**rait, stabilized from rust 1.74 99 100 For compatibility reasons, the `async fn` in traits is supported via a verbose `AFIT` flag. This will become 101 the default mode for the next major release. 102 103- `must_be_async` 104 105 **Keep async**. 106 107 There are three usage variants for `must_be_async` attribute usage: 108 - `#[must_be_async]` or `#[must_be_async(Send)]` 109 - `#[must_be_async(?Send)]` 110 - `#[must_be_async(AFIT)]` 111 112- `must_be_sync` 113 114 **Convert to sync code**. Convert the async code into sync code by 115 removing all `async move`, `async` and `await` keyword 116 117 118- `sync_impl` 119 120 A sync implementation should compile on blocking implementation and 121 must simply disappear when we want async version. 122 123 Although most of the API are almost the same, there definitely come to a 124 point when the async and sync version should differ greatly. For 125 example, a MongoDB client may use the same API for async and sync 126 version, but the code to actually send reqeust are quite different. 127 128 Here, we can use `sync_impl` to mark a synchronous implementation, and a 129 sync implementation should disappear when we want async version. 130 131- `async_impl` 132 133 An async implementation should on compile on async implementation and 134 must simply disappear when we want sync version. 135 136 There are three usage variants for `async_impl` attribute usage: 137 - `#[async_impl]` or `#[async_impl(Send)]` 138 - `#[async_impl(?Send)]` 139 - `#[async_impl(AFIT)]` 140 141- `test` 142 143 Handy macro to unify async and sync **unit and e2e test** code. 144 145 You can specify the condition to compile to sync test code 146 and also the conditions to compile to async test code with given test 147 macro, e.x. `tokio::test`, `async_std::test`, etc. When only sync 148 condition is specified,the test code only compiles when sync condition 149 is met. 150 151 ```rust 152 # #[maybe_async::maybe_async] 153 # async fn async_fn() -> bool { 154 # true 155 # } 156 157 ##[maybe_async::test( 158 feature="is_sync", 159 async( 160 all(not(feature="is_sync"), feature="async_std"), 161 async_std::test 162 ), 163 async( 164 all(not(feature="is_sync"), feature="tokio"), 165 tokio::test 166 ) 167 )] 168 async fn test_async_fn() { 169 let res = async_fn().await; 170 assert_eq!(res, true); 171 } 172 ``` 173 174### What's Under the Hook 175 176`maybe-async` compiles your code in different way with the `is_sync` feature 177gate. It removes all `await` and `async` keywords in your code under 178`maybe_async` macro and conditionally compiles codes under `async_impl` and 179`sync_impl`. 180 181Here is a detailed example on what's going on whe the `is_sync` feature 182gate set or not. 183 184```rust 185#[maybe_async::maybe_async(AFIT)] 186trait A { 187 async fn async_fn_name() -> Result<(), ()> { 188 Ok(()) 189 } 190 fn sync_fn_name() -> Result<(), ()> { 191 Ok(()) 192 } 193} 194 195struct Foo; 196 197#[maybe_async::maybe_async(AFIT)] 198impl A for Foo { 199 async fn async_fn_name() -> Result<(), ()> { 200 Ok(()) 201 } 202 fn sync_fn_name() -> Result<(), ()> { 203 Ok(()) 204 } 205} 206 207#[maybe_async::maybe_async] 208async fn maybe_async_fn() -> Result<(), ()> { 209 let a = Foo::async_fn_name().await?; 210 211 let b = Foo::sync_fn_name()?; 212 Ok(()) 213} 214``` 215 216When `maybe-async` feature gate `is_sync` is **NOT** set, the generated code 217is async code: 218 219```rust 220// Compiled code when `is_sync` is toggled off. 221trait A { 222 async fn maybe_async_fn_name() -> Result<(), ()> { 223 Ok(()) 224 } 225 fn sync_fn_name() -> Result<(), ()> { 226 Ok(()) 227 } 228} 229 230struct Foo; 231 232impl A for Foo { 233 async fn maybe_async_fn_name() -> Result<(), ()> { 234 Ok(()) 235 } 236 fn sync_fn_name() -> Result<(), ()> { 237 Ok(()) 238 } 239} 240 241async fn maybe_async_fn() -> Result<(), ()> { 242 let a = Foo::maybe_async_fn_name().await?; 243 let b = Foo::sync_fn_name()?; 244 Ok(()) 245} 246``` 247 248When `maybe-async` feature gate `is_sync` is set, all async keyword is 249ignored and yields a sync version code: 250 251```rust 252// Compiled code when `is_sync` is toggled on. 253trait A { 254 fn maybe_async_fn_name() -> Result<(), ()> { 255 Ok(()) 256 } 257 fn sync_fn_name() -> Result<(), ()> { 258 Ok(()) 259 } 260} 261 262struct Foo; 263 264impl A for Foo { 265 fn maybe_async_fn_name() -> Result<(), ()> { 266 Ok(()) 267 } 268 fn sync_fn_name() -> Result<(), ()> { 269 Ok(()) 270 } 271} 272 273fn maybe_async_fn() -> Result<(), ()> { 274 let a = Foo::maybe_async_fn_name()?; 275 let b = Foo::sync_fn_name()?; 276 Ok(()) 277} 278``` 279 280### Examples 281 282#### rust client for services 283 284When implementing rust client for any services, like awz3. The higher level 285API of async and sync version is almost the same, such as creating or 286deleting a bucket, retrieving an object, etc. 287 288The example `service_client` is a proof of concept that `maybe_async` can 289actually free us from writing almost the same code for sync and async. We 290can toggle between a sync AWZ3 client and async one by `is_sync` feature 291gate when we add `maybe-async` to dependency. 292 293 294## License 295MIT 296