├── .github └── workflows │ ├── audit.yml │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── kinded ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── errors.rs │ ├── lib.rs │ └── traits.rs ├── kinded_macros ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── gen │ ├── kind_enum.rs │ ├── main_enum.rs │ └── mod.rs │ ├── lib.rs │ ├── mod.rs │ ├── models.rs │ └── parse.rs ├── sandbox ├── .gitignore ├── Cargo.toml └── src │ └── main.rs └── test_suite ├── Cargo.toml └── src └── lib.rs /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | jobs: 8 | security_audit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions-rs/audit-check@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | unit_tests: 11 | name: Unit Tests 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Install Rust 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | 22 | - name: cargo test --all-features 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: test 26 | args: --all-features 27 | 28 | rustfmt: 29 | name: Rustfmt 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | - name: Install Rust 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: stable 39 | components: rustfmt 40 | 41 | - name: Check formatting 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: fmt 45 | args: -- --check 46 | 47 | clippy: 48 | name: Clippy 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout repository 52 | uses: actions/checkout@v2 53 | 54 | - name: Install Rust 55 | uses: actions-rs/toolchain@v1 56 | with: 57 | toolchain: stable 58 | components: clippy 59 | 60 | - name: Clippy Check 61 | uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.3.0 - 2023-08-09 2 | * Make `::all()` function return an array instead of vector. 3 | 4 | ## v0.2.0 - 2023-08-06 5 | * Add `Kind` trait. 6 | 7 | ## v0.1.1 - 2023-08-06 8 | * Add `::all()` to the kind type to iterate over all kind variants 9 | * Generate customizable implementation of `Display` trait 10 | * Generate implementation of `FromStr` trait 11 | 12 | ## v0.0.3 - 2023-08-05 13 | * Make generated `kind()` function public 14 | 15 | ## v0.0.2 - 2023-08-05 16 | * Support enums with generics 17 | 18 | ## v0.0.1 - 2023-08-04 19 | * Very initial release 20 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "convert_case" 7 | version = "0.6.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 10 | dependencies = [ 11 | "unicode-segmentation", 12 | ] 13 | 14 | [[package]] 15 | name = "kinded" 16 | version = "0.3.0" 17 | dependencies = [ 18 | "kinded_macros", 19 | ] 20 | 21 | [[package]] 22 | name = "kinded_macros" 23 | version = "0.3.0" 24 | dependencies = [ 25 | "convert_case", 26 | "proc-macro2", 27 | "quote", 28 | "syn", 29 | ] 30 | 31 | [[package]] 32 | name = "proc-macro2" 33 | version = "1.0.66" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 36 | dependencies = [ 37 | "unicode-ident", 38 | ] 39 | 40 | [[package]] 41 | name = "quote" 42 | version = "1.0.32" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 45 | dependencies = [ 46 | "proc-macro2", 47 | ] 48 | 49 | [[package]] 50 | name = "sandbox" 51 | version = "0.0.1" 52 | dependencies = [ 53 | "kinded", 54 | ] 55 | 56 | [[package]] 57 | name = "syn" 58 | version = "2.0.28" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 61 | dependencies = [ 62 | "proc-macro2", 63 | "quote", 64 | "unicode-ident", 65 | ] 66 | 67 | [[package]] 68 | name = "test_suite" 69 | version = "0.1.0" 70 | dependencies = [ 71 | "kinded", 72 | ] 73 | 74 | [[package]] 75 | name = "unicode-ident" 76 | version = "1.0.11" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 79 | 80 | [[package]] 81 | name = "unicode-segmentation" 82 | version = "1.10.1" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 85 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | resolver = "2" 4 | 5 | members = [ 6 | "kinded", 7 | "kinded_macros", 8 | "sandbox", 9 | "test_suite", 10 | ] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Kinded

2 |

3 | Nutype Build Status 4 | Nutype Documentation 5 | 6 |

7 | 8 |

Generate Rust enum variants without associated data

