├── .github ├── dependabot.yml └── workflows │ └── linux.yml ├── .gitignore ├── .rustfmt.toml ├── .standard-version └── cargo-updater.js ├── .versionrc ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── README.tpl ├── examples └── service_client.rs ├── src ├── lib.rs ├── parse.rs └── visit.rs └── tests ├── test.rs ├── ui ├── 01-maybe-async.rs ├── 02-must-be-async.rs ├── 03-must-be-sync.rs ├── 04-unit-test-util.rs ├── 05-replace-future-generic-type-with-output.rs ├── 06-sync_impl_async_impl.rs └── test_fail │ ├── 01-empty-test.rs │ ├── 01-empty-test.stderr │ ├── 02-unknown-path.rs │ ├── 02-unknown-path.stderr │ ├── 03-async-gt2.rs │ ├── 03-async-gt2.stderr │ ├── 04-bad-sync-cond.rs │ └── 04-bad-sync-cond.stderr └── unit-test-util.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: tokio 11 | versions: 12 | - 0.2.25 13 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: CI (Linux) 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_and_test: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: dtolnay/rust-toolchain@stable 13 | 14 | - name: rustfmt 15 | run: cargo fmt --all -- --check 16 | 17 | - name: check build (async) 18 | run: cargo check --all --bins --examples --tests 19 | 20 | - name: tests (async) 21 | timeout-minutes: 40 22 | run: cargo test --all --no-fail-fast -- --nocapture 23 | 24 | - name: check build (is_sync) 25 | run: cargo check --features=is_sync --all --bins --examples --tests 26 | 27 | - name: tests (is_sync) 28 | timeout-minutes: 40 29 | run: cargo test --features=is_sync --all --no-fail-fast -- --nocapture 30 | 31 | doc: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: dtolnay/rust-toolchain@stable 36 | 37 | - name: doc (async) 38 | env: 39 | RUSTDOCFLAGS: -Dwarnings 40 | run: cargo doc --all --no-deps 41 | 42 | - name: doc (is_sync) 43 | env: 44 | RUSTDOCFLAGS: -Dwarnings 45 | run: cargo doc --all --no-deps --features=is_sync 46 | 47 | publish: 48 | name: Publish Package 49 | needs: build_and_test 50 | if: startsWith(github.ref, 'refs/tags/v') 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: dtolnay/rust-toolchain@stable 55 | - name: login 56 | env: 57 | SUPER_SECRET: ${{ secrets.CARGO_TOKEN }} 58 | run: cargo login "$SUPER_SECRET" 59 | 60 | - name: publish 61 | run: cargo publish 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | .idea/** 6 | .vscode/** -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | format_code_in_doc_comments = true 3 | normalize_doc_attributes = true 4 | wrap_comments = true 5 | format_strings = true 6 | merge_imports = true -------------------------------------------------------------------------------- /.standard-version/cargo-updater.js: -------------------------------------------------------------------------------- 1 | const TOML = require('@iarna/toml') 2 | 3 | module.exports.readVersion = function (contents) { 4 | let data = TOML.parse(contents); 5 | return data.package.version; 6 | } 7 | 8 | module.exports.writeVersion = function (contents, version) { 9 | let data = TOML.parse(contents); 10 | data.package.version = version; 11 | return TOML.stringify(data); 12 | } 13 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "bumpFiles": [ 3 | { 4 | "filename": "Cargo.toml", 5 | "updater": ".standard-version/cargo-updater.js" 6 | } 7 | ], 8 | "packageFiles": [ 9 | { 10 | "filename": "Cargo.toml", 11 | "updater": ".standard-version/cargo-updater.js" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.10](https://github.com/fMeow/maybe-async-rs/compare/v0.2.9...v0.2.10) (2024-02-22) 6 | 7 | ### [0.2.9](https://github.com/fMeow/maybe-async-rs/compare/v0.2.8...v0.2.9) (2024-01-31) 8 | 9 | 10 | ### Features 11 | 12 | * support `async fn` in traits ([282eb76](https://github.com/fMeow/maybe-async-rs/commit/282eb76c0be0433ade8d0a2a11646e09db2f37b7)) 13 | 14 | ### [0.2.8](https://github.com/fMeow/maybe-async-rs/compare/v0.2.7...v0.2.8) (2024-01-30) 15 | 16 | ### [0.2.7](https://github.com/fMeow/maybe-async-rs/compare/v0.2.6...v0.2.7) (2023-02-01) 17 | 18 | 19 | ### Features 20 | 21 | * allow `maybe_async` on static ([a08b112](https://github.com/fMeow/maybe-async-rs/commit/a08b11218bab0d1db304a4f68e0230c022632168)) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * applying to pub(crate) trait fail ([8cf762f](https://github.com/fMeow/maybe-async-rs/commit/8cf762fdeb1d316716fa01fb2525e5a6f5d25987)) 27 | 28 | ### [0.2.6](https://github.com/guoli-lyu/maybe-async-rs/compare/v0.2.4...v0.2.6) (2021-05-28) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * remove async test if condition not match ([0089daa](https://github.com/guoli-lyu/maybe-async-rs/commit/0089daad6e3419e11d123e8c5c87a1139880027f)) 34 | * test is removed when is_sync ([377815a](https://github.com/guoli-lyu/maybe-async-rs/commit/377815a7a81efc4a0332cc2716a7d603b350ff03)) 35 | 36 | ### [0.2.5](https://github.com/guoli-lyu/maybe-async-rs/compare/v0.2.4...v0.2.5) (2021-05-28) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * remove async test if condition not match ([0c49246](https://github.com/guoli-lyu/maybe-async-rs/commit/0c49246a3245773faff482f6b42d66522d2af208)) 42 | 43 | ### [0.2.4](https://github.com/guoli-lyu/maybe-async-rs/compare/v0.2.3...v0.2.4) (2021-03-28) 44 | 45 | 46 | ### Features 47 | 48 | * replace generic type of Future with Output ([f296cc0](https://github.com/guoli-lyu/maybe-async-rs/commit/f296cc05c90923ae3a3eeea3c5173d06d642c2ab)) 49 | * search trait bound that ends with `Future` ([3508ff2](https://github.com/guoli-lyu/maybe-async-rs/commit/3508ff2987cce61808297aa920c522e0f2012a8a)) 50 | 51 | ### [0.2.3](https://github.com/guoli-lyu/maybe-async-rs/compare/v0.2.2...v0.2.3) (2021-03-27) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * enable full feature gate for syn ([614c085](https://github.com/guoli-lyu/maybe-async-rs/commit/614c085444caf6d0d493422ca20f8ed3b86b7315)) 57 | 58 | ### [0.2.2](https://github.com/guoli-lyu/maybe-async-rs/compare/v0.2.1...v0.2.2) (2020-10-19) 59 | 60 | 61 | ### Features 62 | 63 | * avoid extra parenthesis and braces ([8d146f9](https://github.com/guoli-lyu/maybe-async-rs/commit/8d146f9a9234339de1ef6b9f7ffd44421a8d6c68)) 64 | * remove parenthesis wrap in await ([bc5f460](https://github.com/guoli-lyu/maybe-async-rs/commit/bc5f46078bfb5ccc1599570303aa72a84cc5e2d7)) 65 | * wrap await expr into block instead of paren ([5c4232a](https://github.com/guoli-lyu/maybe-async-rs/commit/5c4232a07035e9c2d4add280cc5b090a7bde471b)) 66 | 67 | ### [0.2.1](https://github.com/guoli-lyu/maybe-async-rs/compare/v0.2.0...v0.2.1) (2020-10-05) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * allow unused_paren when convert to sync ([242ded2](https://github.com/guoli-lyu/maybe-async-rs/commit/242ded2fb9f1cc3c883e0f39a081a555e7a74198)) 73 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maybe-async" 3 | version = "0.2.10" 4 | authors = [ "Guoli Lyu " ] 5 | edition = "2021" 6 | readme = "README.md" 7 | license = "MIT" 8 | description = "A procedure macro to unify SYNC and ASYNC implementation" 9 | repository = "https://github.com/fMeow/maybe-async-rs" 10 | documentation = "https://docs.rs/maybe-async" 11 | keywords = [ "maybe", "async", "futures", "macros", "proc_macro" ] 12 | 13 | [dependencies] 14 | proc-macro2 = "1.0" 15 | quote = "1.0" 16 | 17 | [dependencies.syn] 18 | version = "2.0" 19 | features = [ "visit-mut", "full" ] 20 | 21 | [lib] 22 | proc-macro = true 23 | path = "src/lib.rs" 24 | 25 | [badges.maintenance] 26 | status = "actively-developed" 27 | 28 | [dev-dependencies] 29 | async-trait = "0.1" 30 | 31 | [dev-dependencies.trybuild] 32 | version = "1" 33 | features = [ "diff" ] 34 | 35 | [dev-dependencies.async-std] 36 | version = "1" 37 | features = [ "attributes" ] 38 | 39 | [dev-dependencies.tokio] 40 | version = "1" 41 | features = [ "macros", "rt-multi-thread" ] 42 | 43 | [features] 44 | default = [ ] 45 | is_sync = [ ] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Guoli Lyu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # maybe-async 2 | 3 | **Why bother writing similar code twice for blocking and async code?** 4 | 5 | [![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) 6 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 7 | [![Latest Version](https://img.shields.io/crates/v/maybe-async.svg)](https://crates.io/crates/maybe-async) 8 | [![maybe-async](https://docs.rs/maybe-async/badge.svg)](https://docs.rs/maybe-async) 9 | 10 | When implementing both sync and async versions of API in a crate, most API 11 | of 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 14 | macro**. 15 | - Write async code with normal `async`, `await`, and let `maybe_async` 16 | handles 17 | those `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 | 25 | These 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 32 | introduced in Rust 1.51. If not, two crates in dependency with conflict 33 | version (one async and another blocking) can fail compilation. 34 | 35 | 36 | ### Motivation 37 | 38 | The async/await language feature alters the async world of rust. 39 | Comparing with the map/and_then style, now the async code really resembles 40 | sync version code. 41 | 42 | In many crates, the async and sync version of crates shares the same API, 43 | but the minor difference that all async code must be awaited prevent the 44 | unification of async and sync code. In other words, we are forced to write 45 | an 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 | 52 | To use `maybe-async`, we must know which block of codes is only used on 53 | blocking implementation, and which on async. These two implementation should 54 | share the same function signatures except for async/await keywords, and use 55 | `sync_impl` and `async_impl` to mark these implementation. 56 | 57 | Use `maybe_async` macro on codes that share the same API on both async and 58 | blocking 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 177 | gate. 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 | 181 | Here is a detailed example on what's going on whe the `is_sync` feature 182 | gate set or not. 183 | 184 | ```rust 185 | #[maybe_async::maybe_async(AFIT)] 186 | trait A { 187 | async fn async_fn_name() -> Result<(), ()> { 188 | Ok(()) 189 | } 190 | fn sync_fn_name() -> Result<(), ()> { 191 | Ok(()) 192 | } 193 | } 194 | 195 | struct Foo; 196 | 197 | #[maybe_async::maybe_async(AFIT)] 198 | impl 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] 208 | async 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 | 216 | When `maybe-async` feature gate `is_sync` is **NOT** set, the generated code 217 | is async code: 218 | 219 | ```rust 220 | // Compiled code when `is_sync` is toggled off. 221 | trait A { 222 | async fn maybe_async_fn_name() -> Result<(), ()> { 223 | Ok(()) 224 | } 225 | fn sync_fn_name() -> Result<(), ()> { 226 | Ok(()) 227 | } 228 | } 229 | 230 | struct Foo; 231 | 232 | impl 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 | 241 | async 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 | 248 | When `maybe-async` feature gate `is_sync` is set, all async keyword is 249 | ignored and yields a sync version code: 250 | 251 | ```rust 252 | // Compiled code when `is_sync` is toggled on. 253 | trait A { 254 | fn maybe_async_fn_name() -> Result<(), ()> { 255 | Ok(()) 256 | } 257 | fn sync_fn_name() -> Result<(), ()> { 258 | Ok(()) 259 | } 260 | } 261 | 262 | struct Foo; 263 | 264 | impl A for Foo { 265 | fn maybe_async_fn_name() -> Result<(), ()> { 266 | Ok(()) 267 | } 268 | fn sync_fn_name() -> Result<(), ()> { 269 | Ok(()) 270 | } 271 | } 272 | 273 | fn 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 | 284 | When implementing rust client for any services, like awz3. The higher level 285 | API of async and sync version is almost the same, such as creating or 286 | deleting a bucket, retrieving an object, etc. 287 | 288 | The example `service_client` is a proof of concept that `maybe_async` can 289 | actually free us from writing almost the same code for sync and async. We 290 | can toggle between a sync AWZ3 client and async one by `is_sync` feature 291 | gate when we add `maybe-async` to dependency. 292 | 293 | 294 | ## License 295 | MIT 296 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | {{readme}} 4 | -------------------------------------------------------------------------------- /examples/service_client.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_variables)] 2 | /// To use `maybe-async`, we must know which block of codes is only used on 3 | /// blocking implementation, and which on async. These two implementation should 4 | /// share the same API except for async/await keywords, and use `sync_impl` and 5 | /// `async_impl` to mark these implementation. 6 | type Response = String; 7 | type Url = &'static str; 8 | type Method = String; 9 | 10 | /// InnerClient are used to actually send request, 11 | /// which differ a lot between sync and async. 12 | /// 13 | /// Use native async function in trait 14 | #[maybe_async::maybe_async(AFIT)] 15 | trait InnerClient { 16 | async fn request(method: Method, url: Url, data: String) -> Response; 17 | #[inline] 18 | async fn post(url: Url, data: String) -> Response { 19 | Self::request(String::from("post"), url, data).await 20 | } 21 | #[inline] 22 | async fn delete(url: Url, data: String) -> Response { 23 | Self::request(String::from("delete"), url, data).await 24 | } 25 | } 26 | 27 | /// The higher level API for end user. 28 | pub struct ServiceClient; 29 | 30 | /// Synchronous implementation, only compiles when `is_sync` feature is off. 31 | /// Else the compiler will complain that *request is defined multiple times* and 32 | /// blabla. 33 | #[maybe_async::sync_impl] 34 | impl InnerClient for ServiceClient { 35 | fn request(method: Method, url: Url, data: String) -> Response { 36 | // your implementation for sync, like use 37 | // `reqwest::blocking` to send request 38 | String::from("pretend we have a response") 39 | } 40 | } 41 | 42 | /// Asynchronous implementation, only compiles when `is_sync` feature is off. 43 | #[maybe_async::async_impl(AFIT)] 44 | impl InnerClient for ServiceClient { 45 | async fn request(method: Method, url: Url, data: String) -> Response { 46 | // your implementation for async, like use `reqwest::client` 47 | // or `async_std` to send request 48 | String::from("pretend we have a response") 49 | } 50 | } 51 | 52 | /// Code of upstream API are almost the same for sync and async, 53 | /// except for async/await keyword. 54 | impl ServiceClient { 55 | #[maybe_async::maybe_async] 56 | async fn create_bucket(name: String) -> Response { 57 | Self::post("http://correct_url4create", String::from("my_bucket")).await 58 | // When `is_sync` is toggle on, this block will compiles to: 59 | // Self::post("http://correct_url4create", String::from("my_bucket")) 60 | } 61 | #[maybe_async::maybe_async] 62 | async fn delete_bucket(name: String) -> Response { 63 | Self::delete("http://correct_url4delete", String::from("my_bucket")).await 64 | } 65 | // and another thousands of functions that interact with service side 66 | } 67 | 68 | #[maybe_async::sync_impl] 69 | fn main() { 70 | let _ = ServiceClient::create_bucket("bucket".to_owned()); 71 | } 72 | 73 | #[maybe_async::async_impl] 74 | #[tokio::main] 75 | async fn main() { 76 | let _ = ServiceClient::create_bucket("bucket".to_owned()).await; 77 | } 78 | -------------------------------------------------------------------------------- /src/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 | 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 | 319 | 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 | 342 | 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 | 374 | fn async_mode(arg: &str) -> Result { 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] 392 | 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] 409 | 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] 420 | 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] 430 | 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] 445 | 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 | 459 | fn parse_nested_meta_or_str(input: ParseStream) -> Result { 460 | if let Some(s) = input.parse::>()? { 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] 546 | 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 | 553 | fn parse_test_cfg(input: ParseStream) -> Result { 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::()?; 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 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::{ 3 | parse::{discouraged::Speculative, Parse, ParseStream, Result}, 4 | Attribute, Error, ItemFn, ItemImpl, ItemStatic, ItemTrait, 5 | }; 6 | 7 | pub enum Item { 8 | Trait(ItemTrait), 9 | Impl(ItemImpl), 10 | Fn(ItemFn), 11 | Static(ItemStatic), 12 | } 13 | 14 | macro_rules! fork { 15 | ($fork:ident = $input:ident) => {{ 16 | $fork = $input.fork(); 17 | &$fork 18 | }}; 19 | } 20 | 21 | impl Parse for Item { 22 | fn parse(input: ParseStream) -> Result { 23 | let attrs = input.call(Attribute::parse_outer)?; 24 | let mut fork; 25 | let item = if let Ok(mut item) = fork!(fork = input).parse::() { 26 | item.attrs = attrs; 27 | Item::Impl(item) 28 | } else if let Ok(mut item) = fork!(fork = input).parse::() { 29 | item.attrs = attrs; 30 | Item::Trait(item) 31 | } else if let Ok(mut item) = fork!(fork = input).parse::() { 32 | item.attrs = attrs; 33 | Item::Fn(item) 34 | } else if let Ok(mut item) = fork!(fork = input).parse::() { 35 | item.attrs = attrs; 36 | Item::Static(item) 37 | } else { 38 | return Err(Error::new(Span::call_site(), "expected impl, trait or fn")); 39 | }; 40 | input.advance_to(&fork); 41 | Ok(item) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/visit.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | visit_mut::{self, visit_item_mut, visit_path_segment_mut, VisitMut}, 5 | Expr, ExprBlock, File, GenericArgument, GenericParam, Item, PathArguments, PathSegment, Stmt, 6 | Type, TypeParamBound, WherePredicate, 7 | }; 8 | 9 | pub struct ReplaceGenericType<'a> { 10 | generic_type: &'a str, 11 | arg_type: &'a PathSegment, 12 | } 13 | 14 | impl<'a> ReplaceGenericType<'a> { 15 | pub fn new(generic_type: &'a str, arg_type: &'a PathSegment) -> Self { 16 | Self { 17 | generic_type, 18 | arg_type, 19 | } 20 | } 21 | 22 | pub fn replace_generic_type(item: &mut Item, generic_type: &'a str, arg_type: &'a PathSegment) { 23 | let mut s = Self::new(generic_type, arg_type); 24 | s.visit_item_mut(item); 25 | } 26 | } 27 | 28 | impl<'a> VisitMut for ReplaceGenericType<'a> { 29 | fn visit_item_mut(&mut self, i: &mut Item) { 30 | if let Item::Fn(item_fn) = i { 31 | // remove generic type from generics 32 | let args = item_fn 33 | .sig 34 | .generics 35 | .params 36 | .iter() 37 | .filter_map(|param| { 38 | if let GenericParam::Type(type_param) = ¶m { 39 | if type_param.ident.to_string().eq(self.generic_type) { 40 | None 41 | } else { 42 | Some(param) 43 | } 44 | } else { 45 | Some(param) 46 | } 47 | }) 48 | .collect::>(); 49 | item_fn.sig.generics.params = args.into_iter().cloned().collect(); 50 | 51 | // remove generic type from where clause 52 | if let Some(where_clause) = &mut item_fn.sig.generics.where_clause { 53 | let new_where_clause = where_clause 54 | .predicates 55 | .iter() 56 | .filter_map(|predicate| { 57 | if let WherePredicate::Type(predicate_type) = predicate { 58 | if let Type::Path(p) = &predicate_type.bounded_ty { 59 | if p.path.segments[0].ident.to_string().eq(self.generic_type) { 60 | None 61 | } else { 62 | Some(predicate) 63 | } 64 | } else { 65 | Some(predicate) 66 | } 67 | } else { 68 | Some(predicate) 69 | } 70 | }) 71 | .collect::>(); 72 | 73 | where_clause.predicates = new_where_clause.into_iter().cloned().collect(); 74 | }; 75 | } 76 | visit_item_mut(self, i) 77 | } 78 | fn visit_path_segment_mut(&mut self, i: &mut PathSegment) { 79 | // replace generic type with target type 80 | if i.ident.to_string().eq(&self.generic_type) { 81 | *i = self.arg_type.clone(); 82 | } 83 | visit_path_segment_mut(self, i); 84 | } 85 | } 86 | 87 | pub struct AsyncAwaitRemoval; 88 | 89 | impl AsyncAwaitRemoval { 90 | pub fn remove_async_await(&mut self, item: TokenStream) -> TokenStream { 91 | let mut syntax_tree: File = syn::parse(item.into()).unwrap(); 92 | self.visit_file_mut(&mut syntax_tree); 93 | quote!(#syntax_tree) 94 | } 95 | } 96 | 97 | impl VisitMut for AsyncAwaitRemoval { 98 | fn visit_expr_mut(&mut self, node: &mut Expr) { 99 | // Delegate to the default impl to visit nested expressions. 100 | visit_mut::visit_expr_mut(self, node); 101 | 102 | match node { 103 | Expr::Await(expr) => *node = (*expr.base).clone(), 104 | 105 | Expr::Async(expr) => { 106 | let inner = &expr.block; 107 | let sync_expr = if let [Stmt::Expr(expr, None)] = inner.stmts.as_slice() { 108 | // remove useless braces when there is only one statement 109 | expr.clone() 110 | } else { 111 | Expr::Block(ExprBlock { 112 | attrs: expr.attrs.clone(), 113 | block: inner.clone(), 114 | label: None, 115 | }) 116 | }; 117 | *node = sync_expr; 118 | } 119 | _ => {} 120 | } 121 | } 122 | 123 | fn visit_item_mut(&mut self, i: &mut Item) { 124 | // find generic parameter of Future and replace it with its Output type 125 | if let Item::Fn(item_fn) = i { 126 | let mut inputs: Vec<(String, PathSegment)> = vec![]; 127 | 128 | // generic params: , F> 129 | for param in &item_fn.sig.generics.params { 130 | // generic param: T:Future 131 | if let GenericParam::Type(type_param) = param { 132 | let generic_type_name = type_param.ident.to_string(); 133 | 134 | // bound: Future 135 | for bound in &type_param.bounds { 136 | inputs.extend(search_trait_bound(&generic_type_name, bound)); 137 | } 138 | } 139 | } 140 | 141 | if let Some(where_clause) = &item_fn.sig.generics.where_clause { 142 | for predicate in &where_clause.predicates { 143 | if let WherePredicate::Type(predicate_type) = predicate { 144 | let generic_type_name = if let Type::Path(p) = &predicate_type.bounded_ty { 145 | p.path.segments[0].ident.to_string() 146 | } else { 147 | panic!("Please submit an issue"); 148 | }; 149 | 150 | for bound in &predicate_type.bounds { 151 | inputs.extend(search_trait_bound(&generic_type_name, bound)); 152 | } 153 | } 154 | } 155 | } 156 | 157 | for (generic_type_name, path_seg) in &inputs { 158 | ReplaceGenericType::replace_generic_type(i, generic_type_name, path_seg); 159 | } 160 | } 161 | visit_item_mut(self, i); 162 | } 163 | } 164 | 165 | fn search_trait_bound( 166 | generic_type_name: &str, 167 | bound: &TypeParamBound, 168 | ) -> Vec<(String, PathSegment)> { 169 | let mut inputs = vec![]; 170 | 171 | if let TypeParamBound::Trait(trait_bound) = bound { 172 | let segment = &trait_bound.path.segments[trait_bound.path.segments.len() - 1]; 173 | let name = segment.ident.to_string(); 174 | if name.eq("Future") { 175 | // match Future 176 | if let PathArguments::AngleBracketed(args) = &segment.arguments { 177 | // binding: Output=Type 178 | if let GenericArgument::AssocType(binding) = &args.args[0] { 179 | if let Type::Path(p) = &binding.ty { 180 | inputs.push((generic_type_name.to_owned(), p.path.segments[0].clone())); 181 | } 182 | } 183 | } 184 | } 185 | } 186 | inputs 187 | } 188 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn ui() { 3 | let t = trybuild::TestCases::new(); 4 | t.pass("tests/ui/01-maybe-async.rs"); 5 | t.pass("tests/ui/02-must-be-async.rs"); 6 | t.pass("tests/ui/03-must-be-sync.rs"); 7 | t.pass("tests/ui/04-unit-test-util.rs"); 8 | t.pass("tests/ui/05-replace-future-generic-type-with-output.rs"); 9 | t.pass("tests/ui/06-sync_impl_async_impl.rs"); 10 | 11 | t.compile_fail("tests/ui/test_fail/01-empty-test.rs"); 12 | t.compile_fail("tests/ui/test_fail/02-unknown-path.rs"); 13 | t.compile_fail("tests/ui/test_fail/03-async-gt2.rs"); 14 | t.compile_fail("tests/ui/test_fail/04-bad-sync-cond.rs"); 15 | } 16 | -------------------------------------------------------------------------------- /tests/ui/01-maybe-async.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use maybe_async::maybe_async; 4 | 5 | #[maybe_async(Send)] 6 | trait Trait { 7 | fn sync_fn() {} 8 | 9 | async fn declare_async(&self); 10 | 11 | async fn async_fn(&self) { 12 | self.declare_async().await 13 | } 14 | } 15 | 16 | #[maybe_async(?Send)] 17 | pub trait PubTrait { 18 | fn sync_fn() {} 19 | 20 | async fn declare_async(&self); 21 | 22 | async fn async_fn(&self) { 23 | self.declare_async().await 24 | } 25 | } 26 | 27 | #[maybe_async] 28 | pub(crate) trait PubCrateTrait { 29 | fn sync_fn() {} 30 | 31 | async fn declare_async(&self); 32 | 33 | async fn async_fn(&self) { 34 | self.declare_async().await 35 | } 36 | } 37 | 38 | #[maybe_async(AFIT)] 39 | trait AfitTrait { 40 | fn sync_fn_afit() {} 41 | 42 | async fn declare_async_afit(&self); 43 | 44 | async fn async_fn_afit(&self) { 45 | self.declare_async_afit().await 46 | } 47 | } 48 | 49 | #[maybe_async] 50 | async fn async_fn() {} 51 | 52 | #[maybe_async] 53 | pub async fn pub_async_fn() {} 54 | 55 | #[maybe_async] 56 | pub(crate) async fn pub_crate_async_fn() {} 57 | 58 | #[maybe_async] 59 | unsafe fn unsafe_fn() {} 60 | 61 | struct Struct; 62 | 63 | #[maybe_async] 64 | impl Struct { 65 | fn sync_fn_inherent() {} 66 | 67 | async fn declare_async_inherent(&self) {} 68 | 69 | async fn async_fn_inherent(&self) { 70 | async { self.declare_async_inherent().await }.await 71 | } 72 | } 73 | 74 | #[maybe_async] 75 | impl Trait for Struct { 76 | fn sync_fn() {} 77 | 78 | async fn declare_async(&self) {} 79 | 80 | async fn async_fn(&self) { 81 | async { self.declare_async().await }.await 82 | } 83 | } 84 | 85 | #[maybe_async(AFIT)] 86 | impl AfitTrait for Struct { 87 | fn sync_fn_afit() {} 88 | 89 | async fn declare_async_afit(&self) {} 90 | 91 | async fn async_fn_afit(&self) { 92 | async { self.declare_async_afit().await }.await 93 | } 94 | } 95 | 96 | #[cfg(feature = "is_sync")] 97 | fn main() -> std::result::Result<(), ()> { 98 | let s = Struct; 99 | s.declare_async_inherent(); 100 | s.async_fn_inherent(); 101 | s.declare_async(); 102 | s.async_fn(); 103 | s.declare_async_afit(); 104 | s.async_fn_afit(); 105 | async_fn(); 106 | pub_async_fn(); 107 | Ok(()) 108 | } 109 | 110 | #[cfg(not(feature = "is_sync"))] 111 | #[async_std::main] 112 | async fn main() { 113 | let s = Struct; 114 | s.declare_async_inherent().await; 115 | s.async_fn_inherent().await; 116 | s.declare_async().await; 117 | s.async_fn().await; 118 | s.declare_async_afit().await; 119 | s.async_fn_afit().await; 120 | async_fn().await; 121 | pub_async_fn().await; 122 | } 123 | -------------------------------------------------------------------------------- /tests/ui/02-must-be-async.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[maybe_async::maybe_async] 4 | trait Trait { 5 | fn sync_fn() {} 6 | 7 | async fn declare_async(&self); 8 | 9 | async fn async_fn(&self) { 10 | self.declare_async().await 11 | } 12 | } 13 | 14 | #[maybe_async::maybe_async(?Send)] 15 | trait NotSendTrait { 16 | async fn declare_async_not_send(&self); 17 | 18 | async fn async_fn_not_send(&self) { 19 | self.declare_async_not_send().await 20 | } 21 | } 22 | 23 | #[maybe_async::maybe_async] 24 | pub trait PubTrait { 25 | fn sync_fn() {} 26 | 27 | async fn declare_async(&self); 28 | 29 | async fn async_fn(&self) { 30 | self.declare_async().await 31 | } 32 | } 33 | 34 | #[maybe_async::maybe_async] 35 | pub(crate) trait PubCrateTrait { 36 | fn sync_fn() {} 37 | 38 | async fn declare_async(&self); 39 | 40 | async fn async_fn(&self) { 41 | self.declare_async().await 42 | } 43 | } 44 | 45 | #[maybe_async::maybe_async(AFIT)] 46 | trait AfitTrait { 47 | fn sync_fn_afit() {} 48 | 49 | async fn declare_async_afit(&self); 50 | 51 | async fn async_fn_afit(&self) { 52 | self.declare_async_afit().await 53 | } 54 | } 55 | 56 | #[cfg(not(feature = "is_sync"))] 57 | #[maybe_async::must_be_async] 58 | async fn async_fn() {} 59 | 60 | #[cfg(not(feature = "is_sync"))] 61 | #[maybe_async::must_be_async] 62 | pub async fn pub_async_fn() {} 63 | 64 | #[cfg(not(feature = "is_sync"))] 65 | #[maybe_async::maybe_async] 66 | pub(crate) async fn pub_crate_async_fn() {} 67 | 68 | #[cfg(not(feature = "is_sync"))] 69 | #[maybe_async::maybe_async] 70 | unsafe fn unsafe_fn() {} 71 | 72 | struct Struct; 73 | 74 | #[cfg(not(feature = "is_sync"))] 75 | #[maybe_async::must_be_async] 76 | impl Struct { 77 | fn sync_fn_inherent() {} 78 | 79 | async fn declare_async_inherent(&self) {} 80 | 81 | async fn async_fn_inherent(&self) { 82 | async { self.declare_async_inherent().await }.await 83 | } 84 | } 85 | 86 | #[cfg(not(feature = "is_sync"))] 87 | #[maybe_async::must_be_async] 88 | impl Trait for Struct { 89 | fn sync_fn() {} 90 | 91 | async fn declare_async(&self) {} 92 | 93 | async fn async_fn(&self) { 94 | async { self.declare_async().await }.await 95 | } 96 | } 97 | 98 | #[cfg(not(feature = "is_sync"))] 99 | #[maybe_async::must_be_async(?Send)] 100 | impl NotSendTrait for Struct { 101 | async fn declare_async_not_send(&self) {} 102 | 103 | async fn async_fn_not_send(&self) { 104 | async { self.declare_async_not_send().await }.await 105 | } 106 | } 107 | #[cfg(not(feature = "is_sync"))] 108 | #[maybe_async::must_be_async(AFIT)] 109 | impl AfitTrait for Struct { 110 | fn sync_fn_afit() {} 111 | 112 | async fn declare_async_afit(&self) {} 113 | 114 | async fn async_fn_afit(&self) { 115 | async { self.declare_async_afit().await }.await 116 | } 117 | } 118 | 119 | #[cfg(feature = "is_sync")] 120 | fn main() {} 121 | 122 | #[cfg(not(feature = "is_sync"))] 123 | #[async_std::main] 124 | async fn main() { 125 | let s = Struct; 126 | s.declare_async_inherent().await; 127 | s.async_fn_inherent().await; 128 | s.declare_async().await; 129 | s.async_fn().await; 130 | s.declare_async_afit().await; 131 | s.async_fn_afit().await; 132 | async_fn().await; 133 | pub_async_fn().await; 134 | } 135 | -------------------------------------------------------------------------------- /tests/ui/03-must-be-sync.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[maybe_async::maybe_async] 4 | trait Trait { 5 | fn sync_fn() {} 6 | 7 | async fn declare_async(&self); 8 | 9 | async fn async_fn(&self) { 10 | self.declare_async().await 11 | } 12 | } 13 | 14 | #[maybe_async::maybe_async] 15 | pub trait PubTrait { 16 | fn sync_fn() {} 17 | 18 | async fn declare_async(&self); 19 | 20 | async fn async_fn(&self) { 21 | self.declare_async().await 22 | } 23 | } 24 | 25 | #[maybe_async::maybe_async] 26 | pub(crate) trait PubCrateTrait { 27 | fn sync_fn() {} 28 | 29 | async fn declare_async(&self); 30 | 31 | async fn async_fn(&self) { 32 | self.declare_async().await 33 | } 34 | } 35 | 36 | #[maybe_async::maybe_async] 37 | async fn async_fn() {} 38 | 39 | #[maybe_async::maybe_async] 40 | pub async fn pub_async_fn() {} 41 | 42 | #[maybe_async::maybe_async] 43 | pub(crate) async fn pub_crate_async_fn() {} 44 | 45 | #[maybe_async::maybe_async] 46 | unsafe fn unsafe_fn() {} 47 | 48 | struct Struct; 49 | 50 | #[cfg(feature = "is_sync")] 51 | #[maybe_async::must_be_sync] 52 | impl Struct { 53 | fn sync_fn_inherent() {} 54 | 55 | async fn declare_async_inherent(&self) {} 56 | 57 | async fn async_fn_inherent(&self) { 58 | async { self.declare_async_inherent().await }.await 59 | } 60 | } 61 | 62 | #[cfg(feature = "is_sync")] 63 | #[maybe_async::must_be_sync] 64 | impl Trait for Struct { 65 | fn sync_fn() {} 66 | 67 | async fn declare_async(&self) {} 68 | 69 | async fn async_fn(&self) { 70 | async { self.declare_async().await }.await 71 | } 72 | } 73 | 74 | #[cfg(feature = "is_sync")] 75 | fn main() -> std::result::Result<(), ()> { 76 | let s = Struct; 77 | s.declare_async_inherent(); 78 | s.async_fn_inherent(); 79 | s.declare_async(); 80 | s.async_fn(); 81 | async_fn(); 82 | pub_async_fn(); 83 | Ok(()) 84 | } 85 | 86 | #[cfg(not(feature = "is_sync"))] 87 | #[async_std::main] 88 | async fn main() {} 89 | -------------------------------------------------------------------------------- /tests/ui/04-unit-test-util.rs: -------------------------------------------------------------------------------- 1 | use maybe_async::maybe_async; 2 | 3 | #[maybe_async] 4 | async fn async_fn() -> bool { 5 | true 6 | } 7 | 8 | #[maybe_async::test( 9 | feature = "is_sync", 10 | async(all(not(feature="is_sync"), feature = "async_std"), async_std::test), 11 | async(all(not(feature="is_sync"), feature = "tokio"), tokio::test) 12 | )] 13 | async fn test_async_fn() { 14 | let res = async_fn().await; 15 | assert_eq!(res, true); 16 | } 17 | 18 | #[maybe_async::test(feature = "is_sync", async(not(feature = "is_sync"), async_std::test))] 19 | async fn test_async_fn2() { 20 | let res = async_fn().await; 21 | assert_eq!(res, true); 22 | } 23 | 24 | #[maybe_async::test("feature=\"is_sync\"", async(not(feature = "is_sync"), async_std::test))] 25 | async fn test_async_fn3() { 26 | let res = async_fn().await; 27 | assert_eq!(res, true); 28 | } 29 | 30 | #[maybe_async::test(feature = "is_sync", async("not(feature = \"is_sync\")", "async_std::test"))] 31 | async fn test_async_fn4() { 32 | let res = async_fn().await; 33 | assert_eq!(res, true); 34 | } 35 | 36 | fn main() { 37 | 38 | } 39 | -------------------------------------------------------------------------------- /tests/ui/05-replace-future-generic-type-with-output.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | use std::future::Future; 3 | 4 | #[maybe_async::maybe_async] 5 | pub async fn with_fn>>( 6 | test: T, 7 | ) -> Result<(), ()> 8 | where 9 | T: FnOnce() -> F, 10 | { 11 | test().await 12 | } 13 | 14 | #[maybe_async::maybe_async] 15 | pub async fn with_fn_where(test: T) -> Result<(), ()> 16 | where 17 | T: FnOnce() -> F, 18 | F: Sync + Future>, 19 | { 20 | test().await 21 | } 22 | 23 | #[maybe_async::sync_impl] 24 | fn main() { 25 | with_fn(|| Ok(())).unwrap(); 26 | with_fn_where(|| Ok(())).unwrap(); 27 | } 28 | 29 | #[maybe_async::async_impl] 30 | #[tokio::main] 31 | async fn main() { 32 | with_fn(|| async { Ok(()) }).await.unwrap(); 33 | with_fn_where(|| async { Ok(()) }).await.unwrap(); 34 | } 35 | -------------------------------------------------------------------------------- /tests/ui/06-sync_impl_async_impl.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_variables)] 2 | 3 | /// InnerClient differ a lot between sync and async. 4 | #[maybe_async::maybe_async] 5 | trait Trait { 6 | async fn maybe_async_fn(); 7 | } 8 | 9 | /// The higher level API for end user. 10 | pub struct Struct; 11 | 12 | /// Synchronous implementation, only compiles when `is_sync` feature is off. 13 | /// Else the compiler will complain that *request is defined multiple times* and 14 | /// blabla. 15 | #[maybe_async::sync_impl] 16 | impl Trait for Struct { 17 | fn maybe_async_fn() { } 18 | } 19 | 20 | /// Asynchronous implementation, only compiles when `is_sync` feature is off. 21 | #[maybe_async::async_impl] 22 | impl Trait for Struct { 23 | async fn maybe_async_fn() { } 24 | } 25 | 26 | impl Struct { 27 | #[maybe_async::maybe_async] 28 | async fn another_maybe_async_fn() { 29 | Self::maybe_async_fn().await 30 | // When `is_sync` is toggle on, this block will compiles to: 31 | // Self::maybe_async_fn() 32 | } 33 | } 34 | 35 | #[maybe_async::sync_impl] 36 | fn main() { 37 | let _ = Struct::another_maybe_async_fn(); 38 | } 39 | 40 | #[maybe_async::async_impl] 41 | #[tokio::main] 42 | async fn main() { 43 | let _ = Struct::another_maybe_async_fn().await; 44 | } 45 | -------------------------------------------------------------------------------- /tests/ui/test_fail/01-empty-test.rs: -------------------------------------------------------------------------------- 1 | use maybe_async::maybe_async; 2 | 3 | #[maybe_async] 4 | async fn async_fn() -> bool { 5 | true 6 | } 7 | 8 | // at least one sync condition should be specified 9 | #[maybe_async::test()] 10 | async fn test_async_fn() { 11 | let res = async_fn().await; 12 | assert_eq!(res, true); 13 | } 14 | 15 | fn main() { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tests/ui/test_fail/01-empty-test.stderr: -------------------------------------------------------------------------------- 1 | error: Arguments cannot be empty, at least specify the condition for sync code 2 | --> tests/ui/test_fail/01-empty-test.rs:9:1 3 | | 4 | 9 | #[maybe_async::test()] 5 | | ^^^^^^^^^^^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the attribute macro `maybe_async::test` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /tests/ui/test_fail/02-unknown-path.rs: -------------------------------------------------------------------------------- 1 | use maybe_async::maybe_async; 2 | 3 | #[maybe_async] 4 | async fn async_fn() -> bool { 5 | true 6 | } 7 | 8 | // should only accept `async` 9 | #[maybe_async::test(feature="is_sync", unknown(not(feature="is_sync"), async_std::test))] 10 | async fn test_async_fn() { 11 | let res = async_fn().await; 12 | assert_eq!(res, true); 13 | } 14 | 15 | fn main() { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tests/ui/test_fail/02-unknown-path.stderr: -------------------------------------------------------------------------------- 1 | error: Unknown path: `unknown`, must be `async` 2 | --> tests/ui/test_fail/02-unknown-path.rs:9:40 3 | | 4 | 9 | #[maybe_async::test(feature="is_sync", unknown(not(feature="is_sync"), async_std::test))] 5 | | ^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/test_fail/03-async-gt2.rs: -------------------------------------------------------------------------------- 1 | use maybe_async::maybe_async; 2 | 3 | #[maybe_async] 4 | async fn async_fn() -> bool { 5 | true 6 | } 7 | 8 | #[maybe_async::test(feature="is_sync", async(feature="async", async_std::test, added))] 9 | async fn test_async_fn() { 10 | let res = async_fn().await; 11 | assert_eq!(res, true); 12 | } 13 | 14 | fn main() { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /tests/ui/test_fail/03-async-gt2.stderr: -------------------------------------------------------------------------------- 1 | error: Must pass two metas or string literals like `async(condition, async_test_macro)`, you passed 3 metas. 2 | --> tests/ui/test_fail/03-async-gt2.rs:8:40 3 | | 4 | 8 | #[maybe_async::test(feature="is_sync", async(feature="async", async_std::test, added))] 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/test_fail/04-bad-sync-cond.rs: -------------------------------------------------------------------------------- 1 | use maybe_async::maybe_async; 2 | 3 | #[maybe_async] 4 | async fn async_fn() -> bool { 5 | true 6 | } 7 | 8 | // bad sync condition 9 | #[maybe_async::test(unknown(feature="async", async_std::test))] 10 | async fn test_async_fn() { 11 | let res = async_fn().await; 12 | assert_eq!(res, true); 13 | } 14 | 15 | fn main() { 16 | 17 | } -------------------------------------------------------------------------------- /tests/ui/test_fail/04-bad-sync-cond.stderr: -------------------------------------------------------------------------------- 1 | error[E0537]: invalid predicate `unknown` 2 | --> tests/ui/test_fail/04-bad-sync-cond.rs:9:21 3 | | 4 | 9 | #[maybe_async::test(unknown(feature="async", async_std::test))] 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/unit-test-util.rs: -------------------------------------------------------------------------------- 1 | use maybe_async::maybe_async; 2 | 3 | #[maybe_async] 4 | async fn async_fn() -> bool { 5 | true 6 | } 7 | 8 | #[maybe_async::test(feature = "is_sync", async(not(feature = "is_sync"), async_std::test))] 9 | async fn test_async_fn() { 10 | let res = async_fn().await; 11 | assert_eq!(res, true); 12 | } 13 | 14 | #[maybe_async::test(feature = "is_sync", async(not(feature = "is_sync"), tokio::test))] 15 | async fn test_async_fn2() { 16 | let res = async_fn().await; 17 | assert_eq!(res, true); 18 | } 19 | 20 | #[maybe_async::test(feature = "is_sync")] 21 | async fn test_async_fn3() { 22 | let res = async_fn().await; 23 | assert_eq!(res, true); 24 | } 25 | 26 | #[maybe_async::test(feature = "is_sync")] 27 | async fn test_sync_fn() { 28 | let res = async_fn(); 29 | assert_eq!(res, true); 30 | } 31 | --------------------------------------------------------------------------------