├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── etc
├── banner.png
├── favicon.ico
└── logo.png
├── src
└── lib.rs
└── tests
├── example.rs
└── macros.rs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: read
10 |
11 | env:
12 | RUSTFLAGS: -Dwarnings
13 |
14 | jobs:
15 | cargo-tests:
16 | runs-on: ubuntu-latest
17 | timeout-minutes: 20
18 | steps:
19 | - name: Checkout sources
20 | uses: actions/checkout@v3
21 | - name: Install Rust toolchain
22 | uses: actions-rs/toolchain@v1
23 | with:
24 | toolchain: nightly
25 | profile: minimal
26 | override: true
27 | - uses: Swatinem/rust-cache@v1
28 | with:
29 | cache-on-failure: true
30 | - name: cargo test
31 | run: cargo test --all
32 | - name: cargo test all features
33 | run: cargo test --all --all-features
34 | cargo-lint:
35 | runs-on: ubuntu-latest
36 | timeout-minutes: 20
37 | steps:
38 | - name: Checkout sources
39 | uses: actions/checkout@v3
40 | - name: Install Rust toolchain
41 | uses: actions-rs/toolchain@v1
42 | with:
43 | toolchain: nightly
44 | profile: minimal
45 | components: rustfmt, clippy
46 | override: true
47 | - uses: Swatinem/rust-cache@v1
48 | with:
49 | cache-on-failure: true
50 | - name: cargo fmt
51 | run: cargo +nightly fmt --all -- --check
52 | - name: cargo clippy
53 | run: cargo +nightly clippy --all --all-features -- -D warnings
54 | cargo-build:
55 | runs-on: ubuntu-latest
56 | timeout-minutes: 20
57 | continue-on-error: true
58 | steps:
59 | - name: Checkout sources
60 | uses: actions/checkout@v3
61 | - name: Install Rust toolchain
62 | uses: actions-rs/toolchain@v1
63 | with:
64 | toolchain: nightly
65 | profile: minimal
66 | override: true
67 | - uses: Swatinem/rust-cache@v1
68 | with:
69 | cache-on-failure: true
70 | - name: build
71 | id: build
72 | continue-on-error: true
73 | run: cargo build --all
74 | cargo-doc:
75 | runs-on: ubuntu-latest
76 | timeout-minutes: 20
77 | continue-on-error: true
78 | steps:
79 | - name: Checkout sources
80 | uses: actions/checkout@v3
81 | - name: Install Rust toolchain
82 | uses: actions-rs/toolchain@v1
83 | with:
84 | toolchain: nightly
85 | profile: minimal
86 | override: true
87 | - uses: Swatinem/rust-cache@v1
88 | with:
89 | cache-on-failure: true
90 | - name: doclint
91 | id: build
92 | continue-on-error: true
93 | run: RUSTDOCFLAGS="-D warnings" cargo doc --all --no-deps --all-features --document-private-items
94 | - name: doctest
95 | run: cargo test --doc --all --all-features
96 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8 | Cargo.lock
9 |
10 | # These are backup files generated by rustfmt
11 | **/*.rs.bk
12 |
13 | # MSVC Windows builds of rustc generate these, which store debugging information
14 | *.pdb
15 |
16 |
17 | # Added by cargo
18 |
19 | /target
20 | /Cargo.lock
21 |
22 | .DS_Store
23 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "decolor"
3 | description = "Asynchronous runtime abstractions for implicit function decoloring."
4 | version = "0.1.2"
5 | edition = "2021"
6 | license = "MIT"
7 | authors = ["refcell"]
8 | rust-version = "1.72"
9 | keywords = ["decolor", "async", "color", "macro", "library"]
10 | categories = ["asynchronous", "development-tools::procedural-macro-helpers"]
11 | homepage = "https://github.com/refcell/decolor"
12 | repository = "https://github.com/refcell/decolor"
13 | exclude = ["benches/", "tests/"]
14 |
15 | [lib]
16 | proc-macro = true
17 |
18 | [dependencies]
19 | anyhow = "1.0"
20 | proc-macro2 = "1.0"
21 | quote = "1.0"
22 | syn = "2.0"
23 | tokio = { version = "1.33", features = ["full"] }
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 refcell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # decolor
2 |
3 | [![Build Status]][actions]
4 | [![License]][mit-license]
5 | [![Docs]][Docs-rs]
6 | [![Latest Version]][crates.io]
7 | [![rustc 1.31+]][Rust 1.31]
8 |
9 | [Build Status]: https://img.shields.io/github/actions/workflow/status/refcell/decolor/ci.yml?branch=main
10 | [actions]: https://github.com/refcell/decolor/actions?query=branch%3Amain
11 | [Latest Version]: https://img.shields.io/crates/v/decolor.svg
12 | [crates.io]: https://crates.io/crates/decolor
13 | [rustc 1.31+]: https://img.shields.io/badge/rustc_1.31+-lightgray.svg
14 | [Rust 1.31]: https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html
15 | [License]: https://img.shields.io/badge/license-MIT-7795AF.svg
16 | [mit-license]: https://github.com/refcell/decolor/blob/main/LICENSE.md
17 | [Docs-rs]: https://docs.rs/decolor/
18 | [Docs]: https://img.shields.io/docsrs/decolor.svg?color=319e8c&label=docs.rs
19 |
20 | **Asynchronous runtime abstractions for implicit function decoloring.** Decolor is in https://github.com/refcell/decolor/labels/beta
21 |
22 | 
23 |
24 | **[Install](#usage)**
25 | | [User Docs](#what-is-decolor)
26 | | [Crate Docs][crates.io]
27 | | [Reference][Docs-rs]
28 | | [Contributing](#contributing)
29 | | [License](#license)
30 |
31 | ## What is decolor?
32 |
33 | `decolor` is a [procedural macro][proc-macro] crate that implements
34 | a `#[decolor]` [attribute macro][attribute-macro] used to *"decolor"*
35 | an asynchronous rust function. Concretely, the `#[decolor]` macro
36 | can be placed above an asynchronous function to safely1 transform it
37 | into a ["purple" function][purple] (a synchronous function that blocks
38 | on asynchronous functionality internally).
39 |
40 | **1**: Constructing the `block_on()` call in this way prevents nested runtime
41 | panics, but calling the Handle [block_on][block-on] method itself [panics][handle-panics]
42 | if the provided future panics *or* if the runtime on which a timer future is called upon
43 | is shut down prior to completion. Additionally, the [Runtime][runtime]'s
44 | [`block_on`][runtime-block-on] call will [panic][runtime-panic] if it is called from within
45 | an asynchronous execution context.
46 |
47 | [runtime-panic]: https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#panics
48 | [runtime-block-on]: https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#method.block_on
49 | [runtime]: https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#
50 | [block-on]: https://docs.rs/tokio/latest/tokio/runtime/struct.Handle.html#method.block_on
51 | [handle-panics]: https://docs.rs/tokio/latest/tokio/runtime/struct.Handle.html#panics-2
52 | [purple]: https://morestina.net/blog/1686/rust-async-is-colored
53 | [attribute-macro]: https://doc.rust-lang.org/beta/reference/procedural-macros.html#attribute-macros
54 | [proc-macro]: https://doc.rust-lang.org/beta/reference/procedural-macros.html
55 |
56 | ## Usage
57 |
58 | Add `decolor` as a dependency with cargo.
59 |
60 | ```bash,ignore
61 | cargo add decolor
62 | ```
63 |
64 | A short example for building a purple function using the
65 | [decolor][decolor] decorator.
66 |
67 | ```rust
68 | use decolor::decolor;
69 | use tokio::time::{sleep, Duration};
70 |
71 | #[decolor]
72 | async fn foo() -> anyhow::Result<()> {
73 | sleep(Duration::from_secs(1)).await;
74 | println!("Hello, world!");
75 | Ok(())
76 | }
77 |
78 | fn main() {
79 | assert!(foo().is_ok());
80 | }
81 | ```
82 |
83 | ## Contributing
84 |
85 | All contributions are welcome! Experimentation is highly encouraged
86 | and new issues are welcome.
87 |
88 | ## Troubleshooting & Bug Reports
89 |
90 | Please check existing issues for similar bugs or
91 | [open an issue](https://github.com/refcell/decolor/issues/new)
92 | if no relevant issue already exists.
93 |
94 | ## License
95 |
96 | This project is licensed under the [MIT License](LICENSE.md).
97 | Free and open-source, forever.
98 | *All our rust are belong to you.*
99 |
--------------------------------------------------------------------------------
/etc/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refcell/decolor/10b4795612482e756d43858f1d2d63dff62c5fba/etc/banner.png
--------------------------------------------------------------------------------
/etc/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refcell/decolor/10b4795612482e756d43858f1d2d63dff62c5fba/etc/favicon.ico
--------------------------------------------------------------------------------
/etc/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refcell/decolor/10b4795612482e756d43858f1d2d63dff62c5fba/etc/logo.png
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![doc = include_str!("../README.md")]
2 | #![doc(
3 | html_logo_url = "https://raw.githubusercontent.com/refcell/decolor/main/etc/logo.png",
4 | html_favicon_url = "https://raw.githubusercontent.com/refcell/decolor/main/etc/favicon.ico",
5 | issue_tracker_base_url = "https://github.com/refcell/decolor/issues/"
6 | )]
7 | #![warn(
8 | missing_debug_implementations,
9 | missing_docs,
10 | unreachable_pub,
11 | rustdoc::all
12 | )]
13 | #![deny(unused_must_use, rust_2018_idioms)]
14 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
15 |
16 | use proc_macro::TokenStream;
17 | use quote::quote;
18 | use syn::{parse_macro_input, ItemFn};
19 |
20 | /// Procedural macro that accepts an async function
21 | /// and turns it into a blocking function by wrapping
22 | /// the internals with a blocking call.
23 | ///
24 | /// Under the hood, this works by calling attempting
25 | /// to build a tokio [runtime](tokio::runtime::Runtime)
26 | /// and then calling [`block_on(...)`] on the returned
27 | /// runtime. If the runtime construction fails, for example
28 | /// because the runtime is being nested, then [`decolor`]
29 | /// will fallback to using the current [Handle](tokio::runtime::Handle)
30 | /// and then calling [`block_on()`](tokio::runtime::Handle::block_on())
31 | /// on the returned handle.
32 | ///
33 | /// # Example
34 | /// ```
35 | /// use tokio::time::{sleep, Duration};
36 | /// use decolor::decolor;
37 | ///
38 | /// #[decolor]
39 | /// async fn foo() {
40 | /// sleep(Duration::from_secs(1)).await;
41 | /// println!("Hello, world!");
42 | /// }
43 | ///
44 | /// fn main() {
45 | /// foo();
46 | /// }
47 | /// ```
48 | #[proc_macro_attribute]
49 | pub fn decolor(_attr: TokenStream, input: TokenStream) -> TokenStream {
50 | // Parse the input as an async function
51 | let input = parse_macro_input!(input as ItemFn);
52 |
53 | // Extract the function body
54 | let orig_function_body = input.block;
55 |
56 | // Extract the function name and return type.
57 | // Then generate a new synchronous function
58 | // with the same name and return type.
59 | let fn_name = &input.sig.ident;
60 | let orig_return_type = &input.sig.output;
61 | let expanded = quote! {
62 | fn #fn_name() #orig_return_type {
63 | match tokio::runtime::Runtime::new() {
64 | Ok(rt) => rt.block_on(async move {
65 | #orig_function_body
66 | }),
67 | Err(_) => tokio::runtime::Handle::current().block_on(async move {
68 | #orig_function_body
69 | }),
70 | }
71 | }
72 | };
73 |
74 | expanded.into()
75 | }
76 |
--------------------------------------------------------------------------------
/tests/example.rs:
--------------------------------------------------------------------------------
1 | /// Purple function demonstratings a blue function
2 | /// that internally hides a blocking call to a red
3 | /// function using a runtime like tokio.
4 | fn purple() -> anyhow::Result<()> {
5 | let rt = tokio::runtime::Runtime::new().unwrap();
6 | rt.block_on(async {
7 | // Perform an async file read call
8 | println!("Inside purple blocking call...");
9 | });
10 | Ok(())
11 | }
12 |
13 | #[tokio::test]
14 | async fn test_call_purple_inside_runtime() {
15 | let result = std::panic::catch_unwind(|| {
16 | let rt = tokio::runtime::Runtime::new().unwrap();
17 | rt.block_on(async {
18 | // This function call will panic because internally
19 | // it tries to construct a tokio runtime, which would
20 | // create a nested runtime (not allowed).
21 | let _ = purple();
22 | });
23 | });
24 | assert!(result.is_err());
25 | }
26 |
--------------------------------------------------------------------------------
/tests/macros.rs:
--------------------------------------------------------------------------------
1 | use decolor::decolor;
2 |
3 | #[decolor]
4 | async fn foo() -> String {
5 | let one_second = tokio::time::Duration::from_secs(1);
6 | tokio::time::sleep(one_second).await;
7 | "Hello world!".to_string()
8 | }
9 |
10 | #[decolor]
11 | async fn bar() {
12 | let one_second = tokio::time::Duration::from_secs(1);
13 | tokio::time::sleep(one_second).await;
14 | println!("Hello world!");
15 | }
16 |
17 | #[derive(Debug, PartialEq)]
18 | struct Response {
19 | status: u16,
20 | body: String,
21 | }
22 |
23 | #[decolor]
24 | async fn res() -> anyhow::Result {
25 | let one_second = tokio::time::Duration::from_secs(1);
26 | tokio::time::sleep(one_second).await;
27 | Ok(Response {
28 | status: 200,
29 | body: "Hello world!".to_string(),
30 | })
31 | }
32 |
33 | #[test]
34 | fn test_decolor_result_with_struct() {
35 | let expected = Response {
36 | status: 200,
37 | body: "Hello world!".to_string(),
38 | };
39 | assert_eq!(res().unwrap(), expected);
40 | }
41 |
42 | #[test]
43 | fn test_decolor_with_return_type() {
44 | assert_eq!(foo(), "Hello world!".to_string());
45 | }
46 |
47 | #[test]
48 | fn test_decolor_empty_return_type() {
49 | assert_eq!(bar(), ());
50 | }
51 |
--------------------------------------------------------------------------------