├── .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 | ![](https://raw.githubusercontent.com/refcell/decolor/main/etc/banner.png) 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 | --------------------------------------------------------------------------------