├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── README.tpl ├── codecov.yml ├── rustfmt.toml ├── scripts └── coverage.sh ├── src └── lib.rs ├── tarpaulin.toml └── tests └── basic.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - '**' 8 | 9 | name: CI 10 | 11 | jobs: 12 | audit: 13 | name: Audit 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: bp3d-actions/audit-check@9c23bd47e5e7b15b824739e0862cb878a52cc211 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | fmt: 23 | name: Rustfmt 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 10 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: dtolnay/rust-toolchain@master 29 | with: 30 | toolchain: nightly 31 | components: rustfmt 32 | 33 | - run: cargo fmt --all -- --check 34 | 35 | clippy: 36 | name: Clippy 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: dtolnay/rust-toolchain@master 42 | with: 43 | toolchain: nightly 44 | components: clippy 45 | 46 | - run: cargo clippy -- -D warnings 47 | 48 | coverage: 49 | name: Coverage 50 | runs-on: ubuntu-latest 51 | timeout-minutes: 10 52 | steps: 53 | - uses: actions/checkout@v3 54 | - uses: dtolnay/rust-toolchain@stable 55 | 56 | - name: tarpaulin Cache 57 | id: tarpaulin_cache 58 | uses: actions/cache@v3 59 | with: 60 | path: ~/.cargo/bin/cargo-tarpaulin 61 | key: ${{ runner.os }}-cargo-tarpaulin 62 | 63 | - run: cargo install cargo-tarpaulin 64 | if: steps.tarpaulin_cache.outputs.cache-hit != 'true' 65 | 66 | - name: 'Run `cargo-tarpaulin`' 67 | run: cargo tarpaulin --workspace 68 | 69 | - name: Upload to codecov.io 70 | uses: codecov/codecov-action@v1 71 | with: 72 | file: ./target/tarpaulin/cobertura.xml 73 | 74 | build_and_test_linux: 75 | name: Build and Test (Linux) 76 | runs-on: ubuntu-latest 77 | timeout-minutes: 10 78 | steps: 79 | - uses: actions/checkout@v3 80 | - uses: dtolnay/rust-toolchain@stable 81 | 82 | - run: cargo test --release 83 | 84 | build_and_test_windows: 85 | name: Build and Test (Windows) 86 | runs-on: windows-latest 87 | timeout-minutes: 10 88 | steps: 89 | - name: Prepare symlink configuration 90 | run: git config --global core.symlinks true 91 | 92 | - uses: actions/checkout@v3 93 | - uses: dtolnay/rust-toolchain@stable 94 | 95 | - run: cargo test --release 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.1 (2021-12-22) 4 | 5 | * Import all items from parent scope when generating structs in submodule. ([#9]) 6 | 7 | [#9]: https://github.com/azriel91/enum_variant_type/pulls/9 8 | 9 | ## 0.3.0 (2021-12-18) 10 | 11 | * `#[evt(derive(..))]` on enum adds derives on every variant. ([#6], [#7]) 12 | * `#[evt(module = "module1")]` generates structs inside `mod module1`. ([#5], [#7]) 13 | * `#[evt(implement_marker_traits(MarkerTrait1))]` on enum generates `impl MarkerTrait1` for all generated structs. ([#7]) 14 | 15 | [#5]: https://github.com/azriel91/enum_variant_type/issues/5 16 | [#6]: https://github.com/azriel91/enum_variant_type/issues/6 17 | [#7]: https://github.com/azriel91/enum_variant_type/pulls/7 18 | 19 | ## 0.2.1 (2021-04-24) 20 | 21 | * `no-std` support by default. ([#2], [#3]) 22 | 23 | [#2]: https://github.com/azriel91/enum_variant_type/issues/2 24 | [#3]: https://github.com/azriel91/enum_variant_type/pull/3 25 | 26 | ## 0.2.0 (2020-01-13) 27 | 28 | * Allow variants to be skipped using `#[evt(skip)]`. 29 | * ***Breaking:*** `#[evt(..)]` specifies the attributes to attach to the generated type (previously `#[evt_attr(..)]`). 30 | 31 | ## 0.1.0 (2020-01-10) 32 | 33 | * Generates unit, tuple, named struct for each enum variant. 34 | * `impl From for Enum`. 35 | * `impl TryFrom for EnumVariantType`. 36 | * `#[cfg(..)]`, `#[doc = ".."]`, `#[allow(..)]`, `#[deny(..)]` attributes are copied from the variant. 37 | * `#[evt_attr(..)]` specifies the attributes to attach to the generated type. 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "enum_variant_type" 3 | version = "0.3.1" 4 | authors = ["Azriel Hoh "] 5 | edition = "2018" 6 | description = "Generates types for each enum variant and conversion trait impls." 7 | repository = "https://github.com/azriel91/enum_variant_type" 8 | documentation = "https://docs.rs/enum_variant_type/" 9 | readme = "README.md" 10 | keywords = ["enum", "variant", "type"] 11 | license = "MIT OR Apache-2.0" 12 | 13 | [lib] 14 | path = "src/lib.rs" 15 | proc-macro = true 16 | 17 | [dependencies] 18 | proc-macro2 = "1.0.34" 19 | proc_macro_roids = "0.7.0" 20 | quote = "1.0.10" 21 | syn = { version = "1.0.82", features = ["extra-traits", "visit"] } 22 | 23 | [dev-dependencies] 24 | pretty_assertions = "1.0.0" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/enum_variant_type.svg)](https://crates.io/crates/enum_variant_type) 2 | [![docs.rs](https://img.shields.io/docsrs/enum_variant_type)](https://docs.rs/enum_variant_type) 3 | [![CI](https://github.com/azriel91/enum_variant_type/workflows/CI/badge.svg)](https://github.com/azriel91/enum_variant_type/actions/workflows/ci.yml) 4 | [![Coverage Status](https://codecov.io/gh/azriel91/enum_variant_type/branch/main/graph/badge.svg)](https://codecov.io/gh/azriel91/enum_variant_type) 5 | 6 | # Enum Variant Type 7 | 8 | Proc macro derive to generate structs from enum variants. 9 | 10 | This is a poor-man's implementation of . 11 | 12 | ```toml 13 | [dependencies] 14 | enum_variant_type = "0.3.1" 15 | ``` 16 | 17 | ## Examples 18 | 19 | ```rust,edition2018 20 | use enum_variant_type::EnumVariantType; 21 | 22 | #[derive(Debug, EnumVariantType, PartialEq)] 23 | pub enum MyEnum { 24 | /// Unit variant. 25 | #[evt(derive(Clone, Copy, Debug, PartialEq))] 26 | Unit, 27 | /// Tuple variant. 28 | #[evt(derive(Debug, PartialEq))] 29 | Tuple(u32, u64), 30 | /// Struct variant. 31 | #[evt(derive(Debug))] 32 | Struct { field_0: u32, field_1: u64 }, 33 | /// Skipped variant. 34 | #[evt(skip)] 35 | Skipped, 36 | } 37 | 38 | // Now you can do the following: 39 | use core::convert::TryFrom; 40 | let unit: Unit = Unit::try_from(MyEnum::Unit).unwrap(); 41 | let tuple: Tuple = Tuple::try_from(MyEnum::Tuple(12, 34)).unwrap(); 42 | let named: Struct = Struct::try_from(MyEnum::Struct { 43 | field_0: 12, 44 | field_1: 34, 45 | }) 46 | .unwrap(); 47 | 48 | let enum_unit = MyEnum::from(unit); 49 | let enum_tuple = MyEnum::from(tuple); 50 | let enum_struct = MyEnum::from(named); 51 | 52 | // If the enum variant doesn't match the variant type, then the original variant is returned in 53 | // the `Result`'s `Err` variant. 54 | assert_eq!(Err(MyEnum::Unit), Tuple::try_from(MyEnum::Unit)); 55 | ``` 56 | 57 |
58 | 59 | Generated code 60 | 61 | ```rust,edition2018 62 | use core::convert::TryFrom; 63 | 64 | /// Unit variant. 65 | #[derive(Clone, Copy, Debug, PartialEq)] 66 | pub struct Unit; 67 | 68 | /// Tuple variant. 69 | #[derive(Debug, PartialEq)] 70 | pub struct Tuple(pub u32, pub u64); 71 | 72 | /// Struct variant. 73 | #[derive(Debug)] 74 | pub struct Struct { 75 | pub field_0: u32, 76 | pub field_1: u64, 77 | } 78 | 79 | impl From for MyEnum { 80 | fn from(variant_struct: Unit) -> Self { 81 | MyEnum::Unit 82 | } 83 | } 84 | 85 | impl TryFrom for Unit { 86 | type Error = MyEnum; 87 | fn try_from(enum_variant: MyEnum) -> Result { 88 | if let MyEnum::Unit = enum_variant { 89 | Ok(Unit) 90 | } else { 91 | Err(enum_variant) 92 | } 93 | } 94 | } 95 | 96 | impl From for MyEnum { 97 | fn from(variant_struct: Tuple) -> Self { 98 | let Tuple(_0, _1) = variant_struct; 99 | MyEnum::Tuple(_0, _1) 100 | } 101 | } 102 | 103 | impl TryFrom for Tuple { 104 | type Error = MyEnum; 105 | fn try_from(enum_variant: MyEnum) -> Result { 106 | if let MyEnum::Tuple(_0, _1) = enum_variant { 107 | Ok(Tuple(_0, _1)) 108 | } else { 109 | Err(enum_variant) 110 | } 111 | } 112 | } 113 | 114 | impl From for MyEnum { 115 | fn from(variant_struct: Struct) -> Self { 116 | let Struct { field_0, field_1 } = variant_struct; 117 | MyEnum::Struct { field_0, field_1 } 118 | } 119 | } 120 | 121 | impl TryFrom for Struct { 122 | type Error = MyEnum; 123 | fn try_from(enum_variant: MyEnum) -> Result { 124 | if let MyEnum::Struct { field_0, field_1 } = enum_variant { 125 | Ok(Struct { field_0, field_1 }) 126 | } else { 127 | Err(enum_variant) 128 | } 129 | } 130 | } 131 | 132 | # pub enum MyEnum { 133 | # /// Unit variant. 134 | # Unit, 135 | # /// Tuple variant. 136 | # Tuple(u32, u64), 137 | # /// Struct variant. 138 | # Struct { 139 | # field_0: u32, 140 | # field_1: u64, 141 | # }, 142 | # } 143 | # 144 | ``` 145 | 146 |
147 | 148 | #### Additional options specified by an `evt` attribute on enum: 149 | 150 | * `#[evt(derive(Clone, Copy))]`: Derives `Clone`, `Copy` on **every** variant. 151 | * `#[evt(module = "module1")]`: Generated structs are placed into `mod module1 { ... }`. 152 | * `#[evt(implement_marker_traits(MarkerTrait1))]`: Generated structs all `impl MarkerTrait1`. 153 | 154 | ## License 155 | 156 | Licensed under either of 157 | 158 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 159 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 160 | 161 | at your option. 162 | 163 | ### Contribution 164 | 165 | Unless you explicitly state otherwise, any contribution intentionally 166 | submitted for inclusion in the work by you, as defined in the Apache-2.0 167 | license, shall be dual licensed as above, without any additional terms or 168 | conditions. 169 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/enum_variant_type.svg)](https://crates.io/crates/enum_variant_type) 2 | [![docs.rs](https://img.shields.io/docsrs/enum_variant_type)](https://docs.rs/enum_variant_type) 3 | [![CI](https://github.com/azriel91/enum_variant_type/workflows/CI/badge.svg)](https://github.com/azriel91/enum_variant_type/actions/workflows/ci.yml) 4 | [![Coverage Status](https://codecov.io/gh/azriel91/enum_variant_type/branch/main/graph/badge.svg)](https://codecov.io/gh/azriel91/enum_variant_type) 5 | 6 | # Enum Variant Type 7 | 8 | {{readme}} 9 | 10 | ## License 11 | 12 | Licensed under either of 13 | 14 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 15 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 16 | 17 | at your option. 18 | 19 | ### Contribution 20 | 21 | Unless you explicitly state otherwise, any contribution intentionally 22 | submitted for inclusion in the work by you, as defined in the Apache-2.0 23 | license, shall be dual licensed as above, without any additional terms or 24 | conditions. 25 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_code_in_doc_comments = true 2 | merge_imports = true 3 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | # Release options 5 | profile=debug 6 | 7 | # Directories 8 | self_dir="$(dirname "$(readlink -f "${BASH_SOURCE}")")" 9 | repository_dir="$(dirname "${self_dir}")" 10 | target_dir="${repository_dir}/target" 11 | target_profile_dir="${target_dir}/${profile}" 12 | 13 | coverage_dir="${target_dir}/coverage" 14 | test -d "${coverage_dir}" || mkdir -p "${coverage_dir}" 15 | 16 | kcov_exclude_line="kcov-ignore" 17 | kcov_exclude_region="kcov-ignore-start:kcov-ignore-end" 18 | 19 | # Builds all crates including tests, but don't run them yet. 20 | # We will run the tests wrapped in `kcov`. 21 | if grep -qF 'proc-macro = true' "${repository_dir}/Cargo.toml" 22 | then 23 | test_bins_by_crate="$( 24 | cargo test --no-run --message-format=json | 25 | jq -r "select(.profile.test == true and .target.kind[0] == \"proc-macro\") | (.package_id | split(\" \"))[0] + \";\" + .filenames[]" 26 | )" 27 | else 28 | test_bins_by_crate="$( 29 | cargo test --no-run --message-format=json | 30 | jq -r "select(.profile.test == true) | (.package_id | split(\" \"))[0] + \";\" + .filenames[]" 31 | )" 32 | fi 33 | 34 | # Set `LD_LIBRARY_PATH` so that tests can link against it 35 | target_arch=$(rustup toolchain list | grep -F default | cut -d ' ' -f 1 | rev | cut -d '-' -f 1-4 | rev) 36 | export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:$(rustc --print sysroot)/lib/rustlib/${target_arch}/lib/" 37 | 38 | crate_coverage_dirs=() 39 | for test_bin_by_crate in $test_bins_by_crate; do 40 | crate_name=${test_bin_by_crate%%;*} 41 | test_bin_path=${test_bin_by_crate##*;} 42 | test_bin_name=${test_bin_path##*/target/debug/} 43 | 44 | if [[ "${crate_name}" == $(basename "${repository_dir}") ]] 45 | then 46 | crate_dir="${repository_dir}" 47 | else 48 | crate_dir="${repository_dir}/${crate_name}" 49 | fi 50 | 51 | test -d "${crate_dir}" || continue; 52 | 53 | crate_coverage_dir="${coverage_dir}/${test_bin_name}" 54 | crate_coverage_dirs+=("${crate_coverage_dir}") 55 | 56 | ( 57 | echo "Running '${test_bin_path}'" 58 | 59 | export CARGO_MANIFEST_DIR="$crate_dir" 60 | kcov --include-pattern="${crate_dir}/src/,${crate_dir}/tests/" \ 61 | "--exclude-line=${kcov_exclude_line}" \ 62 | "--exclude-region=${kcov_exclude_region}" \ 63 | "${crate_coverage_dir}" "${test_bin_path}" 64 | ) 65 | 66 | done 67 | 68 | rm -rf "${coverage_dir}/merged" 69 | kcov --merge "${coverage_dir}/merged" "${crate_coverage_dirs[@]}" \ 70 | "--exclude-line=${kcov_exclude_line}" \ 71 | "--exclude-region=${kcov_exclude_region}" \ 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_debug_implementations, missing_docs)] 2 | #![no_std] 3 | #![recursion_limit = "128"] 4 | 5 | //! Proc macro derive to generate structs from enum variants. 6 | //! 7 | //! This is a poor-man's implementation of . 8 | //! 9 | //! ```toml 10 | //! [dependencies] 11 | //! enum_variant_type = "0.3.1" 12 | //! ``` 13 | //! 14 | //! # Examples 15 | //! 16 | //! ```rust,edition2018 17 | //! use enum_variant_type::EnumVariantType; 18 | //! 19 | //! #[derive(Debug, EnumVariantType, PartialEq)] 20 | //! pub enum MyEnum { 21 | //! /// Unit variant. 22 | //! #[evt(derive(Clone, Copy, Debug, PartialEq))] 23 | //! Unit, 24 | //! /// Tuple variant. 25 | //! #[evt(derive(Debug, PartialEq))] 26 | //! Tuple(u32, u64), 27 | //! /// Struct variant. 28 | //! #[evt(derive(Debug))] 29 | //! Struct { field_0: u32, field_1: u64 }, 30 | //! /// Skipped variant. 31 | //! #[evt(skip)] 32 | //! Skipped, 33 | //! } 34 | //! 35 | //! // Now you can do the following: 36 | //! use core::convert::TryFrom; 37 | //! let unit: Unit = Unit::try_from(MyEnum::Unit).unwrap(); 38 | //! let tuple: Tuple = Tuple::try_from(MyEnum::Tuple(12, 34)).unwrap(); 39 | //! let named: Struct = Struct::try_from(MyEnum::Struct { 40 | //! field_0: 12, 41 | //! field_1: 34, 42 | //! }) 43 | //! .unwrap(); 44 | //! 45 | //! let enum_unit = MyEnum::from(unit); 46 | //! let enum_tuple = MyEnum::from(tuple); 47 | //! let enum_struct = MyEnum::from(named); 48 | //! 49 | //! // If the enum variant doesn't match the variant type, then the original variant is returned in 50 | //! // the `Result`'s `Err` variant. 51 | //! assert_eq!(Err(MyEnum::Unit), Tuple::try_from(MyEnum::Unit)); 52 | //! ``` 53 | //! 54 | //!
55 | //! 56 | //! Generated code 57 | //! 58 | //! ```rust,edition2018 59 | //! use core::convert::TryFrom; 60 | //! 61 | //! /// Unit variant. 62 | //! #[derive(Clone, Copy, Debug, PartialEq)] 63 | //! pub struct Unit; 64 | //! 65 | //! /// Tuple variant. 66 | //! #[derive(Debug, PartialEq)] 67 | //! pub struct Tuple(pub u32, pub u64); 68 | //! 69 | //! /// Struct variant. 70 | //! #[derive(Debug)] 71 | //! pub struct Struct { 72 | //! pub field_0: u32, 73 | //! pub field_1: u64, 74 | //! } 75 | //! 76 | //! impl From for MyEnum { 77 | //! fn from(variant_struct: Unit) -> Self { 78 | //! MyEnum::Unit 79 | //! } 80 | //! } 81 | //! 82 | //! impl TryFrom for Unit { 83 | //! type Error = MyEnum; 84 | //! fn try_from(enum_variant: MyEnum) -> Result { 85 | //! if let MyEnum::Unit = enum_variant { 86 | //! Ok(Unit) 87 | //! } else { 88 | //! Err(enum_variant) 89 | //! } 90 | //! } 91 | //! } 92 | //! 93 | //! impl From for MyEnum { 94 | //! fn from(variant_struct: Tuple) -> Self { 95 | //! let Tuple(_0, _1) = variant_struct; 96 | //! MyEnum::Tuple(_0, _1) 97 | //! } 98 | //! } 99 | //! 100 | //! impl TryFrom for Tuple { 101 | //! type Error = MyEnum; 102 | //! fn try_from(enum_variant: MyEnum) -> Result { 103 | //! if let MyEnum::Tuple(_0, _1) = enum_variant { 104 | //! Ok(Tuple(_0, _1)) 105 | //! } else { 106 | //! Err(enum_variant) 107 | //! } 108 | //! } 109 | //! } 110 | //! 111 | //! impl From for MyEnum { 112 | //! fn from(variant_struct: Struct) -> Self { 113 | //! let Struct { field_0, field_1 } = variant_struct; 114 | //! MyEnum::Struct { field_0, field_1 } 115 | //! } 116 | //! } 117 | //! 118 | //! impl TryFrom for Struct { 119 | //! type Error = MyEnum; 120 | //! fn try_from(enum_variant: MyEnum) -> Result { 121 | //! if let MyEnum::Struct { field_0, field_1 } = enum_variant { 122 | //! Ok(Struct { field_0, field_1 }) 123 | //! } else { 124 | //! Err(enum_variant) 125 | //! } 126 | //! } 127 | //! } 128 | //! 129 | //! # pub enum MyEnum { 130 | //! # /// Unit variant. 131 | //! # Unit, 132 | //! # /// Tuple variant. 133 | //! # Tuple(u32, u64), 134 | //! # /// Struct variant. 135 | //! # Struct { 136 | //! # field_0: u32, 137 | //! # field_1: u64, 138 | //! # }, 139 | //! # } 140 | //! # 141 | //! ``` 142 | //! 143 | //!
144 | //! 145 | //! ### Additional options specified by an `evt` attribute on enum: 146 | //! 147 | //! * `#[evt(derive(Clone, Copy))]`: Derives `Clone`, `Copy` on **every** variant. 148 | //! * `#[evt(module = "module1")]`: Generated structs are placed into `mod module1 { ... }`. 149 | //! * `#[evt(implement_marker_traits(MarkerTrait1))]`: Generated structs all `impl MarkerTrait1`. 150 | 151 | extern crate alloc; 152 | extern crate proc_macro; 153 | 154 | use alloc::vec::Vec; 155 | use proc_macro::TokenStream; 156 | use proc_macro2::{Ident, Span}; 157 | use proc_macro_roids::{namespace_parameters, FieldsExt}; 158 | use quote::quote; 159 | use syn::{ 160 | parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, Lit, 161 | Meta, NestedMeta, Path, 162 | }; 163 | 164 | /// Attributes that should be copied across. 165 | const ATTRIBUTES_TO_COPY: &[&str] = &["doc", "cfg", "allow", "deny"]; 166 | 167 | /// Derives a struct for each enum variant. 168 | /// 169 | /// Struct fields including their attributes are copied over. 170 | #[cfg(not(tarpaulin_include))] 171 | #[proc_macro_derive(EnumVariantType, attributes(evt))] 172 | pub fn enum_variant_type(input: TokenStream) -> TokenStream { 173 | let ast = parse_macro_input!(input as DeriveInput); 174 | 175 | // Need to do this, otherwise we can't unit test the input. 176 | enum_variant_type_impl(ast).into() 177 | } 178 | 179 | #[inline] 180 | fn enum_variant_type_impl(ast: DeriveInput) -> proc_macro2::TokenStream { 181 | let enum_name = &ast.ident; 182 | let vis = &ast.vis; 183 | let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); 184 | let data_enum = data_enum(&ast); 185 | let variants = &data_enum.variants; 186 | 187 | let mut wrap_in_module = None::; 188 | let mut derive_for_all_variants = None::; 189 | let mut marker_trait_paths = Vec::::new(); 190 | 191 | for attr in ast.attrs.iter() { 192 | if attr.path.is_ident("evt") { 193 | if let Ok(Meta::List(list)) = attr.parse_meta() { 194 | for item in list.nested.iter() { 195 | match item { 196 | NestedMeta::Meta(Meta::NameValue(name_value)) => { 197 | if let (true, Lit::Str(lit_str)) = 198 | (name_value.path.is_ident("module"), &name_value.lit) 199 | { 200 | wrap_in_module = 201 | Some(Ident::new(&lit_str.value(), Span::call_site())); 202 | } else { 203 | panic!("Expected `evt` attribute argument in the form: `#[evt(module = \"some_module_name\")]`"); 204 | } 205 | } 206 | NestedMeta::Meta(Meta::List(list)) => { 207 | if list.path.is_ident("derive") { 208 | let items = list.nested.iter().map(|nested_meta| { 209 | if let NestedMeta::Meta(Meta::Path(path)) = nested_meta { 210 | path.clone() 211 | } else { 212 | panic!("Expected `evt` attribute argument in the form: `#[evt(derive(Clone, Debug))]`"); 213 | } 214 | }); 215 | derive_for_all_variants = Some(parse_quote! { 216 | #[derive( #(#items),* )] 217 | }); 218 | } else if list.path.is_ident("implement_marker_traits") { 219 | marker_trait_paths = list.nested 220 | .iter() 221 | .map(|nested| if let NestedMeta::Meta(Meta::Path(path)) = nested { 222 | path.clone() 223 | } else { 224 | panic!("Expected `evt` attribute argument in the form #[evt(implement_marker_traits(MarkerTrait1, MarkerTrait2))]"); 225 | }).collect(); 226 | } 227 | } 228 | _ => { 229 | panic!("Unexpected usage of `evt` attribute, please see examples at:\n") 230 | } 231 | } 232 | } 233 | } else { 234 | panic!("Unexpected usage of `evt` attribute, please see examples at:\n") 235 | } 236 | } 237 | } 238 | 239 | let mut struct_declarations = proc_macro2::TokenStream::new(); 240 | 241 | let ns: Path = parse_quote!(evt); 242 | let skip: Path = parse_quote!(skip); 243 | let struct_declarations_iter = variants.iter() 244 | .filter(|variant| !proc_macro_roids::contains_tag(&variant.attrs, &ns, &skip)) 245 | .map(|variant| { 246 | 247 | let variant_name = &variant.ident; 248 | let attrs_to_copy = variant 249 | .attrs 250 | .iter() 251 | .filter(|attribute| { 252 | ATTRIBUTES_TO_COPY 253 | .iter() 254 | .any(|attr_to_copy| attribute.path.is_ident(attr_to_copy)) 255 | }) 256 | .collect::>(); 257 | 258 | let evt_meta_lists = namespace_parameters(&variant.attrs, &ns); 259 | let variant_struct_attrs = evt_meta_lists 260 | .into_iter() 261 | .fold( 262 | proc_macro2::TokenStream::new(), 263 | |mut attrs_tokens, variant_struct_attr| { 264 | attrs_tokens.extend(quote!(#[#variant_struct_attr])); 265 | attrs_tokens 266 | }, 267 | ); 268 | let variant_fields = &variant.fields; 269 | 270 | // Need to attach visibility modifier to fields. 271 | let fields_with_vis = variant_fields 272 | .iter() 273 | .cloned() 274 | .map(|mut field| { 275 | field.vis = vis.clone(); 276 | field 277 | }) 278 | .collect::>(); 279 | 280 | let data_struct = match variant_fields { 281 | Fields::Unit => quote! { 282 | struct #variant_name; 283 | }, 284 | Fields::Unnamed(..) => { 285 | quote! { 286 | struct #variant_name #ty_generics (#(#fields_with_vis,)*) #where_clause; 287 | } 288 | } 289 | Fields::Named(..) => quote! { 290 | struct #variant_name #ty_generics #where_clause { 291 | #(#fields_with_vis,)* 292 | } 293 | }, 294 | }; 295 | 296 | // TODO: This generates invalid code if the type parameter is not used by this variant. 297 | let construction_form = variant_fields.construction_form(); 298 | let deconstruct_variant_struct = if variant_fields.is_unit() { 299 | proc_macro2::TokenStream::new() 300 | } else { 301 | quote! { 302 | let #variant_name #construction_form = variant_struct; 303 | } 304 | }; 305 | let impl_from_variant_for_enum = quote! { 306 | impl #impl_generics core::convert::From<#variant_name #ty_generics> 307 | for #enum_name #ty_generics 308 | #where_clause { 309 | fn from(variant_struct: #variant_name #ty_generics) -> Self { 310 | // Deconstruct the parameter. 311 | #deconstruct_variant_struct 312 | 313 | #enum_name::#variant_name #construction_form 314 | } 315 | } 316 | }; 317 | 318 | let impl_try_from_enum_for_variant = quote! { 319 | impl #impl_generics core::convert::TryFrom<#enum_name #ty_generics> 320 | for #variant_name #ty_generics 321 | #where_clause { 322 | type Error = #enum_name #ty_generics; 323 | 324 | fn try_from(enum_variant: #enum_name #ty_generics) -> Result { 325 | // Deconstruct the variant. 326 | if let #enum_name::#variant_name #construction_form = enum_variant { 327 | core::result::Result::Ok(#variant_name #construction_form) 328 | } else { 329 | core::result::Result::Err(enum_variant) 330 | } 331 | } 332 | } 333 | }; 334 | 335 | quote! { 336 | #(#attrs_to_copy)* 337 | #derive_for_all_variants 338 | #variant_struct_attrs 339 | #vis #data_struct 340 | 341 | #impl_from_variant_for_enum 342 | 343 | #impl_try_from_enum_for_variant 344 | 345 | #(impl #ty_generics #marker_trait_paths for #variant_name #ty_generics {})* 346 | } 347 | }); 348 | struct_declarations.extend(struct_declarations_iter); 349 | 350 | if let Some(module_to_wrap_in) = wrap_in_module { 351 | quote! { 352 | #vis mod #module_to_wrap_in { 353 | use super::*; 354 | 355 | #struct_declarations 356 | } 357 | } 358 | } else { 359 | struct_declarations 360 | } 361 | } 362 | 363 | fn data_enum(ast: &DeriveInput) -> &DataEnum { 364 | if let Data::Enum(data_enum) = &ast.data { 365 | data_enum 366 | } else { 367 | panic!("`EnumVariantType` derive can only be used on an enum."); 368 | } 369 | } 370 | 371 | #[cfg(test)] 372 | mod tests { 373 | extern crate alloc; 374 | 375 | use alloc::string::ToString; 376 | use pretty_assertions::assert_eq; 377 | use quote::quote; 378 | use syn::{parse_quote, DeriveInput}; 379 | 380 | use super::enum_variant_type_impl; 381 | 382 | #[test] 383 | fn generates_correct_tokens_for_basic_enum() { 384 | let ast: DeriveInput = parse_quote! { 385 | pub enum MyEnum { 386 | /// Unit variant. 387 | #[evt(derive(Clone, Copy, Debug, PartialEq))] 388 | Unit, 389 | /// Tuple variant. 390 | #[evt(derive(Debug))] 391 | Tuple(u32, u64), 392 | /// Struct variant. 393 | Struct { 394 | field_0: u32, 395 | field_1: u64, 396 | }, 397 | } 398 | }; 399 | 400 | let actual_tokens = enum_variant_type_impl(ast); 401 | let expected_tokens = quote! { 402 | /// Unit variant. 403 | #[derive(Clone, Copy, Debug, PartialEq)] 404 | pub struct Unit; 405 | 406 | impl core::convert::From for MyEnum { 407 | fn from(variant_struct: Unit) -> Self { 408 | MyEnum::Unit 409 | } 410 | } 411 | 412 | impl core::convert::TryFrom for Unit { 413 | type Error = MyEnum; 414 | fn try_from(enum_variant: MyEnum) -> Result { 415 | if let MyEnum::Unit = enum_variant { 416 | core::result::Result::Ok(Unit) 417 | } else { 418 | core::result::Result::Err(enum_variant) 419 | } 420 | } 421 | } 422 | 423 | /// Tuple variant. 424 | #[derive(Debug)] 425 | pub struct Tuple(pub u32, pub u64,); 426 | 427 | impl core::convert::From for MyEnum { 428 | fn from(variant_struct: Tuple) -> Self { 429 | let Tuple(_0, _1,) = variant_struct; 430 | MyEnum::Tuple(_0, _1,) 431 | } 432 | } 433 | 434 | impl core::convert::TryFrom for Tuple { 435 | type Error = MyEnum; 436 | fn try_from(enum_variant: MyEnum) -> Result { 437 | if let MyEnum::Tuple(_0, _1,) = enum_variant { 438 | core::result::Result::Ok(Tuple(_0, _1,)) 439 | } else { 440 | core::result::Result::Err(enum_variant) 441 | } 442 | } 443 | } 444 | 445 | /// Struct variant. 446 | pub struct Struct { 447 | pub field_0: u32, 448 | pub field_1: u64, 449 | } 450 | 451 | impl core::convert::From for MyEnum { 452 | fn from(variant_struct: Struct) -> Self { 453 | let Struct { field_0, field_1, } = variant_struct; 454 | MyEnum::Struct { field_0, field_1, } 455 | } 456 | } 457 | 458 | impl core::convert::TryFrom for Struct { 459 | type Error = MyEnum; 460 | fn try_from(enum_variant: MyEnum) -> Result { 461 | if let MyEnum::Struct { field_0, field_1, } = enum_variant { 462 | core::result::Result::Ok(Struct { field_0, field_1, }) 463 | } else { 464 | core::result::Result::Err(enum_variant) 465 | } 466 | } 467 | } 468 | }; 469 | 470 | assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); 471 | } 472 | 473 | #[test] 474 | fn skips_variants_marked_with_evt_skip() { 475 | let ast: DeriveInput = parse_quote! { 476 | pub enum MyEnum { 477 | /// Unit variant. 478 | #[evt(derive(Clone, Copy, Debug, PartialEq))] 479 | Unit, 480 | /// Skipped variant. 481 | #[evt(skip)] 482 | UnitSkipped, 483 | } 484 | }; 485 | 486 | let actual_tokens = enum_variant_type_impl(ast); 487 | let expected_tokens = quote! { 488 | /// Unit variant. 489 | #[derive(Clone, Copy, Debug, PartialEq)] 490 | pub struct Unit; 491 | 492 | impl core::convert::From for MyEnum { 493 | fn from(variant_struct: Unit) -> Self { 494 | MyEnum::Unit 495 | } 496 | } 497 | 498 | impl core::convert::TryFrom for Unit { 499 | type Error = MyEnum; 500 | fn try_from(enum_variant: MyEnum) -> Result { 501 | if let MyEnum::Unit = enum_variant { 502 | core::result::Result::Ok(Unit) 503 | } else { 504 | core::result::Result::Err(enum_variant) 505 | } 506 | } 507 | } 508 | }; 509 | 510 | assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); 511 | } 512 | 513 | #[test] 514 | fn put_variants_in_module() { 515 | let ast: DeriveInput = parse_quote! { 516 | #[evt(module = "example")] 517 | pub enum MyEnum { 518 | A, 519 | B 520 | } 521 | }; 522 | 523 | let actual_tokens = enum_variant_type_impl(ast); 524 | let expected_tokens = quote! { 525 | pub mod example { 526 | use super::*; 527 | 528 | pub struct A; 529 | 530 | impl core::convert::From for MyEnum { 531 | fn from(variant_struct: A) -> Self { 532 | MyEnum::A 533 | } 534 | } 535 | 536 | impl core::convert::TryFrom for A { 537 | type Error = MyEnum; 538 | fn try_from(enum_variant: MyEnum) -> Result { 539 | if let MyEnum::A = enum_variant { 540 | core::result::Result::Ok(A) 541 | } else { 542 | core::result::Result::Err(enum_variant) 543 | } 544 | } 545 | } 546 | 547 | pub struct B; 548 | 549 | impl core::convert::From for MyEnum { 550 | fn from(variant_struct: B) -> Self { 551 | MyEnum::B 552 | } 553 | } 554 | 555 | impl core::convert::TryFrom for B { 556 | type Error = MyEnum; 557 | fn try_from(enum_variant: MyEnum) -> Result { 558 | if let MyEnum::B = enum_variant { 559 | core::result::Result::Ok(B) 560 | } else { 561 | core::result::Result::Err(enum_variant) 562 | } 563 | } 564 | } 565 | } 566 | }; 567 | 568 | assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); 569 | } 570 | 571 | #[test] 572 | fn derive_traits_for_all_variants() { 573 | let ast: DeriveInput = parse_quote! { 574 | #[evt(derive(Debug))] 575 | pub enum MyEnum { 576 | A, 577 | #[evt(derive(Clone))] 578 | B 579 | } 580 | }; 581 | 582 | let actual_tokens = enum_variant_type_impl(ast); 583 | let expected_tokens = quote! { 584 | #[derive(Debug)] 585 | pub struct A; 586 | 587 | impl core::convert::From for MyEnum { 588 | fn from(variant_struct: A) -> Self { 589 | MyEnum::A 590 | } 591 | } 592 | 593 | impl core::convert::TryFrom for A { 594 | type Error = MyEnum; 595 | fn try_from(enum_variant: MyEnum) -> Result { 596 | if let MyEnum::A = enum_variant { 597 | core::result::Result::Ok(A) 598 | } else { 599 | core::result::Result::Err(enum_variant) 600 | } 601 | } 602 | } 603 | 604 | #[derive(Debug)] 605 | #[derive(Clone)] 606 | pub struct B; 607 | 608 | impl core::convert::From for MyEnum { 609 | fn from(variant_struct: B) -> Self { 610 | MyEnum::B 611 | } 612 | } 613 | 614 | impl core::convert::TryFrom for B { 615 | type Error = MyEnum; 616 | fn try_from(enum_variant: MyEnum) -> Result { 617 | if let MyEnum::B = enum_variant { 618 | core::result::Result::Ok(B) 619 | } else { 620 | core::result::Result::Err(enum_variant) 621 | } 622 | } 623 | } 624 | }; 625 | 626 | assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); 627 | } 628 | 629 | #[test] 630 | fn derive_marker_trait() { 631 | let ast: DeriveInput = parse_quote! { 632 | #[evt(implement_marker_traits(MarkerTrait1))] 633 | pub enum MyEnum { 634 | A, 635 | B 636 | } 637 | }; 638 | 639 | let actual_tokens = enum_variant_type_impl(ast); 640 | let expected_tokens = quote! { 641 | pub struct A; 642 | 643 | impl core::convert::From for MyEnum { 644 | fn from(variant_struct: A) -> Self { 645 | MyEnum::A 646 | } 647 | } 648 | 649 | impl core::convert::TryFrom for A { 650 | type Error = MyEnum; 651 | fn try_from(enum_variant: MyEnum) -> Result { 652 | if let MyEnum::A = enum_variant { 653 | core::result::Result::Ok(A) 654 | } else { 655 | core::result::Result::Err(enum_variant) 656 | } 657 | } 658 | } 659 | 660 | impl MarkerTrait1 for A {} 661 | 662 | pub struct B; 663 | 664 | impl core::convert::From for MyEnum { 665 | fn from(variant_struct: B) -> Self { 666 | MyEnum::B 667 | } 668 | } 669 | 670 | impl core::convert::TryFrom for B { 671 | type Error = MyEnum; 672 | fn try_from(enum_variant: MyEnum) -> Result { 673 | if let MyEnum::B = enum_variant { 674 | core::result::Result::Ok(B) 675 | } else { 676 | core::result::Result::Err(enum_variant) 677 | } 678 | } 679 | } 680 | 681 | impl MarkerTrait1 for B {} 682 | }; 683 | 684 | assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); 685 | } 686 | } 687 | -------------------------------------------------------------------------------- /tarpaulin.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | 3 | [report] 4 | out = ["Html", "Xml"] 5 | output-dir = "target/tarpaulin" 6 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use core::convert::TryFrom; 2 | 3 | use enum_variant_type::EnumVariantType; 4 | 5 | #[derive(Debug, EnumVariantType, PartialEq)] 6 | pub enum MyEnum { 7 | /// Unit variant. 8 | #[evt(derive(Clone, Copy, Debug, PartialEq))] 9 | Unit, 10 | /// Tuple variant. 11 | #[evt(derive(Clone, Copy, Debug, PartialEq))] 12 | Tuple(u32, u64), 13 | /// Struct variant. 14 | #[evt(derive(Clone, Copy, Debug, PartialEq))] 15 | Struct { field_0: u32, field_1: u64 }, 16 | } 17 | 18 | #[test] 19 | fn enum_from_unit_struct() { 20 | assert_eq!(MyEnum::Unit, MyEnum::from(Unit)); 21 | } 22 | 23 | #[test] 24 | fn unit_struct_try_from_enum_ok() { 25 | assert_eq!(Ok(Unit), Unit::try_from(MyEnum::Unit)); 26 | } 27 | 28 | #[test] 29 | fn unit_struct_try_from_enum_err() { 30 | assert_eq!( 31 | Err(MyEnum::Tuple(1, 2)), 32 | Unit::try_from(MyEnum::Tuple(1, 2)) 33 | ); 34 | } 35 | 36 | #[test] 37 | fn enum_from_tuple_struct() { 38 | assert_eq!(MyEnum::Tuple(1, 2), MyEnum::from(Tuple(1, 2))); 39 | } 40 | 41 | #[test] 42 | fn tuple_struct_try_from_enum_ok() { 43 | assert_eq!(Ok(Tuple(1, 2)), Tuple::try_from(MyEnum::Tuple(1, 2))); 44 | } 45 | 46 | #[test] 47 | fn tuple_struct_try_from_enum_err() { 48 | assert_eq!(Err(MyEnum::Unit), Tuple::try_from(MyEnum::Unit)); 49 | } 50 | 51 | #[test] 52 | fn enum_from_named_struct() { 53 | assert_eq!( 54 | MyEnum::Struct { 55 | field_0: 1, 56 | field_1: 2 57 | }, 58 | MyEnum::from(Struct { 59 | field_0: 1, 60 | field_1: 2 61 | }) 62 | ); 63 | } 64 | 65 | #[test] 66 | fn named_struct_try_from_enum_ok() { 67 | assert_eq!( 68 | Ok(Struct { 69 | field_0: 1, 70 | field_1: 2 71 | }), 72 | Struct::try_from(MyEnum::Struct { 73 | field_0: 1, 74 | field_1: 2 75 | }) 76 | ); 77 | } 78 | 79 | #[test] 80 | fn named_struct_try_from_enum_err() { 81 | assert_eq!(Err(MyEnum::Unit), Struct::try_from(MyEnum::Unit)); 82 | } 83 | --------------------------------------------------------------------------------