README.md
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