1 //! **Why bother writing similar code twice for blocking and async code?**
2 //!
3 //! [![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)
4 //! [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5 //! [![Latest Version](https://img.shields.io/crates/v/maybe-async.svg)](https://crates.io/crates/maybe-async)
6 //! [![maybe-async](https://docs.rs/maybe-async/badge.svg)](https://docs.rs/maybe-async)
7 //!
8 //! When implementing both sync and async versions of API in a crate, most API
9 //! of the two version are almost the same except for some async/await keyword.
10 //!
11 //! `maybe-async` help unifying async and sync implementation by **procedural
12 //! macro**.
13 //! - Write async code with normal `async`, `await`, and let `maybe_async`
14 //!   handles
15 //! those `async` and `await` when you need a blocking code.
16 //! - Switch between sync and async by toggling `is_sync` feature gate in
17 //!   `Cargo.toml`.
18 //! - use `must_be_async` and `must_be_sync` to keep code in specified version
19 //! - use `async_impl` and `sync_impl` to only compile code block on specified
20 //!   version
21 //! - A handy macro to unify unit test code is also provided.
22 //!
23 //! These procedural macros can be applied to the following codes:
24 //! - trait item declaration
25 //! - trait implementation
26 //! - function definition
27 //! - struct definition
28 //!
29 //! **RECOMMENDATION**: Enable **resolver ver2** in your crate, which is
30 //! introduced in Rust 1.51. If not, two crates in dependency with conflict
31 //! version (one async and another blocking) can fail compilation.
32 //!
33 //!
34 //! ## Motivation
35 //!
36 //! The async/await language feature alters the async world of rust.
37 //! Comparing with the map/and_then style, now the async code really resembles
38 //! sync version code.
39 //!
40 //! In many crates, the async and sync version of crates shares the same API,
41 //! but the minor difference that all async code must be awaited prevent the
42 //! unification of async and sync code. In other words, we are forced to write
43 //! an async and a sync implementation respectively.
44 //!
45 //! ## Macros in Detail
46 //!
47 //! `maybe-async` offers 4 set of attribute macros: `maybe_async`,
48 //! `sync_impl`/`async_impl`, `must_be_sync`/`must_be_async`,  and `test`.
49 //!
50 //! To use `maybe-async`, we must know which block of codes is only used on
51 //! blocking implementation, and which on async. These two implementation should
52 //! share the same function signatures except for async/await keywords, and use
53 //! `sync_impl` and `async_impl` to mark these implementation.
54 //!
55 //! Use `maybe_async` macro on codes that share the same API on both async and
56 //! blocking code except for async/await keywords. And use feature gate
57 //! `is_sync` in `Cargo.toml` to toggle between async and blocking code.
58 //!
59 //! - `maybe_async`
60 //!
61 //!     Offers a unified feature gate to provide sync and async conversion on
62 //!     demand by feature gate `is_sync`, with **async first** policy.
63 //!
64 //!     Want to keep async code? add `maybe_async` in dependencies with default
65 //!     features, which means `maybe_async` is the same as `must_be_async`:
66 //!
67 //!     ```toml
68 //!     [dependencies]
69 //!     maybe_async = "0.2"
70 //!     ```
71 //!
72 //!     Want to convert async code to sync? Add `maybe_async` to dependencies with
73 //!     an `is_sync` feature gate. In this way, `maybe_async` is the same as
74 //!     `must_be_sync`:
75 //!
76 //!     ```toml
77 //!     [dependencies]
78 //!     maybe_async = { version = "0.2", features = ["is_sync"] }
79 //!     ```
80 //!
81 //!     There are three usage variants for `maybe_async` attribute usage:
82 //!     - `#[maybe_async]` or `#[maybe_async(Send)]`
83 //!
84 //!        In this mode, `#[async_trait::async_trait]` is added to trait declarations and trait implementations
85 //!        to support async fn in traits.
86 //!
87 //!     - `#[maybe_async(?Send)]`
88 //!
89 //!        Not all async traits need futures that are `dyn Future + Send`.
90 //!        In this mode, `#[async_trait::async_trait(?Send)]` is added to trait declarations and trait implementations,
91 //!        to avoid having "Send" and "Sync" bounds placed on the async trait
92 //!        methods.
93 //!
94 //!     - `#[maybe_async(AFIT)]`
95 //!
96 //!        AFIT is acronym for **a**sync **f**unction **i**n **t**rait, stabilized from rust 1.74
97 //!
98 //!     For compatibility reasons, the `async fn` in traits is supported via a verbose `AFIT` flag. This will become
99 //!     the default mode for the next major release.
100 //!
101 //! - `must_be_async`
102 //!
103 //!     **Keep async**.
104 //!
105 //!     There are three usage variants for `must_be_async` attribute usage:
106 //!     - `#[must_be_async]` or `#[must_be_async(Send)]`
107 //!     - `#[must_be_async(?Send)]`
108 //!     - `#[must_be_async(AFIT)]`
109 //!
110 //! - `must_be_sync`
111 //!
112 //!     **Convert to sync code**. Convert the async code into sync code by
113 //!     removing all `async move`, `async` and `await` keyword
114 //!
115 //!
116 //! - `sync_impl`
117 //!
118 //!     A sync implementation should compile on blocking implementation and
119 //!     must simply disappear when we want async version.
120 //!
121 //!     Although most of the API are almost the same, there definitely come to a
122 //!     point when the async and sync version should differ greatly. For
123 //!     example, a MongoDB client may use the same API for async and sync
124 //!     version, but the code to actually send reqeust are quite different.
125 //!
126 //!     Here, we can use `sync_impl` to mark a synchronous implementation, and a
127 //!     sync implementation should disappear when we want async version.
128 //!
129 //! - `async_impl`
130 //!
131 //!     An async implementation should on compile on async implementation and
132 //!     must simply disappear when we want sync version.
133 //!
134 //!     There are three usage variants for `async_impl` attribute usage:
135 //!     - `#[async_impl]` or `#[async_impl(Send)]`
136 //!     - `#[async_impl(?Send)]`
137 //!     - `#[async_impl(AFIT)]`
138 //!
139 //! - `test`
140 //!
141 //!     Handy macro to unify async and sync **unit and e2e test** code.
142 //!
143 //!     You can specify the condition to compile to sync test code
144 //!     and also the conditions to compile to async test code with given test
145 //!     macro, e.x. `tokio::test`, `async_std::test`, etc. When only sync
146 //!     condition is specified,the test code only compiles when sync condition
147 //!     is met.
148 //!
149 //!     ```rust
150 //!     # #[maybe_async::maybe_async]
151 //!     # async fn async_fn() -> bool {
152 //!     #    true
153 //!     # }
154 //!
155 //!     ##[maybe_async::test(
156 //!         feature="is_sync",
157 //!         async(
158 //!             all(not(feature="is_sync"), feature="async_std"),
159 //!             async_std::test
160 //!         ),
161 //!         async(
162 //!             all(not(feature="is_sync"), feature="tokio"),
163 //!             tokio::test
164 //!         )
165 //!     )]
166 //!     async fn test_async_fn() {
167 //!         let res = async_fn().await;
168 //!         assert_eq!(res, true);
169 //!     }
170 //!     ```
171 //!
172 //! ## What's Under the Hook
173 //!
174 //! `maybe-async` compiles your code in different way with the `is_sync` feature
175 //! gate. It removes all `await` and `async` keywords in your code under
176 //! `maybe_async` macro and conditionally compiles codes under `async_impl` and
177 //! `sync_impl`.
178 //!
179 //! Here is a detailed example on what's going on whe the `is_sync` feature
180 //! gate set or not.
181 //!
182 //! ```rust
183 //! #[maybe_async::maybe_async(AFIT)]
184 //! trait A {
185 //!     async fn async_fn_name() -> Result<(), ()> {
186 //!         Ok(())
187 //!     }
188 //!     fn sync_fn_name() -> Result<(), ()> {
189 //!         Ok(())
190 //!     }
191 //! }
192 //!
193 //! struct Foo;
194 //!
195 //! #[maybe_async::maybe_async(AFIT)]
196 //! impl A for Foo {
197 //!     async fn async_fn_name() -> Result<(), ()> {
198 //!         Ok(())
199 //!     }
200 //!     fn sync_fn_name() -> Result<(), ()> {
201 //!         Ok(())
202 //!     }
203 //! }
204 //!
205 //! #[maybe_async::maybe_async]
206 //! async fn maybe_async_fn() -> Result<(), ()> {
207 //!     let a = Foo::async_fn_name().await?;
208 //!
209 //!     let b = Foo::sync_fn_name()?;
210 //!     Ok(())
211 //! }
212 //! ```
213 //!
214 //! When `maybe-async` feature gate `is_sync` is **NOT** set, the generated code
215 //! is async code:
216 //!
217 //! ```rust
218 //! // Compiled code when `is_sync` is toggled off.
219 //! trait A {
220 //!     async fn maybe_async_fn_name() -> Result<(), ()> {
221 //!         Ok(())
222 //!     }
223 //!     fn sync_fn_name() -> Result<(), ()> {
224 //!         Ok(())
225 //!     }
226 //! }
227 //!
228 //! struct Foo;
229 //!
230 //! impl A for Foo {
231 //!     async fn maybe_async_fn_name() -> Result<(), ()> {
232 //!         Ok(())
233 //!     }
234 //!     fn sync_fn_name() -> Result<(), ()> {
235 //!         Ok(())
236 //!     }
237 //! }
238 //!
239 //! async fn maybe_async_fn() -> Result<(), ()> {
240 //!     let a = Foo::maybe_async_fn_name().await?;
241 //!     let b = Foo::sync_fn_name()?;
242 //!     Ok(())
243 //! }
244 //! ```
245 //!
246 //! When `maybe-async` feature gate `is_sync` is set, all async keyword is
247 //! ignored and yields a sync version code:
248 //!
249 //! ```rust
250 //! // Compiled code when `is_sync` is toggled on.
251 //! trait A {
252 //!     fn maybe_async_fn_name() -> Result<(), ()> {
253 //!         Ok(())
254 //!     }
255 //!     fn sync_fn_name() -> Result<(), ()> {
256 //!         Ok(())
257 //!     }
258 //! }
259 //!
260 //! struct Foo;
261 //!
262 //! impl A for Foo {
263 //!     fn maybe_async_fn_name() -> Result<(), ()> {
264 //!         Ok(())
265 //!     }
266 //!     fn sync_fn_name() -> Result<(), ()> {
267 //!         Ok(())
268 //!     }
269 //! }
270 //!
271 //! fn maybe_async_fn() -> Result<(), ()> {
272 //!     let a = Foo::maybe_async_fn_name()?;
273 //!     let b = Foo::sync_fn_name()?;
274 //!     Ok(())
275 //! }
276 //! ```
277 //!
278 //! ## Examples
279 //!
280 //! ### rust client for services
281 //!
282 //! When implementing rust client for any services, like awz3. The higher level
283 //! API of async and sync version is almost the same, such as creating or
284 //! deleting a bucket, retrieving an object, etc.
285 //!
286 //! The example `service_client` is a proof of concept that `maybe_async` can
287 //! actually free us from writing almost the same code for sync and async. We
288 //! can toggle between a sync AWZ3 client and async one by `is_sync` feature
289 //! gate when we add `maybe-async` to dependency.
290 //!
291 //!
292 //! # License
293 //! MIT
294 
295 extern crate proc_macro;
296 
297 use proc_macro::TokenStream;
298 
299 use proc_macro2::{Span, TokenStream as TokenStream2};
300 use syn::{
301     ext::IdentExt,
302     parenthesized,
303     parse::{ParseStream, Parser},
304     parse_macro_input, token, Ident, ImplItem, LitStr, Meta, Result, Token, TraitItem,
305 };
306 
307 use quote::quote;
308 
309 use crate::{parse::Item, visit::AsyncAwaitRemoval};
310 
311 mod parse;
312 mod visit;
313 enum AsyncTraitMode {
314     Send,
315     NotSend,
316     Off,
317 }
318 
convert_async(input: &mut Item, async_trait_mode: AsyncTraitMode) -> TokenStream2319 fn convert_async(input: &mut Item, async_trait_mode: AsyncTraitMode) -> TokenStream2 {
320     match input {
321         Item::Trait(item) => match async_trait_mode {
322             AsyncTraitMode::Send => quote!(#[async_trait::async_trait]#item),
323             AsyncTraitMode::NotSend => quote!(#[async_trait::async_trait(?Send)]#item),
324             AsyncTraitMode::Off => quote!(#item),
325         },
326         Item::Impl(item) => {
327             let async_trait_mode = item
328                 .trait_
329                 .as_ref()
330                 .map_or(AsyncTraitMode::Off, |_| async_trait_mode);
331             match async_trait_mode {
332                 AsyncTraitMode::Send => quote!(#[async_trait::async_trait]#item),
333                 AsyncTraitMode::NotSend => quote!(#[async_trait::async_trait(?Send)]#item),
334                 AsyncTraitMode::Off => quote!(#item),
335             }
336         }
337         Item::Fn(item) => quote!(#item),
338         Item::Static(item) => quote!(#item),
339     }
340 }
341 
convert_sync(input: &mut Item) -> TokenStream2342 fn convert_sync(input: &mut Item) -> TokenStream2 {
343     match input {
344         Item::Impl(item) => {
345             for inner in &mut item.items {
346                 if let ImplItem::Fn(ref mut method) = inner {
347                     if method.sig.asyncness.is_some() {
348                         method.sig.asyncness = None;
349                     }
350                 }
351             }
352             AsyncAwaitRemoval.remove_async_await(quote!(#item))
353         }
354         Item::Trait(item) => {
355             for inner in &mut item.items {
356                 if let TraitItem::Fn(ref mut method) = inner {
357                     if method.sig.asyncness.is_some() {
358                         method.sig.asyncness = None;
359                     }
360                 }
361             }
362             AsyncAwaitRemoval.remove_async_await(quote!(#item))
363         }
364         Item::Fn(item) => {
365             if item.sig.asyncness.is_some() {
366                 item.sig.asyncness = None;
367             }
368             AsyncAwaitRemoval.remove_async_await(quote!(#item))
369         }
370         Item::Static(item) => AsyncAwaitRemoval.remove_async_await(quote!(#item)),
371     }
372 }
373 
async_mode(arg: &str) -> Result<AsyncTraitMode>374 fn async_mode(arg: &str) -> Result<AsyncTraitMode> {
375     match arg {
376         "" | "Send" => Ok(AsyncTraitMode::Send),
377         "?Send" => Ok(AsyncTraitMode::NotSend),
378         // acronym for Async Function in Trait,
379         // TODO make AFIT as default in future release
380         "AFIT" => Ok(AsyncTraitMode::Off),
381         _ => Err(syn::Error::new(
382             Span::call_site(),
383             "Only accepts `Send`, `?Send` or `AFIT` (native async function in trait)",
384         )),
385     }
386 }
387 
388 /// maybe_async attribute macro
389 ///
390 /// Can be applied to trait item, trait impl, functions and struct impls.
391 #[proc_macro_attribute]
maybe_async(args: TokenStream, input: TokenStream) -> TokenStream392 pub fn maybe_async(args: TokenStream, input: TokenStream) -> TokenStream {
393     let mode = match async_mode(args.to_string().replace(" ", "").as_str()) {
394         Ok(m) => m,
395         Err(e) => return e.to_compile_error().into(),
396     };
397     let mut item = parse_macro_input!(input as Item);
398 
399     let token = if cfg!(feature = "is_sync") {
400         convert_sync(&mut item)
401     } else {
402         convert_async(&mut item, mode)
403     };
404     token.into()
405 }
406 
407 /// convert marked async code to async code with `async-trait`
408 #[proc_macro_attribute]
must_be_async(args: TokenStream, input: TokenStream) -> TokenStream409 pub fn must_be_async(args: TokenStream, input: TokenStream) -> TokenStream {
410     let mode = match async_mode(args.to_string().replace(" ", "").as_str()) {
411         Ok(m) => m,
412         Err(e) => return e.to_compile_error().into(),
413     };
414     let mut item = parse_macro_input!(input as Item);
415     convert_async(&mut item, mode).into()
416 }
417 
418 /// convert marked async code to sync code
419 #[proc_macro_attribute]
must_be_sync(_args: TokenStream, input: TokenStream) -> TokenStream420 pub fn must_be_sync(_args: TokenStream, input: TokenStream) -> TokenStream {
421     let mut item = parse_macro_input!(input as Item);
422     convert_sync(&mut item).into()
423 }
424 
425 /// mark sync implementation
426 ///
427 /// only compiled when `is_sync` feature gate is set.
428 /// When `is_sync` is not set, marked code is removed.
429 #[proc_macro_attribute]
sync_impl(_args: TokenStream, input: TokenStream) -> TokenStream430 pub fn sync_impl(_args: TokenStream, input: TokenStream) -> TokenStream {
431     let input = TokenStream2::from(input);
432     let token = if cfg!(feature = "is_sync") {
433         quote!(#input)
434     } else {
435         quote!()
436     };
437     token.into()
438 }
439 
440 /// mark async implementation
441 ///
442 /// only compiled when `is_sync` feature gate is not set.
443 /// When `is_sync` is set, marked code is removed.
444 #[proc_macro_attribute]
async_impl(args: TokenStream, _input: TokenStream) -> TokenStream445 pub fn async_impl(args: TokenStream, _input: TokenStream) -> TokenStream {
446     let mode = match async_mode(args.to_string().replace(" ", "").as_str()) {
447         Ok(m) => m,
448         Err(e) => return e.to_compile_error().into(),
449     };
450     let token = if cfg!(feature = "is_sync") {
451         quote!()
452     } else {
453         let mut item = parse_macro_input!(_input as Item);
454         convert_async(&mut item, mode)
455     };
456     token.into()
457 }
458 
parse_nested_meta_or_str(input: ParseStream) -> Result<TokenStream2>459 fn parse_nested_meta_or_str(input: ParseStream) -> Result<TokenStream2> {
460     if let Some(s) = input.parse::<Option<LitStr>>()? {
461         let tokens = s.value().parse()?;
462         Ok(tokens)
463     } else {
464         let meta: Meta = input.parse()?;
465         Ok(quote!(#meta))
466     }
467 }
468 
469 /// Handy macro to unify test code of sync and async code
470 ///
471 /// Since the API of both sync and async code are the same,
472 /// with only difference that async functions must be awaited.
473 /// So it's tedious to write unit sync and async respectively.
474 ///
475 /// This macro helps unify the sync and async unit test code.
476 /// Pass the condition to treat test code as sync as the first
477 /// argument. And specify the condition when to treat test code
478 /// as async and the lib to run async test, e.x. `async-std::test`,
479 /// `tokio::test`, or any valid attribute macro.
480 ///
481 /// **ATTENTION**: do not write await inside a assert macro
482 ///
483 /// - Examples
484 ///
485 /// ```rust
486 /// #[maybe_async::maybe_async]
487 /// async fn async_fn() -> bool {
488 ///     true
489 /// }
490 ///
491 /// #[maybe_async::test(
492 ///     // when to treat the test code as sync version
493 ///     feature="is_sync",
494 ///     // when to run async test
495 ///     async(all(not(feature="is_sync"), feature="async_std"), async_std::test),
496 ///     // you can specify multiple conditions for different async runtime
497 ///     async(all(not(feature="is_sync"), feature="tokio"), tokio::test)
498 /// )]
499 /// async fn test_async_fn() {
500 ///     let res = async_fn().await;
501 ///     assert_eq!(res, true);
502 /// }
503 ///
504 /// // Only run test in sync version
505 /// #[maybe_async::test(feature = "is_sync")]
506 /// async fn test_sync_fn() {
507 ///     let res = async_fn().await;
508 ///     assert_eq!(res, true);
509 /// }
510 /// ```
511 ///
512 /// The above code is transcripted to the following code:
513 ///
514 /// ```rust
515 /// # use maybe_async::{must_be_async, must_be_sync, sync_impl};
516 /// # #[maybe_async::maybe_async]
517 /// # async fn async_fn() -> bool { true }
518 ///
519 /// // convert to sync version when sync condition is met, keep in async version when corresponding
520 /// // condition is met
521 /// #[cfg_attr(feature = "is_sync", must_be_sync, test)]
522 /// #[cfg_attr(
523 ///     all(not(feature = "is_sync"), feature = "async_std"),
524 ///     must_be_async,
525 ///     async_std::test
526 /// )]
527 /// #[cfg_attr(
528 ///     all(not(feature = "is_sync"), feature = "tokio"),
529 ///     must_be_async,
530 ///     tokio::test
531 /// )]
532 /// async fn test_async_fn() {
533 ///     let res = async_fn().await;
534 ///     assert_eq!(res, true);
535 /// }
536 ///
537 /// // force converted to sync function, and only compile on sync condition
538 /// #[cfg(feature = "is_sync")]
539 /// #[test]
540 /// fn test_sync_fn() {
541 ///     let res = async_fn();
542 ///     assert_eq!(res, true);
543 /// }
544 /// ```
545 #[proc_macro_attribute]
test(args: TokenStream, input: TokenStream) -> TokenStream546 pub fn test(args: TokenStream, input: TokenStream) -> TokenStream {
547     match parse_test_cfg.parse(args) {
548         Ok(test_cfg) => [test_cfg.into(), input].into_iter().collect(),
549         Err(err) => err.to_compile_error().into(),
550     }
551 }
552 
parse_test_cfg(input: ParseStream) -> Result<TokenStream2>553 fn parse_test_cfg(input: ParseStream) -> Result<TokenStream2> {
554     if input.is_empty() {
555         return Err(syn::Error::new(
556             Span::call_site(),
557             "Arguments cannot be empty, at least specify the condition for sync code",
558         ));
559     }
560 
561     // The first attributes indicates sync condition
562     let sync_cond = input.call(parse_nested_meta_or_str)?;
563     let mut ts = quote!(#[cfg_attr(#sync_cond, maybe_async::must_be_sync, test)]);
564 
565     // The rest attributes indicates async condition and async test macro
566     // only accepts in the forms of `async(cond, test_macro)`, but `cond` and
567     // `test_macro` can be either meta attributes or string literal
568     let mut async_conditions = Vec::new();
569     while !input.is_empty() {
570         input.parse::<Token![,]>()?;
571         if input.is_empty() {
572             break;
573         }
574 
575         if !input.peek(Ident::peek_any) {
576             return Err(
577                 input.error("Must be list of metas like: `async(condition, async_test_macro)`")
578             );
579         }
580         let name = input.call(Ident::parse_any)?;
581         if name != "async" {
582             return Err(syn::Error::new(
583                 name.span(),
584                 format!("Unknown path: `{}`, must be `async`", name),
585             ));
586         }
587 
588         if !input.peek(token::Paren) {
589             return Err(
590                 input.error("Must be list of metas like: `async(condition, async_test_macro)`")
591             );
592         }
593 
594         let nested;
595         parenthesized!(nested in input);
596         let list = nested.parse_terminated(parse_nested_meta_or_str, Token![,])?;
597         let len = list.len();
598         let mut iter = list.into_iter();
599         let (Some(async_cond), Some(async_test), None) = (iter.next(), iter.next(), iter.next())
600         else {
601             let msg = format!(
602                 "Must pass two metas or string literals like `async(condition, \
603                  async_test_macro)`, you passed {len} metas.",
604             );
605             return Err(syn::Error::new(name.span(), msg));
606         };
607 
608         let attr = quote!(
609             #[cfg_attr(#async_cond, maybe_async::must_be_async, #async_test)]
610         );
611         async_conditions.push(async_cond);
612         ts.extend(attr);
613     }
614 
615     Ok(if !async_conditions.is_empty() {
616         quote! {
617             #[cfg(any(#sync_cond, #(#async_conditions),*))]
618             #ts
619         }
620     } else {
621         quote! {
622             #[cfg(#sync_cond)]
623             #ts
624         }
625     })
626 }
627