Skip to main content

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//!        Inside an `AFIT` trait you can write either `async fn foo(&self) -> T` or the desugared form
99//!        `fn foo(&self) -> impl Future<Output = T> + Send + '_`. The latter is recommended for public
100//!        traits to avoid the `async_fn_in_trait` warning ("use of `async fn` in public traits is
101//!        discouraged as auto trait bounds cannot be specified"). In sync mode, `maybe_async` detects
102//!        the `impl Future<Output = T> + ...` return type and rewrites it to plain `-> T`.
103//!
104//!     For compatibility reasons, the `async fn` in traits is supported via a verbose `AFIT` flag. This will become
105//!     the default mode for the next major release.
106//!
107//! - `must_be_async`
108//!
109//!     **Keep async**.
110//!
111//!     There are three usage variants for `must_be_async` attribute usage:
112//!     - `#[must_be_async]` or `#[must_be_async(Send)]`
113//!     - `#[must_be_async(?Send)]`
114//!     - `#[must_be_async(AFIT)]`
115//!
116//! - `must_be_sync`
117//!
118//!     **Convert to sync code**. Convert the async code into sync code by
119//!     removing all `async move`, `async` and `await` keyword
120//!
121//!
122//! - `sync_impl`
123//!
124//!     A sync implementation should compile on blocking implementation and
125//!     must simply disappear when we want async version.
126//!
127//!     Although most of the API are almost the same, there definitely come to a
128//!     point when the async and sync version should differ greatly. For
129//!     example, a MongoDB client may use the same API for async and sync
130//!     version, but the code to actually send reqeust are quite different.
131//!
132//!     Here, we can use `sync_impl` to mark a synchronous implementation, and a
133//!     sync implementation should disappear when we want async version.
134//!
135//! - `async_impl`
136//!
137//!     An async implementation should on compile on async implementation and
138//!     must simply disappear when we want sync version.
139//!
140//!     There are three usage variants for `async_impl` attribute usage:
141//!     - `#[async_impl]` or `#[async_impl(Send)]`
142//!     - `#[async_impl(?Send)]`
143//!     - `#[async_impl(AFIT)]`
144//!
145//! - `test`
146//!
147//!     Handy macro to unify async and sync **unit and e2e test** code.
148//!
149//!     You can specify the condition to compile to sync test code
150//!     and also the conditions to compile to async test code with given test
151//!     macro, e.x. `tokio::test`, `async_std::test`, etc. When only sync
152//!     condition is specified,the test code only compiles when sync condition
153//!     is met.
154//!
155//!     ```rust
156//!     # #[maybe_async::maybe_async]
157//!     # async fn async_fn() -> bool {
158//!     #    true
159//!     # }
160//!
161//!     ##[maybe_async::test(
162//!         feature="is_sync",
163//!         async(
164//!             all(not(feature="is_sync"), feature="async_std"),
165//!             async_std::test
166//!         ),
167//!         async(
168//!             all(not(feature="is_sync"), feature="tokio"),
169//!             tokio::test
170//!         )
171//!     )]
172//!     async fn test_async_fn() {
173//!         let res = async_fn().await;
174//!         assert_eq!(res, true);
175//!     }
176//!     ```
177//!
178//! ## What's Under the Hook
179//!
180//! `maybe-async` compiles your code in different way with the `is_sync` feature
181//! gate. It removes all `await` and `async` keywords in your code under
182//! `maybe_async` macro and conditionally compiles codes under `async_impl` and
183//! `sync_impl`.
184//!
185//! Here is a detailed example on what's going on whe the `is_sync` feature
186//! gate set or not.
187//!
188//! ```rust
189//! #[maybe_async::maybe_async(AFIT)]
190//! trait A {
191//!     async fn async_fn_name() -> Result<(), ()> {
192//!         Ok(())
193//!     }
194//!     fn sync_fn_name() -> Result<(), ()> {
195//!         Ok(())
196//!     }
197//! }
198//!
199//! struct Foo;
200//!
201//! #[maybe_async::maybe_async(AFIT)]
202//! impl A for Foo {
203//!     async fn async_fn_name() -> Result<(), ()> {
204//!         Ok(())
205//!     }
206//!     fn sync_fn_name() -> Result<(), ()> {
207//!         Ok(())
208//!     }
209//! }
210//!
211//! #[maybe_async::maybe_async]
212//! async fn maybe_async_fn() -> Result<(), ()> {
213//!     let a = Foo::async_fn_name().await?;
214//!
215//!     let b = Foo::sync_fn_name()?;
216//!     Ok(())
217//! }
218//! ```
219//!
220//! When `maybe-async` feature gate `is_sync` is **NOT** set, the generated code
221//! is async code:
222//!
223//! ```rust
224//! // Compiled code when `is_sync` is toggled off.
225//! trait A {
226//!     async fn maybe_async_fn_name() -> Result<(), ()> {
227//!         Ok(())
228//!     }
229//!     fn sync_fn_name() -> Result<(), ()> {
230//!         Ok(())
231//!     }
232//! }
233//!
234//! struct Foo;
235//!
236//! impl A for Foo {
237//!     async fn maybe_async_fn_name() -> Result<(), ()> {
238//!         Ok(())
239//!     }
240//!     fn sync_fn_name() -> Result<(), ()> {
241//!         Ok(())
242//!     }
243//! }
244//!
245//! async fn maybe_async_fn() -> Result<(), ()> {
246//!     let a = Foo::maybe_async_fn_name().await?;
247//!     let b = Foo::sync_fn_name()?;
248//!     Ok(())
249//! }
250//! ```
251//!
252//! When `maybe-async` feature gate `is_sync` is set, all async keyword is
253//! ignored and yields a sync version code:
254//!
255//! ```rust
256//! // Compiled code when `is_sync` is toggled on.
257//! trait A {
258//!     fn maybe_async_fn_name() -> Result<(), ()> {
259//!         Ok(())
260//!     }
261//!     fn sync_fn_name() -> Result<(), ()> {
262//!         Ok(())
263//!     }
264//! }
265//!
266//! struct Foo;
267//!
268//! impl A for Foo {
269//!     fn maybe_async_fn_name() -> Result<(), ()> {
270//!         Ok(())
271//!     }
272//!     fn sync_fn_name() -> Result<(), ()> {
273//!         Ok(())
274//!     }
275//! }
276//!
277//! fn maybe_async_fn() -> Result<(), ()> {
278//!     let a = Foo::maybe_async_fn_name()?;
279//!     let b = Foo::sync_fn_name()?;
280//!     Ok(())
281//! }
282//! ```
283//!
284//! ## Examples
285//!
286//! ### rust client for services
287//!
288//! When implementing rust client for any services, like awz3. The higher level
289//! API of async and sync version is almost the same, such as creating or
290//! deleting a bucket, retrieving an object, etc.
291//!
292//! The example `service_client` is a proof of concept that `maybe_async` can
293//! actually free us from writing almost the same code for sync and async. We
294//! can toggle between a sync AWZ3 client and async one by `is_sync` feature
295//! gate when we add `maybe-async` to dependency.
296//!
297//!
298//! # License
299//! MIT
300
301extern crate proc_macro;
302
303use proc_macro::TokenStream;
304
305use proc_macro2::{Span, TokenStream as TokenStream2};
306use syn::{
307    ext::IdentExt,
308    parenthesized,
309    parse::{ParseStream, Parser},
310    parse_macro_input, token, Ident, ImplItem, LitStr, Meta, Result, Token, TraitItem,
311};
312
313use quote::quote;
314
315use crate::{parse::Item, visit::AsyncAwaitRemoval};
316
317mod parse;
318mod visit;
319enum AsyncTraitMode {
320    Send,
321    NotSend,
322    Off,
323}
324
325fn convert_async(input: &mut Item, async_trait_mode: AsyncTraitMode) -> TokenStream2 {
326    match input {
327        Item::Trait(item) => match async_trait_mode {
328            AsyncTraitMode::Send => quote!(#[async_trait::async_trait]#item),
329            AsyncTraitMode::NotSend => quote!(#[async_trait::async_trait(?Send)]#item),
330            AsyncTraitMode::Off => quote!(#item),
331        },
332        Item::Impl(item) => {
333            let async_trait_mode = item
334                .trait_
335                .as_ref()
336                .map_or(AsyncTraitMode::Off, |_| async_trait_mode);
337            match async_trait_mode {
338                AsyncTraitMode::Send => quote!(#[async_trait::async_trait]#item),
339                AsyncTraitMode::NotSend => quote!(#[async_trait::async_trait(?Send)]#item),
340                AsyncTraitMode::Off => quote!(#item),
341            }
342        }
343        Item::Fn(item) => quote!(#item),
344        Item::Static(item) => quote!(#item),
345    }
346}
347
348fn convert_sync(input: &mut Item) -> TokenStream2 {
349    match input {
350        Item::Impl(item) => {
351            for inner in &mut item.items {
352                if let ImplItem::Fn(ref mut method) = inner {
353                    if method.sig.asyncness.is_some() {
354                        method.sig.asyncness = None;
355                    }
356                }
357            }
358            AsyncAwaitRemoval.remove_async_await(quote!(#item))
359        }
360        Item::Trait(item) => {
361            for inner in &mut item.items {
362                if let TraitItem::Fn(ref mut method) = inner {
363                    if method.sig.asyncness.is_some() {
364                        method.sig.asyncness = None;
365                    }
366                }
367            }
368            AsyncAwaitRemoval.remove_async_await(quote!(#item))
369        }
370        Item::Fn(item) => {
371            if item.sig.asyncness.is_some() {
372                item.sig.asyncness = None;
373            }
374            AsyncAwaitRemoval.remove_async_await(quote!(#item))
375        }
376        Item::Static(item) => AsyncAwaitRemoval.remove_async_await(quote!(#item)),
377    }
378}
379
380fn async_mode(arg: &str) -> Result<AsyncTraitMode> {
381    match arg {
382        "" | "Send" => Ok(AsyncTraitMode::Send),
383        "?Send" => Ok(AsyncTraitMode::NotSend),
384        // acronym for Async Function in Trait,
385        // TODO make AFIT as default in future release
386        "AFIT" => Ok(AsyncTraitMode::Off),
387        _ => Err(syn::Error::new(
388            Span::call_site(),
389            "Only accepts `Send`, `?Send` or `AFIT` (native async function in trait)",
390        )),
391    }
392}
393
394/// maybe_async attribute macro
395///
396/// Can be applied to trait item, trait impl, functions and struct impls.
397#[proc_macro_attribute]
398pub fn maybe_async(args: TokenStream, input: TokenStream) -> TokenStream {
399    let mode = match async_mode(args.to_string().replace(" ", "").as_str()) {
400        Ok(m) => m,
401        Err(e) => return e.to_compile_error().into(),
402    };
403    let mut item = parse_macro_input!(input as Item);
404
405    let token = if cfg!(feature = "is_sync") {
406        convert_sync(&mut item)
407    } else {
408        convert_async(&mut item, mode)
409    };
410    token.into()
411}
412
413/// convert marked async code to async code with `async-trait`
414#[proc_macro_attribute]
415pub fn must_be_async(args: TokenStream, input: TokenStream) -> TokenStream {
416    let mode = match async_mode(args.to_string().replace(" ", "").as_str()) {
417        Ok(m) => m,
418        Err(e) => return e.to_compile_error().into(),
419    };
420    let mut item = parse_macro_input!(input as Item);
421    convert_async(&mut item, mode).into()
422}
423
424/// convert marked async code to sync code
425#[proc_macro_attribute]
426pub fn must_be_sync(_args: TokenStream, input: TokenStream) -> TokenStream {
427    let mut item = parse_macro_input!(input as Item);
428    convert_sync(&mut item).into()
429}
430
431/// mark sync implementation
432///
433/// only compiled when `is_sync` feature gate is set.
434/// When `is_sync` is not set, marked code is removed.
435#[proc_macro_attribute]
436pub fn sync_impl(_args: TokenStream, input: TokenStream) -> TokenStream {
437    let input = TokenStream2::from(input);
438    let token = if cfg!(feature = "is_sync") {
439        quote!(#input)
440    } else {
441        quote!()
442    };
443    token.into()
444}
445
446/// mark async implementation
447///
448/// only compiled when `is_sync` feature gate is not set.
449/// When `is_sync` is set, marked code is removed.
450#[proc_macro_attribute]
451pub fn async_impl(args: TokenStream, _input: TokenStream) -> TokenStream {
452    let mode = match async_mode(args.to_string().replace(" ", "").as_str()) {
453        Ok(m) => m,
454        Err(e) => return e.to_compile_error().into(),
455    };
456    let token = if cfg!(feature = "is_sync") {
457        quote!()
458    } else {
459        let mut item = parse_macro_input!(_input as Item);
460        convert_async(&mut item, mode)
461    };
462    token.into()
463}
464
465fn parse_nested_meta_or_str(input: ParseStream) -> Result<TokenStream2> {
466    if let Some(s) = input.parse::<Option<LitStr>>()? {
467        let tokens = s.value().parse()?;
468        Ok(tokens)
469    } else {
470        let meta: Meta = input.parse()?;
471        Ok(quote!(#meta))
472    }
473}
474
475/// Handy macro to unify test code of sync and async code
476///
477/// Since the API of both sync and async code are the same,
478/// with only difference that async functions must be awaited.
479/// So it's tedious to write unit sync and async respectively.
480///
481/// This macro helps unify the sync and async unit test code.
482/// Pass the condition to treat test code as sync as the first
483/// argument. And specify the condition when to treat test code
484/// as async and the lib to run async test, e.x. `async-std::test`,
485/// `tokio::test`, or any valid attribute macro.
486///
487/// **ATTENTION**: do not write await inside a assert macro
488///
489/// - Examples
490///
491/// ```rust
492/// #[maybe_async::maybe_async]
493/// async fn async_fn() -> bool {
494///     true
495/// }
496///
497/// #[maybe_async::test(
498///     // when to treat the test code as sync version
499///     feature="is_sync",
500///     // when to run async test
501///     async(all(not(feature="is_sync"), feature="async_std"), async_std::test),
502///     // you can specify multiple conditions for different async runtime
503///     async(all(not(feature="is_sync"), feature="tokio"), tokio::test)
504/// )]
505/// async fn test_async_fn() {
506///     let res = async_fn().await;
507///     assert_eq!(res, true);
508/// }
509///
510/// // Only run test in sync version
511/// #[maybe_async::test(feature = "is_sync")]
512/// async fn test_sync_fn() {
513///     let res = async_fn().await;
514///     assert_eq!(res, true);
515/// }
516/// ```
517///
518/// The above code is transcripted to the following code:
519///
520/// ```rust
521/// # use maybe_async::{must_be_async, must_be_sync, sync_impl};
522/// # #[maybe_async::maybe_async]
523/// # async fn async_fn() -> bool { true }
524///
525/// // convert to sync version when sync condition is met, keep in async version when corresponding
526/// // condition is met
527/// #[cfg_attr(feature = "is_sync", must_be_sync, test)]
528/// #[cfg_attr(
529///     all(not(feature = "is_sync"), feature = "async_std"),
530///     must_be_async,
531///     async_std::test
532/// )]
533/// #[cfg_attr(
534///     all(not(feature = "is_sync"), feature = "tokio"),
535///     must_be_async,
536///     tokio::test
537/// )]
538/// async fn test_async_fn() {
539///     let res = async_fn().await;
540///     assert_eq!(res, true);
541/// }
542///
543/// // force converted to sync function, and only compile on sync condition
544/// #[cfg(feature = "is_sync")]
545/// #[test]
546/// fn test_sync_fn() {
547///     let res = async_fn();
548///     assert_eq!(res, true);
549/// }
550/// ```
551#[proc_macro_attribute]
552pub fn test(args: TokenStream, input: TokenStream) -> TokenStream {
553    match parse_test_cfg.parse(args) {
554        Ok(test_cfg) => [test_cfg.into(), input].into_iter().collect(),
555        Err(err) => err.to_compile_error().into(),
556    }
557}
558
559fn parse_test_cfg(input: ParseStream) -> Result<TokenStream2> {
560    if input.is_empty() {
561        return Err(syn::Error::new(
562            Span::call_site(),
563            "Arguments cannot be empty, at least specify the condition for sync code",
564        ));
565    }
566
567    // The first attributes indicates sync condition
568    let sync_cond = input.call(parse_nested_meta_or_str)?;
569    let mut ts = quote!(#[cfg_attr(#sync_cond, maybe_async::must_be_sync, test)]);
570
571    // The rest attributes indicates async condition and async test macro
572    // only accepts in the forms of `async(cond, test_macro)`, but `cond` and
573    // `test_macro` can be either meta attributes or string literal
574    let mut async_conditions = Vec::new();
575    while !input.is_empty() {
576        input.parse::<Token![,]>()?;
577        if input.is_empty() {
578            break;
579        }
580
581        if !input.peek(Ident::peek_any) {
582            return Err(
583                input.error("Must be list of metas like: `async(condition, async_test_macro)`")
584            );
585        }
586        let name = input.call(Ident::parse_any)?;
587        if name != "async" {
588            return Err(syn::Error::new(
589                name.span(),
590                format!("Unknown path: `{}`, must be `async`", name),
591            ));
592        }
593
594        if !input.peek(token::Paren) {
595            return Err(
596                input.error("Must be list of metas like: `async(condition, async_test_macro)`")
597            );
598        }
599
600        let nested;
601        parenthesized!(nested in input);
602        let list = nested.parse_terminated(parse_nested_meta_or_str, Token![,])?;
603        let len = list.len();
604        let mut iter = list.into_iter();
605        let (Some(async_cond), Some(async_test), None) = (iter.next(), iter.next(), iter.next())
606        else {
607            let msg = format!(
608                "Must pass two metas or string literals like `async(condition, \
609                 async_test_macro)`, you passed {len} metas.",
610            );
611            return Err(syn::Error::new(name.span(), msg));
612        };
613
614        let attr = quote!(
615            #[cfg_attr(#async_cond, maybe_async::must_be_async, #async_test)]
616        );
617        async_conditions.push(async_cond);
618        ts.extend(attr);
619    }
620
621    Ok(if !async_conditions.is_empty() {
622        quote! {
623            #[cfg(any(#sync_cond, #(#async_conditions),*))]
624            #ts
625        }
626    } else {
627        quote! {
628            #[cfg(#sync_cond)]
629            #ts
630        }
631    })
632}