├── .github └── workflows │ ├── check.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples └── require_docs.rs ├── src ├── build.rs ├── derive.rs ├── derive │ └── partial_eq.rs ├── enum.rs ├── extractor.rs ├── iter.rs ├── lib.rs └── param.rs └── tests ├── it.rs └── subenum_specific.rs /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: CI 10 | 11 | jobs: 12 | all-succeeded: 13 | name: All Succeeded 14 | if: always() 15 | runs-on: ubuntu-latest 16 | needs: 17 | - test 18 | - lint 19 | steps: 20 | - name: Check if all jubs succeeded 21 | uses: re-actors/alls-green@release/v1 22 | with: 23 | jobs: ${{ toJSON(needs) }} 24 | 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | env: 29 | RUSTFLAGS: -D warnings 30 | strategy: 31 | matrix: 32 | rust: 33 | - stable 34 | - nightly 35 | flags: 36 | - "" 37 | - "--no-default-features" 38 | steps: 39 | - uses: actions/checkout@v4 40 | name: Checkout 41 | - uses: dtolnay/rust-toolchain@master 42 | with: 43 | toolchain: ${{ matrix.rust }} 44 | name: Install Rust Toolchain 45 | - run: cargo test ${{ matrix.flags }} 46 | - run: cargo doc 47 | 48 | lint: 49 | name: Lint 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | name: Checkout 54 | - uses: dtolnay/rust-toolchain@stable 55 | name: Install Rust Toolchain 56 | with: 57 | components: rustfmt, clippy 58 | - run: cargo fmt --all -- --check 59 | - run: cargo clippy -- -D warnings 60 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Publish 7 | 8 | jobs: 9 | release: 10 | name: GitHub Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: marvinpinto/action-automatic-releases@latest 15 | with: 16 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 17 | prerelease: false 18 | 19 | publish: 20 | name: Crates.io Publish 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: dtolnay/rust-toolchain@stable 25 | name: Install Rust Toolchain 26 | - uses: katyo/publish-crates@v1 27 | with: 28 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project follows semantic versioning. 4 | 5 | ### Unreleased 6 | 7 | ### 1.1.2 (2024-03-12) 8 | - [fixed] Fix error when there's an enum variant called `Error`. 9 | 10 | ### 1.1.1 (2023-09-18) 11 | - [fixed] Support for `#![deny(missing_docs)]`. 12 | 13 | ### 1.1.0 (2023-09-18) 14 | - [added] Default feature `std` and support for no-std. 15 | - [added] Support for subenum-specific proc-macros. 16 | 17 | ### 1.0.1 (2023-02-25) 18 | - [fixed] References to generic types. 19 | 20 | ### 1.0.0 (2023-02-20) 21 | - [fixed] Bug when repeating a type in an unnamed variant. 22 | - [changed] **BREAKING** All enums to be created must now be declared at the 23 | top-level subenum attribute. 24 | - [added] Support for lifetimes and generics. 25 | 26 | ### 0.1.0 (2023-02-13) 27 | - [added] Initial `subenum` macro creation. 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "subenum" 3 | version = "1.1.2" 4 | edition = "2021" 5 | authors = ["Paho Lurie-Gregg "] 6 | documentation = "https://docs.rs/subenum" 7 | repository = "https://github.com/paholg/subenum" 8 | readme = "README.md" 9 | license = "MIT OR Apache-2.0" 10 | description = """A proc-macro to create subsets of enums, that can be converted 11 | to and from.""" 12 | categories = ["development-tools::procedural-macro-helpers"] 13 | keywords = ["enum", "sub-enum", "no-std"] 14 | 15 | [lib] 16 | proc-macro = true 17 | name = "subenum" 18 | 19 | [[example]] 20 | name = "require_docs" 21 | 22 | [dev-dependencies] 23 | derive_more = "0.99.17" 24 | strum = { version = "0.24.1", features = ["derive"], default-features = false } 25 | 26 | [dependencies] 27 | quote = "1.0.23" 28 | syn = { version = "1.0.107", features = ["full", "extra-traits"] } 29 | proc-macro2 = "1.0.51" 30 | heck = "0.4.1" 31 | 32 | [features] 33 | default = ["std", "error_trait", "strum/std"] 34 | std = [] 35 | error_trait = [] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT OR Apache-2.0 -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Paho Lurie-Gregg 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Paho Lurie-Gregg 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 | [![crates.io](https://img.shields.io/crates/v/subenum.svg)](https://crates.io/crates/subenum) 2 | [![Build Status](https://github.com/paholg/subenum/actions/workflows/check.yml/badge.svg)](https://github.com/paholg/subenum/actions/workflows/check.yml) 3 | [![docs.rs](https://img.shields.io/docsrs/subenum)](https://docs.rs/subenum) 4 | 5 | # subenum 6 | 7 | Subenum is a simple proc-macro to derive subsets of enums. It allows conversion 8 | between the parent and the child, will derive any traits on the child that you 9 | have on the parent, and will implement `PartialEq` between the parent and child 10 | if you derive it on the parent. 11 | 12 | ## Simple Example 13 | 14 | I think the simplest way to explain it is with an example: 15 | 16 | ```rust 17 | use subenum::subenum; 18 | 19 | #[subenum(Edible)] 20 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 21 | pub enum Plant { 22 | #[subenum(Edible)] 23 | Basil, 24 | #[subenum(Edible)] 25 | Tomato, 26 | Manzanita, 27 | Pine, 28 | } 29 | 30 | fn main() -> Result<(), EdibleConvertError> { 31 | let plant = Plant::Tomato; 32 | 33 | // We can convert between them. 34 | let edible = Edible::try_from(plant)?; 35 | let _plant2 = Plant::from(edible); 36 | 37 | // We can compare them. 38 | assert_eq!(plant, edible); 39 | 40 | // We derive anything that's derived on the parent, such as clone. 41 | let edible2 = edible.clone(); 42 | 43 | Ok(()) 44 | } 45 | ``` 46 | 47 | ## Complex Example 48 | 49 | In addition to simple enums and built-in traits, `subenum` works with complex 50 | enums and third-party attributes. 51 | 52 | ```rust 53 | use subenum::subenum; 54 | 55 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 56 | pub enum AppleType { 57 | CosmicCrisp, 58 | Fuji, 59 | PinkLady, 60 | } 61 | 62 | #[subenum(Foo, Tree, Edible, Grass)] 63 | #[derive(Debug, Clone, Copy, PartialEq, strum::Display)] 64 | pub enum Plant<'a, T> { 65 | #[subenum(Foo)] 66 | #[strum(serialize = "This is not a plant!")] 67 | Foo { x: i32, y: i32 }, 68 | #[subenum(Tree, Edible)] 69 | Apple(AppleType), 70 | #[subenum(Grass)] 71 | Bamboo(&'a str), 72 | #[subenum(Edible)] 73 | Basil(T), 74 | #[subenum(Tree)] 75 | Fir, 76 | #[subenum(Tree)] 77 | Pine, 78 | #[subenum(Edible)] 79 | Tomato, 80 | #[subenum(Edible, Grass)] 81 | Wheat, 82 | } 83 | 84 | fn main() -> Result<(), TreeConvertError> { 85 | let plant: Plant<'_, u32> = Plant::Apple(AppleType::CosmicCrisp); 86 | let tree = Tree::try_from(plant)?; 87 | 88 | assert_eq!(plant, tree); 89 | 90 | let tree2 = tree.clone(); 91 | assert_eq!(tree2.to_string(), "Apple"); 92 | 93 | let foo = Foo::Foo { x: 3, y: 4 }; 94 | assert_eq!(foo.to_string(), "This is not a plant!"); 95 | 96 | let edible = Edible::Basil(3); 97 | let plant = Plant::from(edible); 98 | 99 | assert_eq!(plant.to_string(), "Basil"); 100 | 101 | // Can't compare two subenums. 102 | // assert_ne!(tree2, edible); 103 | 104 | // But we can do some conversion-trickery 105 | assert_ne!(Plant::from(tree2), Plant::from(edible)); 106 | 107 | Ok(()) 108 | } 109 | ``` 110 | 111 | ## Subenum-specific proc-macros 112 | 113 | Maybe you have an enum that can't be `Copy`d, but the subenum can, and you want 114 | to derive it: 115 | 116 | ```rust 117 | use subenum::subenum; 118 | 119 | #[subenum(Bar, Qux(derive(Copy)))] 120 | #[derive(Debug, PartialEq, Eq, Clone)] 121 | pub enum Foo { 122 | #[subenum(Bar)] 123 | A(String), 124 | #[subenum(Qux)] 125 | B, 126 | #[subenum(Bar, Qux)] 127 | C(u8), 128 | } 129 | 130 | fn main() { 131 | let b = Qux::B; 132 | let c = b; 133 | assert_eq!(b, c); 134 | } 135 | ``` 136 | 137 | 138 | # Limitations 139 | 140 | Bound lifetimes (e.g. `for<'a, 'b, 'c>`) are not currently supported. Please 141 | open a ticket if these are desired. 142 | 143 | # Features 144 | - `default` - `std` and `error_trait` 145 | - `std` - Use standard library collections and allocators within this proc macro 146 | - `error_trait` - Implement 147 | [`Error`](https://doc.rust-lang.org/std/error/trait.Error.html) for 148 | `ConvertError` types. 149 | - When combined with nightly and 150 | [`#![feature(error_in_core)]`](https://github.com/rust-lang/rust/issues/103765) 151 | supports `#[no_std]` 152 | - Otherwise, this feature requires `std` as well. 153 | 154 | # License 155 | 156 | Licensed under either of 157 | 158 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 159 | ) 160 | * MIT license 161 | ([LICENSE-MIT](LICENSE-MIT) or ) 162 | 163 | at your option. 164 | -------------------------------------------------------------------------------- /examples/require_docs.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! An example of using subenum with `#![deny(missing_docs)]`. 3 | //! 4 | //! This is an example because the aforementioned attribute doesn't work in 5 | //! tests. 6 | 7 | use subenum::subenum; 8 | 9 | /// An enum. 10 | #[subenum(Bar)] 11 | pub enum Foo { 12 | /// Variant A 13 | #[subenum(Bar)] 14 | A, 15 | /// Variant B 16 | B, 17 | } 18 | 19 | fn main() {} 20 | -------------------------------------------------------------------------------- /src/build.rs: -------------------------------------------------------------------------------- 1 | use alloc::{format, vec::Vec}; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::{format_ident, quote}; 4 | use syn::{punctuated::Punctuated, DeriveInput, Generics, Ident, Token, TypeParamBound, Variant}; 5 | 6 | use crate::{ 7 | derive::{partial_eq::partial_eq_arm, Derive}, 8 | r#enum::Enum, 9 | snake_case, 10 | }; 11 | 12 | // Add a bound to generics 13 | fn add_bound(generics: &mut Generics, bound: TypeParamBound) { 14 | for param in generics.type_params_mut() { 15 | if param.bounds.iter().all(|b| b != &bound) { 16 | param.bounds.push(bound.clone()); 17 | } 18 | } 19 | } 20 | 21 | // Map a variant from an enum definition to how it would be used in a match 22 | // E.g. 23 | // * Foo -> Foo 24 | // * Foo(Bar, Baz) -> Foo(var1, var2) 25 | // * Foo { x: i32, y: i32 } -> Foo { x, y } 26 | fn variant_to_unary_pat(variant: &Variant) -> TokenStream2 { 27 | let ident = &variant.ident; 28 | 29 | match &variant.fields { 30 | syn::Fields::Named(named) => { 31 | let vars: Punctuated = named.named.iter().map(snake_case).collect(); 32 | quote!(#ident{#vars}) 33 | } 34 | syn::Fields::Unnamed(unnamed) => { 35 | let vars: Punctuated = unnamed 36 | .unnamed 37 | .iter() 38 | .enumerate() 39 | .map(|(idx, _)| format_ident!("var{idx}")) 40 | .collect(); 41 | quote!(#ident(#vars)) 42 | } 43 | syn::Fields::Unit => quote!(#ident), 44 | } 45 | } 46 | 47 | impl Enum { 48 | fn build_inherited_derive<'a>( 49 | &self, 50 | parent: &DeriveInput, 51 | derive: Derive, 52 | variants: impl IntoIterator, 53 | ) -> TokenStream2 { 54 | let child_ident = &self.ident; 55 | let parent_ident = &parent.ident; 56 | 57 | let (_child_impl, child_ty, _child_where) = self.generics.split_for_impl(); 58 | 59 | match derive { 60 | Derive::PartialEq => { 61 | let mut generics = parent.generics.clone(); 62 | add_bound(&mut generics, derive.as_bound()); 63 | let (parent_impl, parent_ty, parent_where) = generics.split_for_impl(); 64 | 65 | let arms: Punctuated = variants 66 | .into_iter() 67 | .map(|variant| partial_eq_arm(variant, child_ident, parent_ident)) 68 | .collect(); 69 | 70 | quote!( 71 | #[automatically_derived] 72 | impl #parent_impl PartialEq<#parent_ident #parent_ty> for #child_ident #child_ty #parent_where { 73 | fn eq(&self, other: &#parent_ident #parent_ty) -> bool { 74 | match (self, other) { 75 | #arms, 76 | _ => false, 77 | } 78 | } 79 | 80 | } 81 | 82 | #[automatically_derived] 83 | impl #parent_impl PartialEq<#child_ident #child_ty> for #parent_ident #parent_ty #parent_where { 84 | fn eq(&self, other: &#child_ident #child_ty) -> bool { 85 | match (other, self) { 86 | #arms, 87 | _ => false, 88 | } 89 | } 90 | 91 | } 92 | ) 93 | } 94 | } 95 | } 96 | 97 | pub fn build(&self, parent: &DeriveInput) -> TokenStream2 { 98 | let attributes = self.attributes.clone(); 99 | let child_attrs = parent.attrs.clone(); 100 | let variants = self 101 | .variants 102 | .iter() 103 | .zip(self.variants_attributes.clone()) 104 | .map(|(variant, attribute)| quote! { #(#attribute)* #variant }) 105 | .collect::>(); 106 | let child_generics = self.generics.clone(); 107 | 108 | let child_ident = &self.ident; 109 | let parent_ident = &parent.ident; 110 | 111 | let error = format_ident!("{child_ident}ConvertError"); 112 | 113 | #[cfg(not(feature = "error_trait"))] 114 | let error_trait_impl = quote!(); 115 | #[cfg(all(feature = "error_trait", feature = "std"))] 116 | let error_trait_impl = quote!( 117 | impl std::error::Error for #error {} 118 | ); 119 | #[cfg(all(feature = "error_trait", not(feature = "std")))] 120 | let error_trait_impl = quote!( 121 | impl core::error::Error for #error {} 122 | ); 123 | 124 | let pats: Vec = self.variants.iter().map(variant_to_unary_pat).collect(); 125 | 126 | let from_child_arms = pats 127 | .iter() 128 | .map(|pat| quote!(#child_ident::#pat => #parent_ident::#pat)); 129 | 130 | let try_from_parent_arms = pats 131 | .iter() 132 | .map(|pat| quote!(#parent_ident::#pat => Ok(#child_ident::#pat))); 133 | 134 | let inherited_derives = self 135 | .derives 136 | .iter() 137 | .map(|&derive| self.build_inherited_derive(parent, derive, &self.variants)); 138 | 139 | let vis = &parent.vis; 140 | 141 | let (_child_impl, child_ty, _child_where) = child_generics.split_for_impl(); 142 | 143 | let (parent_impl, parent_ty, parent_where) = parent.generics.split_for_impl(); 144 | 145 | let error_doc = format!( 146 | "An error type used for converting from [`{parent_ident}`] to [`{child_ident}`]." 147 | ); 148 | 149 | quote!( 150 | #(#[ #attributes ])* 151 | #(#child_attrs)* 152 | #vis enum #child_ident #child_generics { 153 | #(#variants),* 154 | } 155 | 156 | #(#inherited_derives)* 157 | 158 | #[doc = #error_doc] 159 | #[derive(Copy, Clone, Debug)] 160 | #vis struct #error; 161 | 162 | impl core::fmt::Display for #error { 163 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 164 | core::fmt::Debug::fmt(self, f) 165 | } 166 | } 167 | 168 | #error_trait_impl 169 | 170 | #[automatically_derived] 171 | impl #parent_impl core::convert::From<#child_ident #child_ty> for #parent_ident #parent_ty #parent_where { 172 | fn from(child: #child_ident #child_ty) -> Self { 173 | match child { 174 | #(#from_child_arms),* 175 | } 176 | } 177 | } 178 | 179 | #[automatically_derived] 180 | impl #parent_impl core::convert::TryFrom<#parent_ident #parent_ty> for #child_ident #child_ty #parent_where { 181 | type Error = #error; 182 | 183 | fn try_from(parent: #parent_ident #parent_ty) -> Result>::Error> { 184 | match parent { 185 | #(#try_from_parent_arms),*, 186 | _ => Err(#error) 187 | } 188 | } 189 | } 190 | ) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/derive.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::{Ident, Path, TraitBound, TraitBoundModifier, TypeParamBound}; 3 | 4 | pub mod partial_eq; 5 | 6 | #[derive(Clone, Copy, Debug, Ord, PartialOrd, PartialEq, Eq)] 7 | pub enum Derive { 8 | PartialEq, 9 | } 10 | 11 | impl Derive { 12 | pub fn as_bound(&self) -> TypeParamBound { 13 | match self { 14 | Derive::PartialEq => TypeParamBound::Trait(TraitBound { 15 | paren_token: None, 16 | modifier: TraitBoundModifier::None, 17 | lifetimes: None, 18 | path: Path::from(Ident::new("PartialEq", Span::call_site())), 19 | }), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/derive/partial_eq.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::{format_ident, quote}; 3 | use syn::{punctuated::Punctuated, Ident, Token, Variant}; 4 | 5 | use crate::snake_case; 6 | 7 | // Map a variant from an enum definition to how it would be used in a match (a, b) 8 | // E.g. 9 | // * Foo -> (Child::Foo, Parent::Foo) => true, 10 | // * Foo(Bar, Baz) -> (Child::Foo(bar, baz), Parent::Foo(bar2, baz2)) => bar == bar2 && baz == baz2, 11 | // * Foo { x: i32, y: i32 } -> (Child::Foo { x, y }, Parent::Foo { x2, y2 }) => x == x2 && y == y2, 12 | pub fn partial_eq_arm( 13 | variant: &Variant, 14 | child_ident: &Ident, 15 | parent_ident: &Ident, 16 | ) -> TokenStream2 { 17 | let ident = &variant.ident; 18 | 19 | match &variant.fields { 20 | syn::Fields::Named(named) => { 21 | let vars1: Punctuated = named.named.iter().map(snake_case).collect(); 22 | let vars2: Punctuated = 23 | vars1.iter().map(|v| format_ident!("{}_b", v)).collect(); 24 | let vars_rhs: Punctuated = vars1 25 | .iter() 26 | .zip(vars2.iter()) 27 | .map(|(var1, var2)| quote!(#var1 == #var2)) 28 | .collect(); 29 | let vars2: Punctuated = vars1 30 | .iter() 31 | .map(|v| { 32 | let v2 = format_ident!("{}_b", v); 33 | quote!(#v: #v2) 34 | }) 35 | .collect(); 36 | quote!((#child_ident::#ident{#vars1}, #parent_ident::#ident{#vars2}) => #vars_rhs) 37 | } 38 | syn::Fields::Unnamed(unnamed) => { 39 | let vars1: Punctuated = unnamed 40 | .unnamed 41 | .iter() 42 | .enumerate() 43 | .map(|(idx, _)| format_ident!("var{idx}")) 44 | .collect(); 45 | let vars2: Punctuated = 46 | vars1.iter().map(|v| format_ident!("{}_b", v)).collect(); 47 | let vars_rhs: Punctuated = vars1 48 | .iter() 49 | .zip(vars2.iter()) 50 | .map(|(var1, var2)| quote!(#var1 == #var2)) 51 | .collect(); 52 | quote!((#child_ident::#ident(#vars1), #parent_ident::#ident(#vars2)) => #vars_rhs) 53 | } 54 | syn::Fields::Unit => quote!((#child_ident::#ident, #parent_ident::#ident) => true), 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/enum.rs: -------------------------------------------------------------------------------- 1 | use alloc::{ 2 | collections::{BTreeMap, BTreeSet}, 3 | vec::Vec, 4 | }; 5 | use proc_macro2::TokenStream; 6 | use syn::{punctuated::Punctuated, Generics, Ident, Token, TypeParamBound, Variant}; 7 | 8 | use crate::{extractor::Extractor, iter::BoxedIter, param::Param, Derive}; 9 | 10 | pub struct Enum { 11 | pub ident: Ident, 12 | pub variants: Punctuated, 13 | pub variants_attributes: Vec>, 14 | pub attributes: Vec, 15 | pub derives: Vec, 16 | pub generics: Generics, 17 | } 18 | 19 | impl Enum { 20 | pub fn new(ident: Ident, attributes: Vec, derives: Vec) -> Self { 21 | Enum { 22 | ident, 23 | variants: Punctuated::new(), 24 | variants_attributes: Vec::new(), 25 | attributes, 26 | derives, 27 | generics: Generics { 28 | lt_token: Some(syn::token::Lt::default()), 29 | params: Punctuated::new(), 30 | gt_token: Some(syn::token::Gt::default()), 31 | where_clause: None, 32 | }, 33 | } 34 | } 35 | 36 | pub fn compute_generics(&mut self, parent_generics: &Generics) { 37 | let generic_bounds: BTreeMap> = parent_generics 38 | .type_params() 39 | .map(|param| { 40 | ( 41 | Param::Ident(param.ident.clone()), 42 | param.bounds.iter().cloned().collect(), 43 | ) 44 | }) 45 | .chain(parent_generics.lifetimes().map(|lifetime_def| { 46 | ( 47 | Param::Lifetime(lifetime_def.lifetime.clone()), 48 | lifetime_def 49 | .bounds 50 | .iter() 51 | .cloned() 52 | .map(TypeParamBound::Lifetime) 53 | .collect(), 54 | ) 55 | })) 56 | .chain( 57 | parent_generics 58 | .where_clause 59 | .iter() 60 | .flat_map(|clause| &clause.predicates) 61 | .flat_map(|pred| match pred { 62 | syn::WherePredicate::Type(ty) => { 63 | // We have to be a bit careful here. Imagine the bound 64 | // >:: Foo 65 | // We need to treat this as a bound on both `T` and on `U`. 66 | let bounds: Vec = ty.bounds.iter().cloned().collect(); 67 | ty.bounded_ty 68 | .extract_idents() 69 | .into_iter() 70 | .map(move |ident| (Param::Ident(ident), bounds.clone())) 71 | .boxed() 72 | } 73 | syn::WherePredicate::Lifetime(lt) => [( 74 | Param::Lifetime(lt.lifetime.clone()), 75 | lt.bounds 76 | .iter() 77 | .cloned() 78 | .map(TypeParamBound::Lifetime) 79 | .collect(), 80 | )] 81 | .into_iter() 82 | .boxed(), 83 | syn::WherePredicate::Eq(_) => { 84 | panic!("Equality predicates in where clauses are unsupported") 85 | } 86 | }), 87 | ) 88 | .collect(); 89 | 90 | // panic!("{generic_bounds:#?}"); 91 | 92 | let types = self 93 | .variants 94 | .iter() 95 | .flat_map(|variant| match &variant.fields { 96 | syn::Fields::Named(named) => named.named.iter().map(|field| &field.ty).collect(), 97 | syn::Fields::Unnamed(unnamed) => { 98 | unnamed.unnamed.iter().map(|field| &field.ty).collect() 99 | } 100 | syn::Fields::Unit => Vec::new(), 101 | }); 102 | // Extract all of the lifetimes and idents we care about from the types. 103 | let params = types.into_iter().flat_map(|ty| ty.extract_params()); 104 | 105 | // The same generic may appear in multiple bounds, so we use a BTreeSet to dedup. 106 | let relevant_params: BTreeSet = params 107 | .flat_map(|param| param.find_relevant(&generic_bounds)) 108 | .collect(); 109 | 110 | self.generics = generics_subset(parent_generics, relevant_params.into_iter()); 111 | } 112 | } 113 | 114 | /// Given a set of `Generics`, return the subset that we're interested in. 115 | /// Expects `params` already includes all possible types/lifetimes we care 116 | // about. 117 | /// E.g. with generics `T: U, U, V`, this function should never be called with 118 | /// just params of `T`; it would instead expect `T, U`. 119 | /// In short: call `find_all_generics` first. 120 | fn generics_subset(generics: &Generics, params: impl Iterator) -> Generics { 121 | let mut new = Generics::default(); 122 | 123 | for param in params { 124 | let (generic_param, predicate) = param.find(generics); 125 | if let Some(gp) = generic_param { 126 | new.params.push(gp.clone()); 127 | } 128 | if let Some(pred) = predicate { 129 | new.make_where_clause().predicates.push(pred.clone()); 130 | } 131 | } 132 | 133 | new 134 | } 135 | -------------------------------------------------------------------------------- /src/extractor.rs: -------------------------------------------------------------------------------- 1 | use alloc::{borrow::ToOwned, boxed::Box, vec::Vec}; 2 | use syn::{Ident, Lifetime, Type, TypeParamBound}; 3 | 4 | use crate::{iter::BoxedIter, param::Param}; 5 | 6 | pub trait Extractor { 7 | fn extract_lifetimes(&self) -> Vec; 8 | fn extract_idents(&self) -> Vec; 9 | fn extract_params(&self) -> Box> { 10 | self.extract_lifetimes() 11 | .into_iter() 12 | .map(Param::Lifetime) 13 | .chain(self.extract_idents().into_iter().map(Param::Ident)) 14 | .boxed() 15 | } 16 | } 17 | 18 | impl Extractor for Type { 19 | fn extract_lifetimes(&self) -> Vec { 20 | match self { 21 | Type::Array(a) => a.elem.extract_lifetimes(), 22 | Type::BareFn(_) => Vec::new(), 23 | Type::Group(g) => g.elem.extract_lifetimes(), 24 | Type::ImplTrait(it) => it 25 | .bounds 26 | .iter() 27 | .cloned() 28 | .filter_map(|b| match b { 29 | TypeParamBound::Trait(_) => None, 30 | TypeParamBound::Lifetime(lt) => Some(lt), 31 | }) 32 | .collect(), 33 | Type::Infer(_) => Vec::new(), 34 | Type::Macro(_) => Vec::new(), 35 | Type::Never(_) => Vec::new(), 36 | Type::Paren(p) => p.elem.extract_lifetimes(), 37 | Type::Path(_) => Vec::new(), 38 | Type::Ptr(p) => p.elem.extract_lifetimes(), 39 | Type::Reference(r) => r 40 | .lifetime 41 | .iter() 42 | .cloned() 43 | .chain(r.elem.extract_lifetimes()) 44 | .collect(), 45 | Type::Slice(s) => s.elem.extract_lifetimes(), 46 | Type::TraitObject(to) => to 47 | .bounds 48 | .iter() 49 | .cloned() 50 | .filter_map(|b| match b { 51 | TypeParamBound::Trait(_) => None, 52 | TypeParamBound::Lifetime(lt) => Some(lt), 53 | }) 54 | .collect(), 55 | 56 | Type::Tuple(t) => t.elems.iter().flat_map(Self::extract_lifetimes).collect(), 57 | Type::Verbatim(_) => Vec::new(), 58 | #[allow(unknown_lints)] 59 | #[cfg_attr(test, deny(non_exhaustive_omitted_patterns))] 60 | _ => Vec::new(), 61 | } 62 | } 63 | 64 | fn extract_idents(&self) -> Vec { 65 | match self { 66 | Type::Array(a) => a.elem.extract_idents(), 67 | Type::BareFn(_) => Vec::new(), 68 | Type::Group(g) => g.elem.extract_idents(), 69 | Type::ImplTrait(it) => it 70 | .bounds 71 | .iter() 72 | .cloned() 73 | .filter_map(|b| match b { 74 | TypeParamBound::Trait(t) => t.path.get_ident().map(ToOwned::to_owned), 75 | TypeParamBound::Lifetime(_) => None, 76 | }) 77 | .collect(), 78 | Type::Infer(_) => Vec::new(), 79 | Type::Macro(_) => Vec::new(), 80 | Type::Never(_) => Vec::new(), 81 | Type::Paren(p) => p.elem.extract_idents(), 82 | Type::Path(p) => p 83 | .path 84 | .get_ident() 85 | .map(ToOwned::to_owned) 86 | .into_iter() 87 | .collect(), 88 | Type::Ptr(p) => p.elem.extract_idents(), 89 | Type::Reference(r) => r.elem.extract_idents(), 90 | Type::Slice(s) => s.elem.extract_idents(), 91 | Type::TraitObject(_) => Vec::new(), 92 | Type::Tuple(t) => t.elems.iter().flat_map(Self::extract_idents).collect(), 93 | Type::Verbatim(_) => Vec::new(), 94 | #[allow(unknown_lints)] 95 | #[cfg_attr(test, deny(non_exhaustive_omitted_patterns))] 96 | _ => Vec::new(), 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/iter.rs: -------------------------------------------------------------------------------- 1 | use alloc::boxed::Box; 2 | 3 | pub trait BoxedIter { 4 | type Item; 5 | fn boxed(self) -> Box>; 6 | } 7 | 8 | impl + 'static, T> BoxedIter for I { 9 | type Item = T; 10 | fn boxed(self) -> Box> { 11 | Box::new(self) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_doctest_main)] 2 | #![doc = include_str!("../README.md")] 3 | #![no_std] 4 | extern crate alloc; 5 | 6 | mod build; 7 | mod derive; 8 | mod r#enum; 9 | mod extractor; 10 | mod iter; 11 | mod param; 12 | 13 | use alloc::{borrow::ToOwned, collections::BTreeMap, string::ToString, vec::Vec}; 14 | 15 | use derive::Derive; 16 | use heck::ToSnakeCase; 17 | use proc_macro::TokenStream; 18 | use proc_macro2::Ident; 19 | use quote::quote; 20 | use r#enum::Enum; 21 | use syn::{ 22 | parse_macro_input, Attribute, AttributeArgs, DeriveInput, Field, Meta, MetaList, MetaNameValue, 23 | NestedMeta, Type, 24 | }; 25 | 26 | const SUBENUM: &str = "subenum"; 27 | const ERR: &str = 28 | "subenum must be called with a list of identifiers, like `#[subenum(EnumA, EnumB(derive(Clone)))]`"; 29 | 30 | fn snake_case(field: &Field) -> Ident { 31 | let ident = field.ident.as_ref().unwrap_or_else(|| { 32 | // No ident; the Type must be Path. Use that. 33 | match &field.ty { 34 | Type::Path(path) => path.path.get_ident().unwrap(), 35 | _ => unimplemented!("a"), 36 | } 37 | }); 38 | Ident::new(&ident.to_string().to_snake_case(), ident.span()) 39 | } 40 | 41 | /// Remove our #[subenum(...)] attributes from the input. 42 | fn sanitize_input(input: &mut DeriveInput) { 43 | let data = match input.data { 44 | syn::Data::Enum(ref mut data) => data, 45 | _ => panic!("SubEnum may only be used on enums."), 46 | }; 47 | 48 | for variant in data.variants.iter_mut() { 49 | // TODO: Switch to Vec::drain_filter once stabilized. 50 | let mut i = 0; 51 | while i < variant.attrs.len() { 52 | if variant.attrs[i].path.is_ident(SUBENUM) { 53 | variant.attrs.remove(i); 54 | } else { 55 | i += 1; 56 | } 57 | } 58 | } 59 | } 60 | 61 | fn attribute_paths(attr: &Attribute) -> impl Iterator { 62 | let meta = attr.parse_meta().unwrap(); 63 | let nested = match meta { 64 | Meta::List(list) => list.nested, 65 | _ => unimplemented!("b"), 66 | }; 67 | nested.into_iter().map(|nested| match nested { 68 | NestedMeta::Meta(meta) => meta, 69 | _ => unimplemented!("c"), 70 | }) 71 | } 72 | 73 | fn build_enum_map(args: AttributeArgs, derives: &[Derive]) -> BTreeMap { 74 | args.into_iter() 75 | .map(|nested| match nested { 76 | NestedMeta::Meta(meta) => meta, 77 | NestedMeta::Lit(_) => panic!("{}", ERR), 78 | }) 79 | .map(|meta| match meta { 80 | Meta::Path(path) => (path.get_ident().expect(ERR).to_owned(), Vec::new()), 81 | Meta::List(MetaList { path, nested, .. }) => ( 82 | path.get_ident().expect(ERR).to_owned(), 83 | nested 84 | .into_iter() 85 | .map(|nested| match nested { 86 | NestedMeta::Meta(meta) => meta, 87 | NestedMeta::Lit(_) => panic!("{}", ERR), 88 | }) 89 | .map(|meta| match meta { 90 | Meta::Path(path) => quote! { #path }, 91 | Meta::List(MetaList { path, nested, .. }) => quote! { #path(#nested) }, 92 | Meta::NameValue(MetaNameValue { path, lit, .. }) => quote! { #path = #lit }, 93 | }) 94 | .collect::>(), 95 | ), 96 | _ => panic!("{}", ERR), 97 | }) 98 | .map(|(ident, attrs)| { 99 | ( 100 | ident.clone(), 101 | Enum::new(ident.clone(), attrs, derives.to_owned()), 102 | ) 103 | }) 104 | .collect() 105 | } 106 | 107 | #[proc_macro_attribute] 108 | pub fn subenum(args: TokenStream, tokens: TokenStream) -> TokenStream { 109 | let args = parse_macro_input!(args as AttributeArgs); 110 | let mut input = parse_macro_input!(tokens as DeriveInput); 111 | let data = match input.data { 112 | syn::Data::Enum(ref data) => data, 113 | _ => panic!("subenum may only be used on enums."), 114 | }; 115 | 116 | let mut derives = Vec::new(); 117 | for attr in &input.attrs { 118 | if attr.path.is_ident("derive") { 119 | for meta in attribute_paths(attr) { 120 | match meta { 121 | Meta::Path(path) => { 122 | if path.is_ident("PartialEq") { 123 | derives.push(Derive::PartialEq); 124 | } 125 | } 126 | _ => unimplemented!("{:?}", meta), 127 | } 128 | } 129 | } 130 | } 131 | let mut enums = build_enum_map(args, &derives); 132 | 133 | for variant in &data.variants { 134 | for attribute in &variant.attrs { 135 | // Check for "subenum", iterate through the idents. 136 | if attribute.path.is_ident(SUBENUM) { 137 | for meta in attribute_paths(attribute) { 138 | let mut var = variant.clone(); 139 | 140 | let (ident, attrs) = match meta { 141 | Meta::Path(ref path) => (path.get_ident().unwrap(), Vec::new()), 142 | Meta::List(MetaList { 143 | ref path, nested, .. 144 | }) => ( 145 | path.get_ident().unwrap(), 146 | nested 147 | .into_iter() 148 | .map(|nested| match nested { 149 | NestedMeta::Meta(meta) => meta, 150 | NestedMeta::Lit(_) => panic!("{}", ERR), 151 | }) 152 | .map(|meta| match meta { 153 | Meta::Path(path) => quote! { #[ #path ] }, 154 | Meta::List(MetaList { path, nested, .. }) => { 155 | quote! { #[ #path(#nested) ] } 156 | } 157 | Meta::NameValue(MetaNameValue { path, lit, .. }) => { 158 | quote! { #[ #path = #lit ] } 159 | } 160 | }) 161 | .collect::>(), 162 | ), 163 | _ => unimplemented!("e"), 164 | }; 165 | 166 | // We want all attributes except the "subenum" one. 167 | var.attrs.retain(|attr| attribute != attr); 168 | 169 | let e = enums 170 | .get_mut(ident) 171 | .expect("All enums to be created must be declared at the top-level subenum attribute"); 172 | e.variants.push(var); 173 | e.variants_attributes.push(attrs); 174 | } 175 | } 176 | } 177 | } 178 | 179 | for e in enums.values_mut() { 180 | e.compute_generics(&input.generics); 181 | } 182 | 183 | let enums: Vec<_> = enums.into_values().map(|e| e.build(&input)).collect(); 184 | 185 | sanitize_input(&mut input); 186 | 187 | quote!( 188 | #input 189 | 190 | #(#enums)* 191 | ) 192 | .into() 193 | } 194 | -------------------------------------------------------------------------------- /src/param.rs: -------------------------------------------------------------------------------- 1 | use alloc::{collections::BTreeMap, vec::Vec}; 2 | use syn::{GenericParam, Generics, Ident, Lifetime, TypeParamBound, WherePredicate}; 3 | 4 | use crate::extractor::Extractor; 5 | 6 | /// A type or lifetime param, potentially used as a generic. 7 | /// E.g. the 'a in `'a: 'b + 'c` or the T in `T: U + V`. 8 | #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] 9 | pub enum Param { 10 | Lifetime(Lifetime), 11 | Ident(Ident), 12 | } 13 | 14 | impl From for Param { 15 | fn from(value: Ident) -> Self { 16 | Param::Ident(value) 17 | } 18 | } 19 | 20 | impl From for Param { 21 | fn from(value: Lifetime) -> Self { 22 | Param::Lifetime(value) 23 | } 24 | } 25 | 26 | impl Param { 27 | /// Given a set of `Generics`, finds the first `GenericParam` and 28 | /// `WherePredicate` where it appears. 29 | pub fn find<'b>( 30 | &self, 31 | generics: &'b Generics, 32 | ) -> (Option<&'b GenericParam>, Option<&'b WherePredicate>) { 33 | match self { 34 | Param::Lifetime(lt) => find_lt(lt, generics), 35 | Param::Ident(ty) => find_ident(ty, generics), 36 | } 37 | } 38 | 39 | /// Given a param and a map of bounds, will find all params that we may 40 | /// need. 41 | /// Example: 42 | /// Given `T` and bounds `T: U, U: V, V: W + X, W, X, Y: Z, Z` 43 | /// Will return `T, U, V, W, X`. 44 | pub fn find_relevant(&self, bound_map: &BTreeMap>) -> Vec { 45 | match bound_map.get(self) { 46 | Some(bounds) => bounds 47 | .iter() 48 | .flat_map(|bound| match bound { 49 | TypeParamBound::Trait(tr) => { 50 | // TODO: Handle BoundLifetimes (`for<'a, 'b, 'c>`) 51 | tr.path 52 | .get_ident() 53 | .into_iter() 54 | .cloned() 55 | .flat_map(|ident| Param::from(ident).find_relevant(bound_map)) 56 | .collect() 57 | } 58 | TypeParamBound::Lifetime(lifetime) => { 59 | Param::from(lifetime.clone()).find_relevant(bound_map) 60 | } 61 | }) 62 | .chain([self.clone()]) 63 | .collect(), 64 | None => Vec::new(), 65 | } 66 | } 67 | } 68 | fn find_lt<'a>( 69 | lt: &Lifetime, 70 | generics: &'a Generics, 71 | ) -> (Option<&'a GenericParam>, Option<&'a WherePredicate>) { 72 | let generic_param = generics.params.iter().find(|p| match p { 73 | GenericParam::Lifetime(lt_def) => <_def.lifetime == lt, 74 | _ => false, 75 | }); 76 | 77 | let predicate = generics 78 | .where_clause 79 | .iter() 80 | .flat_map(|wh| wh.predicates.iter()) 81 | .find(|pred| match pred { 82 | WherePredicate::Lifetime(pred_lt) => &pred_lt.lifetime == lt, 83 | _ => false, 84 | }); 85 | 86 | (generic_param, predicate) 87 | } 88 | 89 | fn find_ident<'a>( 90 | ident: &Ident, 91 | generics: &'a Generics, 92 | ) -> (Option<&'a GenericParam>, Option<&'a WherePredicate>) { 93 | let generic_param = generics.params.iter().find(|p| match p { 94 | GenericParam::Type(ty_param) => &ty_param.ident == ident, 95 | _ => false, 96 | }); 97 | 98 | let predicate = generics 99 | .where_clause 100 | .iter() 101 | .flat_map(|wh| wh.predicates.iter()) 102 | .find(|pred| match pred { 103 | WherePredicate::Type(pred_ty) => pred_ty 104 | .bounded_ty 105 | .extract_idents() 106 | .iter() 107 | .any(|id| id == ident), 108 | _ => false, 109 | }); 110 | 111 | (generic_param, predicate) 112 | } 113 | -------------------------------------------------------------------------------- /tests/it.rs: -------------------------------------------------------------------------------- 1 | use subenum::subenum; 2 | 3 | #[subenum(Bar)] 4 | #[derive(Clone, Debug, PartialEq, Eq)] 5 | enum Foo { 6 | #[subenum(Bar)] 7 | A(String, String, String, i32, i32, i32, u32, i32), 8 | #[subenum(Bar)] 9 | B { 10 | x: i32, 11 | y: i32, 12 | z: String, 13 | w: String, 14 | }, 15 | } 16 | 17 | #[subenum(Both, Str, Tee, Neither)] 18 | #[derive(Clone, Debug)] 19 | enum Pippy<'a, T> { 20 | #[subenum(Both)] 21 | A(&'a str, T), 22 | #[subenum(Both, Str)] 23 | B { a: &'a str }, 24 | #[subenum(Both, Tee)] 25 | C { a: T }, 26 | #[subenum(Neither, Tee)] 27 | D, 28 | #[subenum(Neither, Str)] 29 | E(String), 30 | #[subenum(Neither, Both)] 31 | F { a: u32 }, 32 | } 33 | 34 | #[subenum(Flip, Flop)] 35 | #[derive(Clone, Debug, PartialEq, Eq)] 36 | enum Flippy 37 | where 38 | T: Clone, 39 | { 40 | #[subenum(Flip)] 41 | A(T), 42 | #[subenum(Flop)] 43 | B(U), 44 | } 45 | 46 | #[subenum(Floop)] 47 | #[derive(Clone, Debug, PartialEq)] 48 | enum Snoo { 49 | #[subenum(Floop)] 50 | A(T), 51 | } 52 | 53 | #[test] 54 | fn test_snoo() { 55 | let a: Snoo = Snoo::A(3); 56 | let b: Floop = a.clone().try_into().unwrap(); 57 | 58 | assert_eq!(a, b); 59 | } 60 | 61 | #[subenum(Boop)] 62 | #[derive(Clone, Debug, PartialEq, Eq)] 63 | enum Boo<'a> { 64 | #[subenum(Boop)] 65 | A(&'a str), 66 | } 67 | 68 | #[subenum(Phew)] 69 | enum Whew<'a: 'b, 'b, 'c, T, U> { 70 | #[subenum(Phew)] 71 | A(&'a T), 72 | #[subenum(Phew)] 73 | B(&'b [&'c [U; 7]]), 74 | } 75 | 76 | #[subenum(SubEnumWithErrorVariant)] 77 | #[derive(Debug, Clone, Copy, PartialEq)] 78 | enum EnumWithErrorVariant { 79 | #[subenum(SubEnumWithErrorVariant)] 80 | Error, 81 | } 82 | 83 | #[test] 84 | fn test_enum_with_error_variant() { 85 | let a = EnumWithErrorVariant::Error; 86 | let b = SubEnumWithErrorVariant::try_from(a).unwrap(); 87 | 88 | assert_eq!(a, b); 89 | } 90 | -------------------------------------------------------------------------------- /tests/subenum_specific.rs: -------------------------------------------------------------------------------- 1 | use subenum::subenum; 2 | 3 | #[subenum( 4 | Binary(derive(derive_more::Display)), 5 | Unary, 6 | Keyword(derive(Copy, strum::EnumString), strum(serialize_all = "snake_case")) 7 | )] 8 | #[derive(Clone, Debug, PartialEq)] 9 | enum Token { 10 | #[subenum(Binary(display(fmt = "-")), Unary)] 11 | Minus, 12 | #[subenum(Binary(display(fmt = "+")))] 13 | Plus, 14 | #[subenum(Keyword)] 15 | And, 16 | #[subenum(Keyword)] 17 | Or, 18 | #[subenum(Keyword)] 19 | Var, 20 | } 21 | 22 | #[test] 23 | fn test_token() { 24 | let a = Token::Minus; 25 | let b = Binary::try_from(a.clone()).unwrap(); 26 | println!("b: {}", b); 27 | 28 | let c = "and".parse::().unwrap(); 29 | let d = Token::from(c); 30 | println!("{:?} {:?}", c, d); 31 | 32 | assert_eq!(a, b); 33 | } 34 | 35 | #[subenum(EnumB)] 36 | enum EnumA { 37 | #[subenum(EnumB)] 38 | B, 39 | #[subenum(EnumB)] 40 | C(T), 41 | } 42 | --------------------------------------------------------------------------------