Expand description
§maybe-async-cfg2
Don’t repeat yourself when writing blocking and async code.
When implementing both sync and async variants of an API in a crate, the APIs of the two variants are almost the same except for async/await keywords.
maybe-async-cfg2
helps unify async and sync implementations using a procedural macro.
- Write async code with normal
async
andawait
keywords, and letmaybe_async_cfg2
handle removing them when blocking code is needed. - Add
maybe
attributes and specify feature conditions in the macro parameters to determine which variant of code should be generated. - Use
only_if
(orremove_if
) to keep code in a specific variant when necessary.
The maybe
procedural macro can be applied to the following code:
- use declarations
- trait declarations
- trait implementations
- function definitions
- struct and enum definitions
- modules
RECOMMENDATION: Use resolver version 2 in Cargo.toml
, which was introduced in Rust 1.51.
Without it, two crates in a dependency with conflicting versions (one async and another
blocking) can fail compilation.
[package]
resolver = "2"
# or when using workspaces
[workspace]
resolver = "2"
§Motivation
The async/await language feature transformed the async world of Rust. Compared with the map/and_then style, async code now more closely resembles sync code.
In many crates, the async and sync variants share the same API, but the minor difference that all async code must be awaited prevents the unification of async and sync code. In other words, it is necessary to write an async and a sync implementation respectively.
§Macros in Detail
To use maybe-async-cfg2
, it is necessary to distinguish which code is used exclusively in the
sync vs. async variants. These two variants of the implementation should share the same function
signatures except for async/await keywords.
Use the maybe
macro for code that is the same in both async and sync variants.
Specify in the macro parameters the conditions (based on features) under which async and/or sync
variants of the code should appear.
-
attribute macro
maybe
Offers a unified way to provide sync and async conversion on demand depending on enabled feature flags, with an async first policy.
[dependencies] maybe_async_cfg2 = "0.3" [features] use_sync = [] use_async = []
In this and all the following examples, two features are used. Any conditions can be used, for example, replacing
feature="use_sync"
withnot(feature="use_async")
everywhere.maybe-async-cfg2
does not analyze the conditions in any way, just substituting them as is.Add the
maybe
attribute before all items that must be different in sync vs. async code.To keep async code, specify the
async
parameter with the condition (based on features) for when the code should be async.To convert async code to sync, specify the
sync
parameter with the condition when sync code should be generated.#[maybe_async_cfg2::maybe( idents(Foo), sync(feature="use_sync"), async(feature="use_async") )] struct Struct { f: Foo, }
After conversion:
#[cfg(feature="use_sync")] struct StructSync { f: FooSync, } #[cfg(feature="use_async")] struct StructAsync { f: FooAsync, }
-
procedural macro
content
The
content
macro allows specifying common parameters for manymaybe
macros. Use the internaldefault
attribute with the required parameters inside thecontent
macro.maybe_async_cfg2::content!{ #![maybe_async_cfg2::default( idents(Foo, Bar), )] #[maybe_async_cfg2::maybe( sync(feature="use_sync"), async(feature="use_async") )] struct Struct { f: Foo, } #[maybe_async_cfg2::maybe( sync(feature="use_sync"), async(feature="use_async") )] async fn func(b: Bar) { todo!() } } // content!
After conversion:
#[cfg(feature="use_sync")] struct StructSync { f: FooSync, } #[cfg(feature="use_async")] struct StructAsync { f: FooAsync, } #[cfg(feature="use_sync")] fn func_sync(b: BarSync) { todo!() } #[cfg(feature="use_async")] async fn func_async(b: BarAsync) { todo!() }
§Doctests
When writing doctests, they can be marked as applicable only in the corresponding code variant.
To do this, specify only_if(
VARIANT_KEY)
in the doctest attributes. Then in all other
variants, this doctest will be replaced with an empty string.
#[maybe_async_cfg2::maybe(
idents(Foo),
sync(feature = "use_sync"),
async(feature = "use_async")
)]
/// This is a structure.
/// ```rust, only_if(sync)
/// let s = StructSync{ f: FooSync::new() };
/// ```
/// ```rust, only_if(async)
/// let s = StructAsync{ f: FooAsync::new().await };
/// ```
struct Struct {
f: Foo,
}
After conversion:
#[cfg(feature = "use_sync")]
/// This is a structure.
/// ```rust, only_if(sync)
/// let s = StructSync{ f: FooSync::new() };
/// ```
struct StructSync {
f: FooSync,
}
#[cfg(feature = "use_async")]
/// This is a structure.
///
/// ```rust, only_if(async)
/// let s = StructAsync{ f: FooAsync::new().await };
/// ```
struct StructAsync {
f: FooAsync,
}
§Examples
§Rust client for services
When implementing a Rust client for any service, like awz3, the higher-level API of async and sync variants is almost the same, such as creating or deleting a bucket, retrieving an object, etc.
The example service_client
is a proof of concept that maybe_async_cfg2
can actually free us
from writing duplicate code for sync and async variants. The is_sync
feature gate
allows toggling between sync and async AWZ3 client implementations.
§Acknowledgements
This crate is a redesigned fork of these wonderful crates:
Thanks!
§License
MIT
Macros§
- content
- A wrapper for code with common
maybe
parameters
Attribute Macros§
- maybe
- Marks code that can be presented in several variants.
- noop
- Does nothing (leaves content intact).
- only_if
- Marks conditional content that should only be used in the specified variant of code.
- remove
- Removes marked content.
- remove_
if - Marks conditional content that should be used in all variants of code except the specified one.