├── .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 |
4 |
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