maybe_async/
lib.rs

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
295extern crate proc_macro;
296
297use proc_macro::TokenStream;
298
299use proc_macro2::{Span, TokenStream as TokenStream2};
300use syn::{
301    ext::IdentExt,
302    parenthesized,
303    parse::{ParseStream, Parser},
304    parse_macro_input, token, Ident, ImplItem, LitStr, Meta, Result, Token, TraitItem,
305};
306
307use quote::quote;
308
309use crate::{parse::Item, visit::AsyncAwaitRemoval};
310
311mod parse;
312mod visit;
313enum AsyncTraitMode {
314    Send,
315    NotSend,
316    Off,
317}
318
319fn 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
342fn 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
374fn 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]
392pub 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]
409pub 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]
420pub 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]
430pub 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]
445pub 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
459fn 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]
546pub 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
553fn 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}