9 | 10 | 11 | ## Get Started 12 | 13 | ```rs 14 | use kinded::Kinded; 15 | 16 | #[derive(Kinded)] 17 | enum Drink { 18 | Mate, 19 | Coffee(String), 20 | Tea { variety: String, caffeine: bool } 21 | } 22 | 23 | let drink = Drink::Coffee("Espresso".to_owned()); 24 | assert_eq!(drink.kind(), DrinkKind::Coffee); 25 | ``` 26 | 27 | Note, the definition of `DrinkKind` enum is generated automatically as well as `Drink::kind()` method. 28 | To put it simply you get something similar to the following: 29 | 30 | ```rs 31 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 32 | enum DrinkKind { 33 | Mate, 34 | Coffee, 35 | Tea 36 | } 37 | 38 | impl Drink { 39 | fn kind(&self) -> DrinkKind { 40 | Drink::Mate => DrinkKind::Mate, 41 | Drink::Coffee(..) => DrinkKind::Coffee, 42 | Drink::Tea { .. } => DrinkKind::Tea, 43 | } 44 | } 45 | ``` 46 | 47 | ## Kinded trait 48 | 49 | The library provides `Kinded` trait: 50 | 51 | ```rs 52 | pub trait Kinded { 53 | type Kind: PartialEq + Eq + Debug + Clone + Copy; 54 | 55 | fn kind(&self) -> Self::Kind; 56 | } 57 | ``` 58 | 59 | From the example above, the derived implementation of `Kinded` for `Drink` resembles the following: 60 | 61 | ```rs 62 | impl Kinded for Drink { 63 | type Kind = DrinkKind; 64 | 65 | fn kind(&self) -> DrinkKind { /* implementation */ } 66 | } 67 | ``` 68 | 69 | The `Kinded` trait allows to build abstract functions that can be used with different enum types. 70 | 71 | ## Get all kind variants 72 | 73 | The kind type gets implementation of `::all()` associated function, which returns a vector with all kind variants: 74 | 75 | ```rs 76 | assert_eq!(DrinkKind::all(), [DrinkKind::Mate, DrinkKind::Coffee, DrinkKind::Tea]); 77 | ``` 78 | 79 | 80 | ## Attributes 81 | 82 | ### Custom kind type name 83 | 84 | By default the kind type name is generated by adding postfix `Kind` to the original enum name. 85 | This can be customized with `kind = ` attribute: 86 | 87 | ```rs 88 | use kinded::Kinded; 89 | 90 | #[derive(Kinded)] 91 | #[kinded(kind = SimpleDrink)] 92 | enum Drink { 93 | Mate, 94 | Coffee(String), 95 | Tea { variety: String, caffeine: bool } 96 | } 97 | ``` 98 | 99 | ### Derive traits 100 | 101 | By default the kind type implements the following traits: `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Display`, `FromStr`, `From`, `From<&T>`. 102 | 103 | Extra traits can be derived with `derive(..)` attribute: 104 | 105 | ```rs 106 | use kinded::Kinded; 107 | use std::collections::HashSet; 108 | 109 | #[derive(Kinded)] 110 | #[kinded(derive(Hash))] 111 | enum Drink { 112 | Mate, 113 | Coffee(String), 114 | Tea { variety: String, caffeine: bool } 115 | } 116 | 117 | let mut drink_kinds = HashSet::new(); 118 | drink_kinds.insert(DrinkKind::Mate); 119 | ``` 120 | 121 | ### Display trait 122 | 123 | Implementation of `Display` trait can be customized in the `serde` fashion: 124 | 125 | ```rs 126 | use kinded::Kinded; 127 | 128 | #[derive(Kinded)] 129 | #[kinded(display = "snake_case")] 130 | enum Drink { 131 | VeryHotBlackTea, 132 | Milk { fat: f64 }, 133 | } 134 | 135 | let tea = DrinkKind::VeryHotBlackTea; 136 | assert_eq!(tea.to_string(), "very_hot_black_tea"); 137 | ``` 138 | 139 | The possible values are `"snake_case"`, `"camelCase"`, `"PascalCase"`, `"SCREAMING_SNAKE_CASE"`, `"kebab-case"`, `"SCREAMING-KEBAB-CASE"`, `"Title Case"`, `"lowercase"`, `"UPPERCASE"`. 140 | 141 | ### FromStr trait 142 | 143 | The kind type implements `FromStr` trait. The implementation tries it's best to parse, checking all the possible cases mentioned above. 144 | 145 | ```rs 146 | use kinded::Kinded; 147 | 148 | #[derive(Kinded)] 149 | #[kinded(display = "snake_case")] 150 | enum Drink { 151 | VeryHotBlackTea, 152 | Milk { fat: f64 }, 153 | } 154 | 155 | assert_eq!( 156 | "VERY_HOT_BLACK_TEA".parse::().unwrap(), 157 | DrinkKind::VeryHotBlackTea 158 | ); 159 | 160 | assert_eq!( 161 | "veryhotblacktea".parse::().unwrap(), 162 | DrinkKind::VeryHotBlackTea 163 | ); 164 | ``` 165 | 166 | 167 | ## A note about enum-kinds 168 | 169 | There is a very similar crate [enum-kinds](https://github.com/Soft/enum-kinds) that does almost the same job. 170 | 171 | Here is what makes `kinded` different: 172 | * It provides `Kinded` trait, on top of which users can build abstractions. 173 | * Generates customizable implementation of `Display` trait. 174 | * Generates implementation of `FromStr` trait. 175 | * Generates `kind()` function to extra ergonomics. 176 | 177 | ## A note about the war in Ukraine 🇺🇦 178 | 179 | Today I live in Berlin, I have the luxury to live a physically safe life. 180 | But I am Ukrainian. The first 25 years of my life I spent in [Kharkiv](https://en.wikipedia.org/wiki/Kharkiv), 181 | the second-largest city in Ukraine, 60km away from the border with russia. Today about [a third of my home city is destroyed](https://www.youtube.com/watch?v=ihoufBFSZds) by russians. 182 | My parents, my relatives and my friends had to survive the artillery and air attack, living for over a month in basements. 183 | 184 | Some of them have managed to evacuate to EU. Some others are trying to live "normal lifes" in Kharkiv, doing there daily duties. 185 | And some are at the front line right now, risking their lives every second to protect the rest. 186 | 187 | I encourage you to donate to [Charity foundation of Serhiy Prytula](https://prytulafoundation.org/en). 188 | Just pick the project you like and donate. This is one of the best-known foundations, you can watch a [little documentary](https://www.youtube.com/watch?v=VlmWqoeub1Q) about it. 189 | Your contribution to the Ukrainian military force is a contribution to my calmness, so I can spend more time developing the project. 190 | 191 | Thank you. 192 | 193 | 194 | ## License 195 | 196 | MIT © [Serhii Potapov](https://www.greyblake.com) 197 | -------------------------------------------------------------------------------- /kinded/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /kinded/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kinded" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Serhii Potapov "] 6 | 7 | description = "Generate enums with same variants, but without data." 8 | keywords = ["enum", "macros", "kind", "derive"] 9 | license = "MIT" 10 | repository = "https://github.com/greyblake/kinded" 11 | homepage = "https://github.com/greyblake/kinded" 12 | documentation = "https://docs.rs/kinded" 13 | readme = "README.md" 14 | categories = ["data-structures", "rust-patterns"] 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | kinded_macros = { version = "0.3.0", path = "../kinded_macros" } 20 | -------------------------------------------------------------------------------- /kinded/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /kinded/src/errors.rs: -------------------------------------------------------------------------------- 1 | /// An error which is returned when parsing of a kind type failures. 2 | pub struct ParseKindError { 3 | kind_type_name: String, 4 | given_string: String, 5 | } 6 | 7 | impl ParseKindError { 8 | /// This method is used by `kinded` macro to construct an error for FromStr trait and is not 9 | /// recommend for a direct usage by users. 10 | pub fn from_type_and_string(given_string: String) -> ParseKindError { 11 | let full_kind_type_name = std::any::type_name::(); 12 | let kind_type_name = full_kind_type_name 13 | .split("::") 14 | .last() 15 | .expect("Type name cannot be empty") 16 | .to_string(); 17 | ParseKindError { 18 | kind_type_name, 19 | given_string, 20 | } 21 | } 22 | } 23 | 24 | impl ::core::fmt::Display for ParseKindError { 25 | fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { 26 | let Self { 27 | kind_type_name, 28 | given_string, 29 | } = self; 30 | write!(f, r#"Failed to parse "{given_string}" as {kind_type_name}"#) 31 | } 32 | } 33 | 34 | impl ::core::fmt::Debug for ParseKindError { 35 | fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> Result<(), ::core::fmt::Error> { 36 | write!(f, "ParseKindError: {self}") 37 | } 38 | } 39 | 40 | impl ::std::error::Error for ParseKindError { 41 | fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> { 42 | None 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kinded/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Kinded 2 | //! 3 | //! Generate Rust enum kind types without boilerplate. 4 | //! 5 | //! ## Get Started 6 | //! 7 | //! ``` 8 | //! use kinded::Kinded; 9 | //! 10 | //! #[derive(Kinded)] 11 | //! enum Drink { 12 | //! Mate, 13 | //! Coffee(String), 14 | //! Tea { variety: String, caffeine: bool } 15 | //! } 16 | //! 17 | //! let drink = Drink::Coffee("Espresso".to_owned()); 18 | //! assert_eq!(drink.kind(), DrinkKind::Coffee); 19 | //! ``` 20 | //! 21 | //! Note, the definition of `DrinkKind` enum is generated automatically as well as `Drink::kind()` method. 22 | //! To put it simply you get something similar to the following: 23 | //! 24 | //! ```ignore 25 | //! #[derive(Debug, Clone, Copy, PartialEq, Eq)] 26 | //! enum DrinkKind { 27 | //! Mate, 28 | //! Coffee, 29 | //! Tea 30 | //! } 31 | //! 32 | //! impl Drink { 33 | //! fn kind(&self) -> DrinkKind { 34 | //! Drink::Mate => DrinkKind::Mate, 35 | //! Drink::Coffee(..) => DrinkKind::Coffee, 36 | //! Drink::Tea { .. } => DrinkKind::Tea, 37 | //! } 38 | //! } 39 | //! 40 | //! ## Kinded trait 41 | //! 42 | //! The library provides `Kinded` trait: 43 | //! 44 | //! ```rs 45 | //! pub trait Kinded { 46 | //! type Kind: PartialEq + Eq + Debug + Clone + Copy; 47 | //! 48 | //! fn kind(&self) -> Self::Kind; 49 | //! } 50 | //! ``` 51 | //! 52 | //! From the example above, the derived implementation of `Kinded` for `Drink` resembles the following: 53 | //! 54 | //! ```ignore 55 | //! impl Kinded for Drink { 56 | //! type Kind = DrinkKind; 57 | //! 58 | //! fn kind(&self) -> DrinkKind { /* implementation */ } 59 | //! } 60 | //! ``` 61 | //! 62 | //! The `Kinded` trait allows to build abstract functions that can be used with different enum types. 63 | //! 64 | //! ## Get all kind variants 65 | //! 66 | //! The kind type gets implementation of `::all()` associated function, which returns a vector with all kind variants: 67 | //! 68 | //! ``` 69 | //! use kinded::Kinded; 70 | //! 71 | //! #[derive(Kinded)] 72 | //! enum Drink { 73 | //! Mate, 74 | //! Coffee(String), 75 | //! Tea { variety: String, caffeine: bool } 76 | //! } 77 | //! 78 | //! assert_eq!(DrinkKind::all(), [DrinkKind::Mate, DrinkKind::Coffee, DrinkKind::Tea]); 79 | //! ``` 80 | //! 81 | //! ## Attributes 82 | //! 83 | //! ### Custom kind type name 84 | //! 85 | //! By default the kind type name is generated by adding postfix `Kind` to the original enum name. 86 | //! This can be customized with `kind = ` attribute: 87 | //! 88 | //! ``` 89 | //! use kinded::Kinded; 90 | //! 91 | //! #[derive(Kinded)] 92 | //! #[kinded(kind = SimpleDrink)] 93 | //! enum Drink { 94 | //! Mate, 95 | //! Coffee(String), 96 | //! Tea { variety: String, caffeine: bool } 97 | //! } 98 | //! 99 | //! assert_eq!(Drink::Mate.kind(), SimpleDrink::Mate); 100 | //! ``` 101 | //! 102 | //! ### Derive traits 103 | //! 104 | //! By default the kind type implements the following traits: `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `From`, `From<&T>`. 105 | //! 106 | //! Extra traits can be derived with `derive(..)` attribute: 107 | //! 108 | //! ``` 109 | //! use kinded::Kinded; 110 | //! use std::collections::HashSet; 111 | //! 112 | //! #[derive(Kinded)] 113 | //! #[kinded(derive(Hash))] 114 | //! enum Drink { 115 | //! Mate, 116 | //! Coffee(String), 117 | //! Tea { variety: String, caffeine: bool } 118 | //! } 119 | //! 120 | //! let mut drink_kinds = HashSet::new(); 121 | //! drink_kinds.insert(DrinkKind::Mate); 122 | //! ``` 123 | //! 124 | //! ### Customize Display trait 125 | //! 126 | //! Implementation of `Display` trait can be customized in the `serde` fashion: 127 | //! 128 | //! ``` 129 | //! use kinded::Kinded; 130 | //! 131 | //! #[derive(Kinded)] 132 | //! #[kinded(display = "snake_case")] 133 | //! enum Drink { 134 | //! VeryHotBlackTea, 135 | //! Milk { fat: f64 }, 136 | //! } 137 | //! 138 | //! let tea = DrinkKind::VeryHotBlackTea; 139 | //! assert_eq!(tea.to_string(), "very_hot_black_tea"); 140 | //! ``` 141 | //! 142 | //! ### FromStr trait 143 | //! 144 | //! The kind type implements `FromStr` trait. The implementation tries it's best to parse, checking all the possible cases mentioned above. 145 | //! 146 | //! ``` 147 | //! use kinded::Kinded; 148 | //! 149 | //! #[derive(Kinded)] 150 | //! #[kinded(display = "snake_case")] 151 | //! enum Drink { 152 | //! VeryHotBlackTea, 153 | //! Milk { fat: f64 }, 154 | //! } 155 | //! 156 | //! assert_eq!( 157 | //! "VERY_HOT_BLACK_TEA".parse::().unwrap(), 158 | //! DrinkKind::VeryHotBlackTea 159 | //! ); 160 | //! 161 | //! assert_eq!( 162 | //! "veryhotblacktea".parse::().unwrap(), 163 | //! DrinkKind::VeryHotBlackTea 164 | //! ); 165 | //! ``` 166 | //! 167 | //! The possible values are `"snake_case"`, `"camelCase"`, `"PascalCase"`, `"SCREAMING_SNAKE_CASE"`, `"kebab-case"`, `"SCREAMING-KEBAB-CASE"`, `"Title Case"`, `"lowercase"`, `"UPPERCASE"`. 168 | //! 169 | //! ## A note about the war in Ukraine 🇺🇦 170 | //! 171 | //! Today I live in Berlin, I have the luxury to live a physically safe life. 172 | //! But I am Ukrainian. The first 25 years of my life I spent in [Kharkiv](https://en.wikipedia.org/wiki/Kharkiv), 173 | //! the second-largest city in Ukraine, 60km away from the border with russia. Today about [a third of my home city is destroyed](https://www.youtube.com/watch?v=ihoufBFSZds) by russians. 174 | //! My parents, my relatives and my friends had to survive the artillery and air attack, living for over a month in basements. 175 | //! 176 | //! Some of them have managed to evacuate to EU. Some others are trying to live "normal lifes" in Kharkiv, doing there daily duties. 177 | //! And some are at the front line right now, risking their lives every second to protect the rest. 178 | //! 179 | //! I encourage you to donate to [Charity foundation of Serhiy Prytula](https://prytulafoundation.org/en). 180 | //! Just pick the project you like and donate. This is one of the best-known foundations, you can watch a [little documentary](https://www.youtube.com/watch?v=VlmWqoeub1Q) about it. 181 | //! Your contribution to the Ukrainian military force is a contribution to my calmness, so I can spend more time developing the project. 182 | //! 183 | //! Thank you. 184 | //! 185 | //! 186 | //! ## License 187 | //! 188 | //! MIT © [Serhii Potapov](https://www.greyblake.com) 189 | 190 | mod errors; 191 | mod traits; 192 | 193 | pub use errors::ParseKindError; 194 | pub use kinded_macros::Kinded; 195 | pub use traits::{Kind, Kinded}; 196 | -------------------------------------------------------------------------------- /kinded/src/traits.rs: -------------------------------------------------------------------------------- 1 | use ::core::fmt::Debug; 2 | 3 | /// A trait that can be implemented by a main enum type. 4 | /// Typically should be derived with `#[derive(kinded::Kinded)]`. 5 | pub trait Kinded { 6 | type Kind: PartialEq + Eq + Debug + Clone + Copy + Kind; 7 | 8 | /// Get a kind variant without data. 9 | fn kind(&self) -> Self::Kind; 10 | } 11 | 12 | pub trait Kind: PartialEq + Eq + Debug + Clone + Copy { 13 | /// Return a slice with all possible kind variants. 14 | fn all() -> &'static [Self]; 15 | } 16 | -------------------------------------------------------------------------------- /kinded_macros/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /kinded_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kinded_macros" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Serhii Potapov "] 6 | 7 | description = "Generate enums with same variants, but without data." 8 | keywords = ["enum", "macros", "kind", "derive"] 9 | license = "MIT" 10 | repository = "https://github.com/greyblake/kinded" 11 | homepage = "https://github.com/greyblake/kinded" 12 | documentation = "https://docs.rs/kinded" 13 | readme = "README.md" 14 | categories = ["data-structures", "rust-patterns"] 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | convert_case = "0.6.0" 20 | proc-macro2 = "1.0" 21 | quote = "1.0" 22 | syn = { version = "2.0", features = ["extra-traits", "full"] } 23 | 24 | [lib] 25 | proc-macro = true 26 | -------------------------------------------------------------------------------- /kinded_macros/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /kinded_macros/src/gen/kind_enum.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{DisplayCase, Meta}; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use quote::quote; 4 | 5 | pub fn gen_kind_enum(meta: &Meta) -> TokenStream { 6 | let kind_enum_definition = gen_definition(meta); 7 | let impl_from_traits = gen_impl_from_traits(meta); 8 | let impl_display_trait = gen_impl_display_trait(meta); 9 | let impl_from_str_trait = gen_impl_from_str_trait(meta); 10 | let impl_kind_trait = gen_impl_kind_trait(meta); 11 | 12 | quote!( 13 | #kind_enum_definition 14 | #impl_from_traits 15 | #impl_display_trait 16 | #impl_from_str_trait 17 | #impl_kind_trait 18 | ) 19 | } 20 | 21 | fn gen_definition(meta: &Meta) -> TokenStream { 22 | let vis = &meta.vis; 23 | let kind_name = meta.kind_name(); 24 | let variant_names: Vec<&Ident> = meta.variants.iter().map(|v| &v.ident).collect(); 25 | let traits = meta.derive_traits(); 26 | 27 | quote!( 28 | #[derive(#(#traits),*)] // #[derive(Debug, Clone, Copy, PartialEq, Eq)] 29 | #vis enum #kind_name { // pub enum DrinkKind { 30 | #(#variant_names),* // Mate, Coffee, Tea 31 | } // } 32 | 33 | impl #kind_name { // impl DrinkKind { 34 | pub fn all() -> &'static [#kind_name] { // pub fn all() -> &'static [DrinkKind] { 35 | &[ // &[ 36 | #(#kind_name::#variant_names),* // DrinkKind::Mate, DrinkKind::Coffee, DrinkKind::Tea 37 | ] // ] 38 | } // } 39 | } // } 40 | ) 41 | } 42 | 43 | fn gen_impl_from_traits(meta: &Meta) -> TokenStream { 44 | let kind_name = meta.kind_name(); 45 | let generics = &meta.generics; 46 | let main_enum_with_generics = meta.main_enum_with_generics(); 47 | 48 | quote!( 49 | impl #generics From<#main_enum_with_generics> for #kind_name { // impl From> for DrinkKind { 50 | fn from(value: #main_enum_with_generics) -> #kind_name { // fn from(value: Drink) -> DrinkKind { 51 | value.kind() // value.kind() 52 | } // } 53 | } // } 54 | 55 | impl #generics From<&#main_enum_with_generics> for #kind_name { // impl From> for DrinkKind { 56 | fn from(value: &#main_enum_with_generics) -> #kind_name { // fn from(value: &Drink) -> DrinkKind { 57 | value.kind() // value.kind() 58 | } // } 59 | } // } 60 | ) 61 | } 62 | 63 | fn gen_impl_display_trait(meta: &Meta) -> TokenStream { 64 | let kind_name = meta.kind_name(); 65 | let maybe_case = meta.kinded_attrs.display; 66 | 67 | let match_branches = meta.variants.iter().map(|variant| { 68 | let original_variant_name_str = variant.ident.to_string(); 69 | let cased_variant_name = apply_maybe_case(original_variant_name_str, maybe_case); 70 | let variant_name = &variant.ident; 71 | quote!( 72 | #kind_name::#variant_name => write!(f, #cased_variant_name) 73 | ) 74 | }); 75 | 76 | quote!( 77 | impl std::fmt::Display for #kind_name { // impl std::fmt::Display for DrinkKind { 78 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 79 | match self { // match self { 80 | #(#match_branches),* // DrinkKind::Mate => write!(f, "mate"), 81 | } // } 82 | } // } 83 | } // 84 | ) 85 | } 86 | 87 | fn apply_maybe_case(original: String, maybe_display_case: Option) -> String { 88 | if let Some(display_case) = maybe_display_case { 89 | display_case.apply(&original) 90 | } else { 91 | original 92 | } 93 | } 94 | 95 | fn gen_impl_from_str_trait(meta: &Meta) -> TokenStream { 96 | let kind_name = meta.kind_name(); 97 | 98 | let original_match_branches = meta.variants.iter().map(|variant| { 99 | let ident = &variant.ident; 100 | let name_str = ident.to_string(); 101 | quote!(#name_str => return Ok(#kind_name::#ident),) 102 | }); 103 | 104 | let alt_match_branches = meta.variants.iter().map(|variant| { 105 | let ident = &variant.ident; 106 | let name_str = ident.to_string(); 107 | let alternatives = DisplayCase::all().map(|case| case.apply(&name_str)); 108 | quote!(#(#alternatives)|* => return Ok(#kind_name::#ident),) 109 | }); 110 | 111 | quote!( 112 | impl ::core::str::FromStr for #kind_name { 113 | type Err = ::kinded::ParseKindError; 114 | 115 | fn from_str(s: &str) -> ::core::result::Result { 116 | // First try to match the variants as they are 117 | match s { // match s { 118 | #(#original_match_branches)* // "HotMate" => Mate::HotMate, 119 | _ => () // _ => (), 120 | } // 121 | 122 | // Now try to match all possible alternative spelling of 123 | // the variants 124 | match s { // match s { 125 | #(#alt_match_branches)* // "hot_mate" | "HOT_MATE" | "hotMate" | .. => Mate::HotMate 126 | _ => () // _ => () 127 | } // } 128 | 129 | // If still no success, then return an error 130 | let error = ::kinded::ParseKindError::from_type_and_string::<#kind_name>(s.to_owned()); 131 | Err(error) 132 | } 133 | } 134 | ) 135 | } 136 | 137 | fn gen_impl_kind_trait(meta: &Meta) -> TokenStream { 138 | let kind_name = meta.kind_name(); 139 | 140 | quote!( 141 | impl ::kinded::Kind for #kind_name { 142 | fn all() -> &'static [#kind_name] { 143 | Self::all() 144 | } 145 | } 146 | ) 147 | } 148 | -------------------------------------------------------------------------------- /kinded_macros/src/gen/main_enum.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{FieldsType, Meta, Variant}; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use quote::quote; 4 | 5 | pub fn gen_main_enum_extra(meta: &Meta) -> TokenStream { 6 | let fn_kind = gen_fn_kind(meta); 7 | let main_enum_with_generics = meta.main_enum_with_generics(); 8 | let generics = &meta.generics; 9 | 10 | let impl_kinded_trait = gen_impl_kinded_trait(meta); 11 | 12 | quote!( 13 | impl #generics #main_enum_with_generics { // impl Drink { 14 | #fn_kind // fn kind(&self) -> DrinkKind { ... } 15 | } // } 16 | 17 | #impl_kinded_trait // impl ::kinded::Kinded for Drink { .. } 18 | ) 19 | } 20 | 21 | fn gen_fn_kind(meta: &Meta) -> TokenStream { 22 | let name = &meta.ident; 23 | let kind_name = meta.kind_name(); 24 | let match_branches = meta 25 | .variants 26 | .iter() 27 | .map(|variant| gen_match_branch(name, &kind_name, variant)); 28 | 29 | quote!( 30 | pub fn kind(&self) -> #kind_name { // pub fn kind(&self) -> DrinkKind { 31 | match self { // match self { 32 | #(#match_branches),* // Drink::Coffee(..) => DrinkKind::Coffee, 33 | } // } 34 | } // } 35 | ) 36 | } 37 | 38 | fn gen_match_branch(name: &Ident, kind_name: &Ident, variant: &Variant) -> TokenStream { 39 | let variant_name = &variant.ident; 40 | let variant_destruct = match variant.fields_type { 41 | FieldsType::Named => quote!({ .. }), 42 | FieldsType::Unnamed => quote!((..)), 43 | FieldsType::Unit => quote!(), 44 | }; 45 | 46 | quote!( 47 | #name::#variant_name #variant_destruct => #kind_name::#variant_name 48 | ) 49 | } 50 | 51 | fn gen_impl_kinded_trait(meta: &Meta) -> TokenStream { 52 | let kind_name = meta.kind_name(); 53 | let main_enum_with_generics = meta.main_enum_with_generics(); 54 | let generics = &meta.generics; 55 | 56 | quote!( 57 | impl #generics ::kinded::Kinded for #main_enum_with_generics { // impl ::kinded::Kinded for Drink { 58 | type Kind = #kind_name; // type Kind = DrinkKind; 59 | // 60 | fn kind(&self) -> #kind_name { // fn kind(&self) -> DrinkKind { 61 | self.kind() // self.kind() 62 | } // } 63 | } // } 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /kinded_macros/src/gen/mod.rs: -------------------------------------------------------------------------------- 1 | mod kind_enum; 2 | mod main_enum; 3 | 4 | use crate::models::Meta; 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | 8 | pub fn generate(meta: Meta) -> TokenStream { 9 | let kind_enum = kind_enum::gen_kind_enum(&meta); 10 | let main_enum_extra = main_enum::gen_main_enum_extra(&meta); 11 | 12 | quote!( 13 | #kind_enum 14 | #main_enum_extra 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /kinded_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Kinded 2 | //! 3 | //! Generate Rust enum kind types without boilerplate. 4 | //! 5 | //! Author: [Serhii Potapov](https://www.greyblake.com/) 6 | //! 7 | //! This is a supporting macro crate, that should not be used directly. 8 | //! For the documentation please refer to [kinded](https://docs.rs/kinded/) crate. 9 | 10 | pub(crate) mod gen; 11 | pub(crate) mod models; 12 | pub(crate) mod parse; 13 | 14 | use proc_macro2::TokenStream; 15 | use syn::DeriveInput; 16 | 17 | #[proc_macro_derive(Kinded, attributes(kinded))] 18 | pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 19 | expand_derive(input) 20 | .unwrap_or_else(|e| syn::Error::to_compile_error(&e)) 21 | .into() 22 | } 23 | 24 | fn expand_derive(input: proc_macro::TokenStream) -> Result { 25 | let derive_input: DeriveInput = 26 | syn::parse(input).expect("kinded failed parse token stream as DeriveInput"); 27 | let meta = parse::parse_derive_input(derive_input)?; 28 | Ok(gen::generate(meta)) 29 | } 30 | -------------------------------------------------------------------------------- /kinded_macros/src/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{DisplayCase, FieldsType, Meta, Variant}; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use quote::quote; 4 | -------------------------------------------------------------------------------- /kinded_macros/src/models.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, TokenStream}; 2 | use quote::{format_ident, quote}; 3 | use syn::{Generics, Path, Visibility}; 4 | 5 | #[derive(Debug)] 6 | pub struct Meta { 7 | /// Visibility of enum. 8 | /// Kind implementation inherits this visibility automatically. 9 | pub vis: Visibility, 10 | 11 | pub ident: Ident, 12 | 13 | pub generics: Generics, 14 | 15 | pub variants: Vec, 16 | 17 | /// Attributes specified with #[kinded(..)] above the enum definition. 18 | pub kinded_attrs: KindedAttributes, 19 | } 20 | 21 | impl Meta { 22 | /// Get the name for the kind type. 23 | pub fn kind_name(&self) -> Ident { 24 | if let Some(ref kind_name) = self.kinded_attrs.kind { 25 | kind_name.clone() 26 | } else { 27 | format_ident!("{}Kind", self.ident) 28 | } 29 | } 30 | 31 | /// Get the traits that need to be derived. 32 | pub fn derive_traits(&self) -> Vec { 33 | const DEFAULT_DERIVE_TRAITS: &[&str] = &["Debug", "Clone", "Copy", "PartialEq", "Eq"]; 34 | 35 | let mut traits: Vec = DEFAULT_DERIVE_TRAITS 36 | .iter() 37 | .map(|trait_name| Path::from(format_ident!("{trait_name}"))) 38 | .collect(); 39 | 40 | // Add the extra specified traits, if they're different from the default ones 41 | if let Some(ref extra_traits) = self.kinded_attrs.derive { 42 | for extra_trait in extra_traits { 43 | if !traits.contains(extra_trait) { 44 | traits.push(extra_trait.clone()); 45 | } 46 | } 47 | } 48 | 49 | traits 50 | } 51 | 52 | pub fn main_enum_with_generics(&self) -> TokenStream { 53 | let type_name = &self.ident; 54 | let generics = &self.generics; 55 | 56 | quote!(#type_name #generics) 57 | } 58 | } 59 | 60 | #[derive(Debug)] 61 | pub struct Variant { 62 | pub ident: Ident, 63 | pub fields_type: FieldsType, 64 | } 65 | 66 | /// This mimics syn::Fields, but without payload. 67 | #[derive(Debug)] 68 | pub enum FieldsType { 69 | /// Example: `Admin { id: i32 }` 70 | Named, 71 | 72 | /// Example: `User(i32)` 73 | Unnamed, 74 | 75 | /// Example: `Guest` 76 | Unit, 77 | } 78 | 79 | /// Attributes specified with #[kinded(..)] 80 | #[derive(Debug, Default)] 81 | pub struct KindedAttributes { 82 | /// Name for the kind type, specified with `kind = ...` 83 | pub kind: Option, 84 | 85 | /// Traits to derive, specified with `derive(...)` 86 | pub derive: Option>, 87 | 88 | /// Attributes to customize implementation for Display trait 89 | pub display: Option, 90 | } 91 | 92 | /// This uses the same names as serde + "Title Case" variant. 93 | /// Some names are different from what `convert_case` crate uses. 94 | #[derive(Debug, Clone, Copy)] 95 | pub enum DisplayCase { 96 | /// snake_case 97 | Snake, 98 | 99 | /// camelCase 100 | Camel, 101 | 102 | /// PascalCase 103 | Pascal, 104 | 105 | /// SCREAMING_SNAKE_CASE 106 | ScreamingSnake, 107 | 108 | /// kebab-case 109 | Kebab, 110 | 111 | /// SCREAMING-KEBAB-CASE 112 | ScreamingKebab, 113 | 114 | /// Title Case 115 | Title, 116 | 117 | /// lowercase 118 | Lower, 119 | 120 | /// UPPERCASE 121 | Upper, 122 | } 123 | 124 | impl From for convert_case::Case { 125 | fn from(display_case: DisplayCase) -> convert_case::Case { 126 | use convert_case::Case; 127 | 128 | // Note that convert_case use slightly different names than serde. 129 | match display_case { 130 | DisplayCase::Snake => Case::Snake, 131 | DisplayCase::Camel => Case::Camel, 132 | DisplayCase::Pascal => Case::Pascal, 133 | DisplayCase::ScreamingSnake => Case::ScreamingSnake, 134 | DisplayCase::Kebab => Case::Kebab, 135 | DisplayCase::ScreamingKebab => Case::Cobol, 136 | DisplayCase::Title => Case::Title, 137 | DisplayCase::Lower => Case::Flat, 138 | DisplayCase::Upper => Case::UpperFlat, 139 | } 140 | } 141 | } 142 | 143 | impl DisplayCase { 144 | pub fn all() -> impl Iterator { 145 | use DisplayCase::*; 146 | [ 147 | Snake, 148 | Camel, 149 | Pascal, 150 | ScreamingSnake, 151 | Kebab, 152 | ScreamingKebab, 153 | Title, 154 | Lower, 155 | Upper, 156 | ] 157 | .into_iter() 158 | } 159 | 160 | pub fn apply(self, s: &str) -> String { 161 | use convert_case::{Case, Casing}; 162 | let case: Case = self.into(); 163 | s.to_case(case) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /kinded_macros/src/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{DisplayCase, FieldsType, KindedAttributes, Meta, Variant}; 2 | use proc_macro2::Ident; 3 | use quote::ToTokens; 4 | use syn::{ 5 | bracketed, parenthesized, 6 | parse::{Parse, ParseStream}, 7 | spanned::Spanned, 8 | Attribute, Data, DeriveInput, LitStr, Path, Token, 9 | }; 10 | 11 | pub fn parse_derive_input(input: DeriveInput) -> Result { 12 | let kinded_attrs: KindedAttributes = { 13 | match find_kinded_attr(&input)? { 14 | Some(kinded_attr) => syn::parse2(kinded_attr.to_token_stream())?, 15 | None => KindedAttributes::default(), 16 | } 17 | }; 18 | 19 | let data = match input.data { 20 | Data::Enum(enum_data) => enum_data, 21 | Data::Struct(..) | Data::Union(..) => { 22 | return Err(syn::Error::new( 23 | input.ident.span(), 24 | "Kinded can be derived only on enums", 25 | )); 26 | } 27 | }; 28 | 29 | Ok(Meta { 30 | vis: input.vis, 31 | ident: input.ident, 32 | generics: input.generics, 33 | variants: data.variants.iter().map(parse_variant).collect(), 34 | kinded_attrs, 35 | }) 36 | } 37 | 38 | fn parse_variant(variant: &syn::Variant) -> Variant { 39 | Variant { 40 | ident: variant.ident.clone(), 41 | fields_type: parse_fields_type(&variant.fields), 42 | } 43 | } 44 | 45 | fn parse_fields_type(fields: &syn::Fields) -> FieldsType { 46 | match fields { 47 | syn::Fields::Named(..) => FieldsType::Named, 48 | syn::Fields::Unnamed(..) => FieldsType::Unnamed, 49 | syn::Fields::Unit => FieldsType::Unit, 50 | } 51 | } 52 | 53 | /// Find `#[kinded(..)]` attribute on the enum. 54 | fn find_kinded_attr(input: &DeriveInput) -> Result, syn::Error> { 55 | let kinded_attrs: Vec<_> = input 56 | .attrs 57 | .iter() 58 | .filter(|&attr| attr.path().is_ident("kinded")) 59 | .collect(); 60 | 61 | if kinded_attrs.len() > 1 { 62 | let &attr = kinded_attrs.last().unwrap(); 63 | let span = attr.span(); 64 | let msg = "Multiple #[kinded(..)] attributes are not allowed."; 65 | Err(syn::Error::new(span, msg)) 66 | } else { 67 | let maybe_kinded_attr = kinded_attrs.into_iter().next(); 68 | Ok(maybe_kinded_attr) 69 | } 70 | } 71 | 72 | impl Parse for KindedAttributes { 73 | fn parse(input: ParseStream) -> syn::Result { 74 | let mut kinded_attrs = KindedAttributes::default(); 75 | 76 | // Unwrap the irrelevant part and reassign input to the relevant input: 77 | // 78 | // #[kinded( RELEVANT_INPUT )] 79 | // 80 | let input = { 81 | let _: Token!(#) = input.parse()?; 82 | let bracketed_content; 83 | bracketed!(bracketed_content in input); 84 | let _kinded: Ident = bracketed_content.parse()?; 85 | 86 | let parenthesized_content; 87 | parenthesized!(parenthesized_content in bracketed_content); 88 | parenthesized_content 89 | }; 90 | 91 | while !input.is_empty() { 92 | let attr_name: Ident = input.parse()?; 93 | if attr_name == "kind" { 94 | let _: Token!(=) = input.parse()?; 95 | let kind: Ident = input.parse()?; 96 | if kinded_attrs.kind.is_none() { 97 | kinded_attrs.kind = Some(kind); 98 | } else { 99 | let msg = format!("Duplicated attribute: {attr_name}"); 100 | return Err(syn::Error::new(attr_name.span(), msg)); 101 | } 102 | } else if attr_name == "derive" { 103 | let derive_input; 104 | parenthesized!(derive_input in input); 105 | let parsed_traits = derive_input.parse_terminated(Path::parse, Token![,])?; 106 | let traits: Vec = parsed_traits.into_iter().collect(); 107 | if kinded_attrs.derive.is_none() { 108 | kinded_attrs.derive = Some(traits); 109 | } else { 110 | let msg = format!("Duplicated attribute: {attr_name}"); 111 | return Err(syn::Error::new(attr_name.span(), msg)); 112 | } 113 | } else if attr_name == "display" { 114 | let _: Token!(=) = input.parse()?; 115 | let case_lit_str: LitStr = input.parse()?; 116 | let case = match case_lit_str.value().as_ref() { 117 | "snake_case" => DisplayCase::Snake, 118 | "camelCase" => DisplayCase::Camel, 119 | "PascalCase" => DisplayCase::Pascal, 120 | "SCREAMING_SNAKE_CASE" => DisplayCase::ScreamingSnake, 121 | "kebab-case" => DisplayCase::Kebab, 122 | "SCREAMING-KEBAB-CASE" => DisplayCase::ScreamingKebab, 123 | "Title Case" => DisplayCase::Title, 124 | "lowercase" => DisplayCase::Lower, 125 | "UPPERCASE" => DisplayCase::Upper, 126 | _ => { 127 | let valid_values = [ 128 | "snake_case", 129 | "camelCase", 130 | "PascalCase", 131 | "SCREAMING_SNAKE_CASE", 132 | "kebab-case", 133 | "SCREAMING-KEBAB-CASE", 134 | "Title Case", 135 | "lowercase", 136 | "UPPERCASE", 137 | ] 138 | .map(|value| format!(r#""{value}""#)) 139 | .join(", "); 140 | let given_value = format!(r#""{}""#, case_lit_str.value()); 141 | let msg = format!("Invalid value for display: {given_value}\nValid values are: {valid_values}"); 142 | return Err(syn::Error::new(case_lit_str.span(), msg)); 143 | } 144 | }; 145 | if kinded_attrs.derive.is_none() { 146 | kinded_attrs.display = Some(case); 147 | } else { 148 | let msg = format!("Duplicated attribute: {attr_name}"); 149 | return Err(syn::Error::new(attr_name.span(), msg)); 150 | } 151 | } else { 152 | let msg = format!("Unknown attribute: {attr_name}"); 153 | return Err(syn::Error::new(attr_name.span(), msg)); 154 | } 155 | 156 | // Parse `,` unless it's the end of the stream 157 | if !input.is_empty() { 158 | let _comma: Token![,] = input.parse()?; 159 | } 160 | } 161 | 162 | Ok(kinded_attrs) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /sandbox/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /sandbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sandbox" 3 | version = "0.0.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | kinded = { path = "../kinded" } 10 | -------------------------------------------------------------------------------- /sandbox/src/main.rs: -------------------------------------------------------------------------------- 1 | use kinded::Kinded; 2 | 3 | #[allow(dead_code)] 4 | #[derive(Kinded)] 5 | enum Drink { 6 | Mate, 7 | Coffee(String), 8 | Tea { variety: String, caffeine: bool }, 9 | } 10 | 11 | fn main() { 12 | // Mate 13 | { 14 | let drink = Drink::Mate; 15 | assert_eq!(drink.kind(), DrinkKind::Mate); 16 | } 17 | 18 | // Coffee 19 | { 20 | let drink = Drink::Coffee("Espresso".to_owned()); 21 | assert_eq!(drink.kind(), DrinkKind::Coffee); 22 | } 23 | 24 | // Tea 25 | { 26 | let drink = Drink::Tea { 27 | variety: "Green".to_owned(), 28 | caffeine: true, 29 | }; 30 | assert_eq!(drink.kind(), DrinkKind::Tea); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test_suite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test_suite" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | kinded = { path = "../kinded" } 10 | -------------------------------------------------------------------------------- /test_suite/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | #![allow(dead_code)] 3 | 4 | use kinded::Kinded; 5 | 6 | #[derive(Kinded)] 7 | enum Role { 8 | Guest, 9 | User(i32), 10 | #[allow(dead_code)] 11 | Admin { 12 | id: i32, 13 | }, 14 | } 15 | 16 | mod main_enum { 17 | use super::*; 18 | 19 | mod fn_kind { 20 | use super::*; 21 | 22 | #[test] 23 | fn should_convert_unit_variant() { 24 | let guest = Role::Guest; 25 | assert_eq!(guest.kind(), RoleKind::Guest); 26 | } 27 | 28 | #[test] 29 | fn should_convert_unnamed_variant() { 30 | let user = Role::User(13); 31 | assert_eq!(user.kind(), RoleKind::User); 32 | } 33 | 34 | #[test] 35 | fn should_convert_named_variant() { 36 | let admin = Role::Admin { id: 404 }; 37 | assert_eq!(admin.kind(), RoleKind::Admin); 38 | } 39 | } 40 | 41 | mod traits { 42 | use super::*; 43 | use kinded::Kinded; 44 | 45 | fn compute_kind(val: T) -> ::Kind { 46 | val.kind() 47 | } 48 | 49 | #[test] 50 | fn should_implement_kinded() { 51 | let admin = Role::Admin { id: 32 }; 52 | assert_eq!(compute_kind(admin), RoleKind::Admin); 53 | } 54 | } 55 | } 56 | 57 | mod kind_enum { 58 | use super::RoleKind; 59 | 60 | mod traits { 61 | use super::super::{Role, RoleKind}; 62 | 63 | #[test] 64 | fn should_implement_debug() { 65 | assert_eq!(format!("{:?}", RoleKind::Guest), "Guest") 66 | } 67 | 68 | #[test] 69 | fn should_implement_clone() { 70 | let _ = RoleKind::Admin.clone(); 71 | } 72 | 73 | #[test] 74 | fn should_implement_copy() { 75 | fn receive_copy() {} 76 | 77 | receive_copy::(); 78 | } 79 | 80 | #[test] 81 | fn should_implement_eq() { 82 | assert!(RoleKind::Guest.eq(&RoleKind::Guest)); 83 | assert!(!RoleKind::Guest.eq(&RoleKind::User)); 84 | } 85 | 86 | #[test] 87 | fn should_implement_from() { 88 | let user = Role::User(123); 89 | assert_eq!(RoleKind::from(user), RoleKind::User); 90 | } 91 | 92 | #[test] 93 | fn should_implement_from_ref() { 94 | let guest = Role::Guest; 95 | assert_eq!(RoleKind::from(&guest), RoleKind::Guest); 96 | } 97 | 98 | mod display_trait { 99 | use super::RoleKind; 100 | 101 | #[test] 102 | fn should_implement_display() { 103 | let guest = RoleKind::Guest; 104 | assert_eq!(format!("{guest}"), "Guest"); 105 | 106 | let user = RoleKind::User; 107 | assert_eq!(format!("{user}"), "User"); 108 | } 109 | 110 | #[test] 111 | fn should_display_snake_case() { 112 | #[derive(kinded::Kinded)] 113 | #[kinded(display = "snake_case")] 114 | enum Drink { 115 | HotMate, 116 | } 117 | 118 | assert_eq!(DrinkKind::HotMate.to_string(), "hot_mate") 119 | } 120 | 121 | #[test] 122 | fn should_display_camel_case() { 123 | #[derive(kinded::Kinded)] 124 | #[kinded(display = "camelCase")] 125 | enum Drink { 126 | HotMate, 127 | } 128 | 129 | assert_eq!(DrinkKind::HotMate.to_string(), "hotMate") 130 | } 131 | 132 | #[test] 133 | fn should_display_pascal_case() { 134 | #[derive(kinded::Kinded)] 135 | #[kinded(display = "PascalCase")] 136 | enum Drink { 137 | HotMate, 138 | } 139 | 140 | assert_eq!(DrinkKind::HotMate.to_string(), "HotMate") 141 | } 142 | 143 | #[test] 144 | fn should_display_screaming_snake_case() { 145 | #[derive(kinded::Kinded)] 146 | #[kinded(display = "SCREAMING_SNAKE_CASE")] 147 | enum Drink { 148 | HotMate, 149 | } 150 | 151 | assert_eq!(DrinkKind::HotMate.to_string(), "HOT_MATE") 152 | } 153 | 154 | #[test] 155 | fn should_display_kebab_case() { 156 | #[derive(kinded::Kinded)] 157 | #[kinded(display = "kebab-case")] 158 | enum Drink { 159 | HotMate, 160 | } 161 | 162 | assert_eq!(DrinkKind::HotMate.to_string(), "hot-mate") 163 | } 164 | 165 | #[test] 166 | fn should_display_screaming_kebab_case() { 167 | #[derive(kinded::Kinded)] 168 | #[kinded(display = "SCREAMING-KEBAB-CASE")] 169 | enum Drink { 170 | HotMate, 171 | } 172 | 173 | assert_eq!(DrinkKind::HotMate.to_string(), "HOT-MATE") 174 | } 175 | 176 | #[test] 177 | fn should_display_title_case() { 178 | #[derive(kinded::Kinded)] 179 | #[kinded(display = "Title Case")] 180 | enum Drink { 181 | HotMate, 182 | } 183 | 184 | assert_eq!(DrinkKind::HotMate.to_string(), "Hot Mate") 185 | } 186 | 187 | #[test] 188 | fn should_display_lower_case() { 189 | #[derive(kinded::Kinded)] 190 | #[kinded(display = "lowercase")] 191 | enum Drink { 192 | HotMate, 193 | } 194 | 195 | assert_eq!(DrinkKind::HotMate.to_string(), "hotmate") 196 | } 197 | 198 | #[test] 199 | fn should_display_upper_case() { 200 | #[derive(kinded::Kinded)] 201 | #[kinded(display = "UPPERCASE")] 202 | enum Drink { 203 | HotMate, 204 | } 205 | 206 | assert_eq!(DrinkKind::HotMate.to_string(), "HOTMATE") 207 | } 208 | } 209 | 210 | mod from_str_trait { 211 | #[derive(kinded::Kinded)] 212 | enum Mate { 213 | HotMate, 214 | Terere, 215 | } 216 | 217 | #[test] 218 | fn should_implement_from_str_trait() { 219 | let kind: MateKind = "Terere".parse().unwrap(); 220 | assert_eq!(kind, MateKind::Terere); 221 | 222 | let kind: MateKind = "HotMate".parse().unwrap(); 223 | assert_eq!(kind, MateKind::HotMate); 224 | } 225 | 226 | #[test] 227 | fn should_parse_alternative_cases() { 228 | // All possible alternatives of HoteMate 229 | let hot_mate_alternatives = [ 230 | "hot_mate", // snake_case 231 | "hotMate", // camelCase 232 | "HotMate", // PascalCase 233 | "HOT_MATE", // SCREAMING_SNAKE_CASE 234 | "hot-mate", // kebab-case 235 | "HOT-MATE", // SCREAMING-KEBAB-CASE 236 | "Hot Mate", // Title Case 237 | "hotmate", // lowercase 238 | "HOTMATE", // UPPERCASE 239 | ]; 240 | for alt in hot_mate_alternatives { 241 | let kind: MateKind = alt.parse().unwrap(); 242 | assert_eq!(kind, MateKind::HotMate); 243 | } 244 | 245 | // Just a few alternatives of Terere 246 | let terere_alternatives = ["terere", "TERERE", "Terere"]; 247 | for alt in terere_alternatives { 248 | let kind: MateKind = alt.parse().unwrap(); 249 | assert_eq!(kind, MateKind::Terere); 250 | } 251 | } 252 | 253 | #[test] 254 | fn should_return_error_on_failure() { 255 | let error: kinded::ParseKindError = "Calabaza".parse::().unwrap_err(); 256 | assert_eq!( 257 | error.to_string(), 258 | r#"Failed to parse "Calabaza" as MateKind"# 259 | ); 260 | } 261 | 262 | #[test] 263 | fn should_distinguish_very_similar_abbreviations() { 264 | #[derive(kinded::Kinded)] 265 | enum Db { 266 | MySql, 267 | MySQL, 268 | } 269 | 270 | assert_eq!("MySql".parse::().unwrap(), DbKind::MySql); 271 | assert_eq!("MySQL".parse::().unwrap(), DbKind::MySQL); 272 | } 273 | } 274 | 275 | mod kind_trait { 276 | use crate::RoleKind; 277 | 278 | #[test] 279 | fn should_implement_kind_trait() { 280 | assert_eq!( 281 | RoleKind::all(), 282 | [RoleKind::Guest, RoleKind::User, RoleKind::Admin] 283 | ) 284 | } 285 | } 286 | } 287 | 288 | #[test] 289 | fn should_provide_all_function_that_returns_iterator() { 290 | fn impl_iter(_: impl IntoIterator) {} 291 | impl_iter(RoleKind::all()); 292 | } 293 | } 294 | 295 | #[test] 296 | fn should_allow_to_give_custom_name_kind_type() { 297 | #[derive(Kinded)] 298 | #[kinded(kind = SimpleDrink)] 299 | enum Drink { 300 | Tea(&'static str), 301 | Coffee(&'static str), 302 | } 303 | 304 | let green_tea = Drink::Tea("Green"); 305 | assert_eq!(green_tea.kind(), SimpleDrink::Tea); 306 | } 307 | 308 | #[test] 309 | fn should_allow_to_derive_custom_traits() { 310 | use std::collections::HashMap; 311 | 312 | #[derive(Kinded)] 313 | #[kinded(derive(Hash, Eq))] 314 | enum Drink { 315 | Tea(&'static str), 316 | Coffee(&'static str), 317 | } 318 | 319 | let mut drinks = HashMap::new(); 320 | drinks.insert(DrinkKind::Tea, 5); 321 | } 322 | 323 | #[test] 324 | fn should_work_with_generics() { 325 | use std::collections::HashMap; 326 | 327 | #[derive(Kinded)] 328 | enum Maybe { 329 | Just(T), 330 | Nothing, 331 | } 332 | 333 | assert_eq!(Maybe::Just(13).kind(), MaybeKind::Just); 334 | } 335 | 336 | #[test] 337 | fn should_work_with_lifetimes() { 338 | use std::collections::HashMap; 339 | 340 | #[derive(Kinded)] 341 | enum Identifier<'a, I> { 342 | Name(&'a str), 343 | Id(I), 344 | } 345 | 346 | let identifier: Identifier = Identifier::Name("Xen"); 347 | assert_eq!(identifier.kind(), IdentifierKind::Name); 348 | } 349 | --------------------------------------------------------------------------------