From 2f2ac9d70504db088ccc1c1fee16e4ccdf46ab14 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 18:33:57 +0100 Subject: [PATCH 1/8] Support skip_derive() --- CHANGELOG.md | 3 +- README.md | 38 +++++ kinded/src/lib.rs | 37 ++++- kinded_macros/src/generate/kind_enum.rs | 22 ++- kinded_macros/src/models.rs | 135 +++++++++++++++++ kinded_macros/src/parse.rs | 140 ++++++++++++++++++ test_suite/src/lib.rs | 186 ++++++++++++++++++++++++ 7 files changed, 556 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24af51c..91184f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ -## Unreleased +## v0.5.0 - 2026-02-xx +- Add `skip_derive` attribute to opt out of default trait implementations (fixes #19). - Add `attrs` attribute to pass extra attributes to the generated kind enum (e.g., `#[kinded(attrs(serde(rename_all = "snake_case")))]`) - Add per-variant `attrs` attribute to pass attributes to individual kind variants (e.g., `#[kinded(attrs(default))]`) (fixes #22) - Make `kind()` method `const fn`, allowing usage in const contexts (fixes #12) diff --git a/README.md b/README.md index a2789b9..42dbc57 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,44 @@ let mut drink_kinds = HashSet::new(); drink_kinds.insert(DrinkKind::Mate); ``` +### Skip default traits + +In some cases you may need to opt out of default trait implementations. +For example, when using `kinded` with crates like `enumset` that provide their own trait implementations, +you can use `skip_derive(..)` to avoid conflicts: + +```rs +use kinded::Kinded; + +#[derive(Kinded)] +#[kinded(skip_derive(Display, FromStr))] +enum Task { + Download { url: String }, + Process(Vec), +} + +// Display and FromStr are not implemented for TaskKind +// You can provide your own custom implementations if needed +``` + +The following traits can be skipped: +- Derived traits: `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq` +- Implemented traits: `Display`, `FromStr`, `From` + +You can combine `skip_derive` with `derive` to replace default traits: + +```rs +use kinded::Kinded; + +#[derive(Kinded)] +#[kinded(skip_derive(Display), derive(Hash))] +enum Expr { + Literal(i64), + Variable(String), + BinaryOp { left: Box, op: char, right: Box }, +} +``` + ### Extra attributes If you're using derive macros from other libraries like Serde or Strum, you may want to add diff --git a/kinded/src/lib.rs b/kinded/src/lib.rs index 2c85015..2603040 100644 --- a/kinded/src/lib.rs +++ b/kinded/src/lib.rs @@ -120,7 +120,7 @@ //! //! ### Derive traits //! -//! By default the kind type implements the following traits: `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `From`, `From<&T>`. +//! By default the kind type implements the following traits: `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Display`, `FromStr`, `From`, `From<&T>`. //! //! Extra traits can be derived with `derive(..)` attribute: //! @@ -140,6 +140,41 @@ //! drink_kinds.insert(DrinkKind::Mate); //! ``` //! +//! ### Skip default traits +//! +//! In some cases you may need to opt out of default trait implementations. +//! For example, when using `kinded` with crates like `enumset` that provide their own trait implementations, +//! you can use `skip_derive(..)` to avoid conflicts: +//! +//! ``` +//! use kinded::Kinded; +//! +//! #[derive(Kinded)] +//! #[kinded(skip_derive(Display, FromStr))] +//! enum Task { +//! Download { url: String }, +//! Process(Vec), +//! } +//! +//! // Display and FromStr are not implemented for TaskKind +//! // You can provide your own custom implementations if needed +//! ``` +//! +//! The following traits can be skipped: +//! - Derived traits: `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq` +//! - Implemented traits: `Display`, `FromStr`, `From` +//! +//! You can combine `skip_derive` with `derive` to replace default traits: +//! +//! ``` +//! #[derive(kinded::Kinded)] +//! #[kinded(skip_derive(Display), derive(Hash))] +//! enum Expr { +//! Literal(i64), +//! Variable(String), +//! } +//! ``` +//! //! ### Generic attributes //! //! If you're using derive traits from other libraries like Serde or Sqlx, you might want to add diff --git a/kinded_macros/src/generate/kind_enum.rs b/kinded_macros/src/generate/kind_enum.rs index e0d0b07..e0ebe51 100644 --- a/kinded_macros/src/generate/kind_enum.rs +++ b/kinded_macros/src/generate/kind_enum.rs @@ -4,11 +4,27 @@ use quote::quote; pub fn gen_kind_enum(meta: &Meta) -> TokenStream { let kind_enum_definition = gen_definition(meta); - let impl_from_traits = gen_impl_from_traits(meta); - let impl_display_trait = gen_impl_display_trait(meta); - let impl_from_str_trait = gen_impl_from_str_trait(meta); let impl_kind_trait = gen_impl_kind_trait(meta); + // Conditionally generate trait implementations based on skip_derive + let impl_from_traits = if meta.kinded_attrs.should_skip_derive("From") { + quote!() + } else { + gen_impl_from_traits(meta) + }; + + let impl_display_trait = if meta.kinded_attrs.should_skip_derive("Display") { + quote!() + } else { + gen_impl_display_trait(meta) + }; + + let impl_from_str_trait = if meta.kinded_attrs.should_skip_derive("FromStr") { + quote!() + } else { + gen_impl_from_str_trait(meta) + }; + quote!( #kind_enum_definition #impl_from_traits diff --git a/kinded_macros/src/models.rs b/kinded_macros/src/models.rs index c83a987..8ccac50 100644 --- a/kinded_macros/src/models.rs +++ b/kinded_macros/src/models.rs @@ -34,6 +34,7 @@ impl Meta { let mut traits: Vec = DEFAULT_DERIVE_TRAITS .iter() + .filter(|name| !self.kinded_attrs.should_skip_derive(name)) .map(|trait_name| Path::from(format_ident!("{trait_name}"))) .collect(); @@ -94,6 +95,11 @@ pub struct KindedAttributes { /// Traits to derive, specified with `derive(...)` pub derive: Option>, + /// Opt out default derives/implementations for traits like Debug, Clone, Copy, PartialEq, Eq, + /// FromStr, Display, etc. It may be needed in some cases for compatibility with other crates + /// that provide similar macros. See https://github.com/greyblake/kinded/pull/19 + pub skip_derive: Option>, + /// Attributes to customize implementation for Display trait pub display: Option, @@ -101,6 +107,16 @@ pub struct KindedAttributes { pub meta_attrs: Option>, } +impl KindedAttributes { + /// Check if a trait should be skipped from derive/implementation. + pub fn should_skip_derive(&self, name: &str) -> bool { + self.skip_derive + .as_ref() + .map(|traits| traits.iter().any(|t| t == name)) + .unwrap_or(false) + } +} + /// This uses the same names as serde + "Title Case" variant. /// Some names are different from what `convert_case` crate uses. #[derive(Debug, Clone, Copy)] @@ -175,3 +191,122 @@ impl DisplayCase { s.to_case(case) } } + +#[cfg(test)] +mod tests { + use super::*; + use syn::parse_quote; + + fn create_meta(kinded_attrs: KindedAttributes) -> Meta { + Meta { + vis: Visibility::Inherited, + ident: format_ident!("TestEnum"), + generics: Generics::default(), + variants: vec![], + kinded_attrs, + } + } + + #[test] + fn derive_traits_default() { + let meta = create_meta(KindedAttributes::default()); + let traits: Vec = meta + .derive_traits() + .iter() + .map(|p| quote!(#p).to_string()) + .collect(); + assert_eq!(traits, vec!["Debug", "Clone", "Copy", "PartialEq", "Eq"]); + } + + #[test] + fn derive_traits_with_extra() { + let meta = create_meta(KindedAttributes { + derive: Some(vec![parse_quote!(Hash), parse_quote!(Serialize)]), + ..Default::default() + }); + let traits: Vec = meta + .derive_traits() + .iter() + .map(|p| quote!(#p).to_string()) + .collect(); + assert_eq!( + traits, + vec![ + "Debug", + "Clone", + "Copy", + "PartialEq", + "Eq", + "Hash", + "Serialize" + ] + ); + } + + #[test] + fn derive_traits_skip_one() { + let meta = create_meta(KindedAttributes { + skip_derive: Some(vec![format_ident!("Clone")]), + ..Default::default() + }); + let traits: Vec = meta + .derive_traits() + .iter() + .map(|p| quote!(#p).to_string()) + .collect(); + assert_eq!(traits, vec!["Debug", "Copy", "PartialEq", "Eq"]); + } + + #[test] + fn derive_traits_skip_multiple() { + let meta = create_meta(KindedAttributes { + skip_derive: Some(vec![ + format_ident!("Clone"), + format_ident!("Copy"), + format_ident!("Eq"), + ]), + ..Default::default() + }); + let traits: Vec = meta + .derive_traits() + .iter() + .map(|p| quote!(#p).to_string()) + .collect(); + assert_eq!(traits, vec!["Debug", "PartialEq"]); + } + + #[test] + fn derive_traits_skip_all() { + let meta = create_meta(KindedAttributes { + skip_derive: Some(vec![ + format_ident!("Debug"), + format_ident!("Clone"), + format_ident!("Copy"), + format_ident!("PartialEq"), + format_ident!("Eq"), + ]), + ..Default::default() + }); + let traits: Vec = meta + .derive_traits() + .iter() + .map(|p| quote!(#p).to_string()) + .collect(); + assert!(traits.is_empty()); + } + + #[test] + fn derive_traits_skip_and_add() { + let meta = create_meta(KindedAttributes { + derive: Some(vec![parse_quote!(Hash)]), + skip_derive: Some(vec![format_ident!("Clone"), format_ident!("Copy")]), + ..Default::default() + }); + let traits: Vec = meta + .derive_traits() + .iter() + .map(|p| quote!(#p).to_string()) + .collect(); + assert_eq!(traits, vec!["Debug", "PartialEq", "Eq", "Hash"]); + } +} diff --git a/kinded_macros/src/parse.rs b/kinded_macros/src/parse.rs index de26a2f..3300946 100644 --- a/kinded_macros/src/parse.rs +++ b/kinded_macros/src/parse.rs @@ -168,6 +168,39 @@ impl Parse for KindedAttributes { let msg = format!("Duplicated attribute: {attr_name}"); return Err(syn::Error::new(attr_name.span(), msg)); } + } else if attr_name == "skip_derive" { + let skip_input; + parenthesized!(skip_input in input); + let parsed_traits = skip_input.parse_terminated(Ident::parse, Token![,])?; + let traits: Vec = parsed_traits.into_iter().collect(); + + // Validate that only allowed traits are specified + const ALLOWED_SKIP_DERIVE: &[&str] = &[ + "Debug", + "Clone", + "Copy", + "PartialEq", + "Eq", + "Display", + "FromStr", + "From", + ]; + for trait_name in &traits { + if !ALLOWED_SKIP_DERIVE.contains(&trait_name.to_string().as_str()) { + let msg = format!( + "Unknown trait to skip: `{trait_name}`. Allowed traits: {}", + ALLOWED_SKIP_DERIVE.join(", ") + ); + return Err(syn::Error::new(trait_name.span(), msg)); + } + } + + if kinded_attrs.skip_derive.is_none() { + kinded_attrs.skip_derive = Some(traits); + } else { + let msg = format!("Duplicated attribute: {attr_name}"); + return Err(syn::Error::new(attr_name.span(), msg)); + } } else if attr_name == "display" { let _: Token!(=) = input.parse()?; let case_lit_str: LitStr = input.parse()?; @@ -228,3 +261,110 @@ impl Parse for KindedAttributes { Ok(kinded_attrs) } } + +#[cfg(test)] +mod tests { + use super::*; + use quote::quote; + + fn parse_kinded_attrs(tokens: proc_macro2::TokenStream) -> syn::Result { + syn::parse2(tokens) + } + + #[test] + fn parse_skip_derive_single() { + let attrs = parse_kinded_attrs(quote! { #[kinded(skip_derive(Clone))] }).unwrap(); + let skip: Vec = attrs + .skip_derive + .unwrap() + .iter() + .map(|i| i.to_string()) + .collect(); + assert_eq!(skip, vec!["Clone"]); + } + + #[test] + fn parse_skip_derive_multiple() { + let attrs = + parse_kinded_attrs(quote! { #[kinded(skip_derive(Clone, Copy, Debug))] }).unwrap(); + let skip: Vec = attrs + .skip_derive + .unwrap() + .iter() + .map(|i| i.to_string()) + .collect(); + assert_eq!(skip, vec!["Clone", "Copy", "Debug"]); + } + + #[test] + fn parse_skip_derive_all_allowed() { + let attrs = parse_kinded_attrs(quote! { + #[kinded(skip_derive(Debug, Clone, Copy, PartialEq, Eq, Display, FromStr, From))] + }) + .unwrap(); + let skip: Vec = attrs + .skip_derive + .unwrap() + .iter() + .map(|i| i.to_string()) + .collect(); + assert_eq!( + skip, + vec![ + "Debug", + "Clone", + "Copy", + "PartialEq", + "Eq", + "Display", + "FromStr", + "From" + ] + ); + } + + #[test] + fn parse_skip_derive_with_other_attrs() { + let attrs = parse_kinded_attrs(quote! { + #[kinded(kind = MyKind, skip_derive(Clone, Copy), derive(Hash))] + }) + .unwrap(); + + assert_eq!(attrs.kind.unwrap().to_string(), "MyKind"); + + let skip: Vec = attrs + .skip_derive + .unwrap() + .iter() + .map(|i| i.to_string()) + .collect(); + assert_eq!(skip, vec!["Clone", "Copy"]); + + let derive: Vec = attrs + .derive + .unwrap() + .iter() + .map(|p| quote!(#p).to_string()) + .collect(); + assert_eq!(derive, vec!["Hash"]); + } + + #[test] + fn parse_skip_derive_invalid_trait() { + let result = parse_kinded_attrs(quote! { #[kinded(skip_derive(Hash))] }); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("Unknown trait to skip")); + assert!(err.contains("Hash")); + } + + #[test] + fn parse_skip_derive_duplicated() { + let result = parse_kinded_attrs(quote! { + #[kinded(skip_derive(Clone), skip_derive(Copy))] + }); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("Duplicated attribute")); + } +} diff --git a/test_suite/src/lib.rs b/test_suite/src/lib.rs index fe2bd33..3391acc 100644 --- a/test_suite/src/lib.rs +++ b/test_suite/src/lib.rs @@ -840,3 +840,189 @@ mod variant_attrs { assert_eq!(kind, ActionKind::Update); } } + +mod skip_derive { + use super::*; + + mod skip_display { + use super::*; + + #[derive(Kinded)] + #[kinded(skip_derive(Display))] + enum Priority { + Low, + High, + } + + // Display is skipped, so we can't use format!("{}", kind) + // but Debug should still work + #[test] + fn should_still_have_debug() { + let kind = PriorityKind::Low; + let debug_str = alloc::format!("{:?}", kind); + assert_eq!(debug_str, "Low"); + } + + #[test] + fn should_still_have_from_str() { + use core::str::FromStr; + let kind = PriorityKind::from_str("Low").unwrap(); + assert_eq!(kind, PriorityKind::Low); + } + + #[test] + fn should_still_have_clone_copy() { + let kind = PriorityKind::Low; + let cloned = kind.clone(); + let copied = kind; + assert_eq!(cloned, copied); + } + } + + mod skip_from_str { + use super::*; + + #[derive(Kinded)] + #[kinded(skip_derive(FromStr))] + enum Level { + Info, + Warn, + } + + // FromStr is skipped, but Display should still work + #[test] + fn should_still_have_display() { + let kind = LevelKind::Info; + let display_str = alloc::format!("{}", kind); + assert_eq!(display_str, "Info"); + } + + #[test] + fn should_still_have_from() { + let level = Level::Info; + let kind: LevelKind = level.into(); + assert_eq!(kind, LevelKind::Info); + } + } + + mod skip_from { + use super::*; + + #[derive(Kinded)] + #[kinded(skip_derive(From))] + enum Event { + Click, + Hover, + } + + // From is skipped, but kind() method should still work + #[test] + fn should_still_have_kind_method() { + let event = Event::Click; + assert_eq!(event.kind(), EventKind::Click); + } + + #[test] + fn should_still_have_display() { + let kind = EventKind::Click; + let display_str = alloc::format!("{}", kind); + assert_eq!(display_str, "Click"); + } + + #[test] + fn should_still_have_from_str() { + use core::str::FromStr; + let kind = EventKind::from_str("Click").unwrap(); + assert_eq!(kind, EventKind::Click); + } + } + + mod skip_multiple_impl_traits { + use super::*; + + #[derive(Kinded)] + #[kinded(skip_derive(Display, FromStr, From))] + enum Minimal { + A, + B, + } + + #[test] + fn should_still_have_debug() { + let kind = MinimalKind::A; + let debug_str = alloc::format!("{:?}", kind); + assert_eq!(debug_str, "A"); + } + + #[test] + fn should_still_have_partial_eq_eq() { + assert_eq!(MinimalKind::A, MinimalKind::A); + assert_ne!(MinimalKind::A, MinimalKind::B); + } + + #[test] + fn should_still_have_clone_copy() { + let kind = MinimalKind::A; + let cloned = kind.clone(); + let copied = kind; + assert_eq!(cloned, copied); + } + + #[test] + fn should_still_have_kind_method() { + let val = Minimal::A; + assert_eq!(val.kind(), MinimalKind::A); + } + + #[test] + fn should_still_have_all_method() { + assert_eq!(MinimalKind::all(), &[MinimalKind::A, MinimalKind::B]); + } + } + + mod skip_display_and_add_hash { + use super::*; + use core::hash::{Hash, Hasher}; + + #[derive(Kinded)] + #[kinded(skip_derive(Display), derive(Hash))] + enum Token { + Ident, + Number, + } + + #[test] + fn should_have_hash() { + // Verify Hash is implemented by using it + struct DummyHasher(u64); + impl Hasher for DummyHasher { + fn finish(&self) -> u64 { + self.0 + } + fn write(&mut self, bytes: &[u8]) { + for &b in bytes { + self.0 = self.0.wrapping_add(b as u64); + } + } + } + + let mut hasher = DummyHasher(0); + TokenKind::Ident.hash(&mut hasher); + let hash1 = hasher.finish(); + + let mut hasher = DummyHasher(0); + TokenKind::Number.hash(&mut hasher); + let hash2 = hasher.finish(); + + // Different variants should (likely) have different hashes + assert_ne!(hash1, hash2); + } + + #[test] + fn should_still_have_from_str() { + use core::str::FromStr; + let kind = TokenKind::from_str("Ident").unwrap(); + assert_eq!(kind, TokenKind::Ident); + } + } +} From fd95c7b045afa1cc5fc11e3069d5bf0ba6a84f67 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 19:12:13 +0100 Subject: [PATCH 2/8] Refactor: introduce enum Trait --- kinded_macros/src/generate/kind_enum.rs | 8 +- kinded_macros/src/models.rs | 103 +++++++++++++++++++----- kinded_macros/src/parse.rs | 89 ++++++++------------ 3 files changed, 119 insertions(+), 81 deletions(-) diff --git a/kinded_macros/src/generate/kind_enum.rs b/kinded_macros/src/generate/kind_enum.rs index e0ebe51..cc45420 100644 --- a/kinded_macros/src/generate/kind_enum.rs +++ b/kinded_macros/src/generate/kind_enum.rs @@ -1,4 +1,4 @@ -use crate::models::{DisplayCase, Meta, Variant}; +use crate::models::{DisplayCase, Meta, Trait, Variant}; use proc_macro2::{Ident, TokenStream}; use quote::quote; @@ -7,19 +7,19 @@ pub fn gen_kind_enum(meta: &Meta) -> TokenStream { let impl_kind_trait = gen_impl_kind_trait(meta); // Conditionally generate trait implementations based on skip_derive - let impl_from_traits = if meta.kinded_attrs.should_skip_derive("From") { + let impl_from_traits = if meta.kinded_attrs.should_skip(Trait::From) { quote!() } else { gen_impl_from_traits(meta) }; - let impl_display_trait = if meta.kinded_attrs.should_skip_derive("Display") { + let impl_display_trait = if meta.kinded_attrs.should_skip(Trait::Display) { quote!() } else { gen_impl_display_trait(meta) }; - let impl_from_str_trait = if meta.kinded_attrs.should_skip_derive("FromStr") { + let impl_from_str_trait = if meta.kinded_attrs.should_skip(Trait::FromStr) { quote!() } else { gen_impl_from_str_trait(meta) diff --git a/kinded_macros/src/models.rs b/kinded_macros/src/models.rs index 8ccac50..7b54308 100644 --- a/kinded_macros/src/models.rs +++ b/kinded_macros/src/models.rs @@ -2,6 +2,75 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use syn::{Generics, Meta as SynMeta, Path, Visibility}; +/// Traits that are automatically implemented for the generated kind enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Trait { + // Derived via #[derive(...)] + Debug, + Clone, + Copy, + PartialEq, + Eq, + // Manually implemented + Display, + FromStr, + From, +} + +impl Trait { + /// Traits that are derived by default via `#[derive(...)]`. + pub const fn default_derives() -> &'static [Trait] { + &[ + Trait::Debug, + Trait::Clone, + Trait::Copy, + Trait::PartialEq, + Trait::Eq, + ] + } + + /// All traits that can be skipped. + pub const fn all() -> &'static [Trait] { + &[ + Trait::Debug, + Trait::Clone, + Trait::Copy, + Trait::PartialEq, + Trait::Eq, + Trait::Display, + Trait::FromStr, + Trait::From, + ] + } + + pub const fn as_str(&self) -> &'static str { + match self { + Trait::Debug => "Debug", + Trait::Clone => "Clone", + Trait::Copy => "Copy", + Trait::PartialEq => "PartialEq", + Trait::Eq => "Eq", + Trait::Display => "Display", + Trait::FromStr => "FromStr", + Trait::From => "From", + } + } + + pub fn from_str(s: &str) -> Option { + match s { + "Debug" => Some(Trait::Debug), + "Clone" => Some(Trait::Clone), + "Copy" => Some(Trait::Copy), + "PartialEq" => Some(Trait::PartialEq), + "Eq" => Some(Trait::Eq), + "Display" => Some(Trait::Display), + "FromStr" => Some(Trait::FromStr), + "From" => Some(Trait::From), + _ => None, + } + } +} + #[derive(Debug)] pub struct Meta { /// Visibility of enum. @@ -30,12 +99,10 @@ impl Meta { /// Get the traits that need to be derived. pub fn derive_traits(&self) -> Vec { - const DEFAULT_DERIVE_TRAITS: &[&str] = &["Debug", "Clone", "Copy", "PartialEq", "Eq"]; - - let mut traits: Vec = DEFAULT_DERIVE_TRAITS + let mut traits: Vec = Trait::default_derives() .iter() - .filter(|name| !self.kinded_attrs.should_skip_derive(name)) - .map(|trait_name| Path::from(format_ident!("{trait_name}"))) + .filter(|t| !self.kinded_attrs.should_skip(**t)) + .map(|t| Path::from(format_ident!("{}", t.as_str()))) .collect(); // Add the extra specified traits, if they're different from the default ones @@ -98,7 +165,7 @@ pub struct KindedAttributes { /// Opt out default derives/implementations for traits like Debug, Clone, Copy, PartialEq, Eq, /// FromStr, Display, etc. It may be needed in some cases for compatibility with other crates /// that provide similar macros. See https://github.com/greyblake/kinded/pull/19 - pub skip_derive: Option>, + pub skip_derive: Option>, /// Attributes to customize implementation for Display trait pub display: Option, @@ -109,10 +176,10 @@ pub struct KindedAttributes { impl KindedAttributes { /// Check if a trait should be skipped from derive/implementation. - pub fn should_skip_derive(&self, name: &str) -> bool { + pub fn should_skip(&self, t: Trait) -> bool { self.skip_derive .as_ref() - .map(|traits| traits.iter().any(|t| t == name)) + .map(|traits| traits.contains(&t)) .unwrap_or(false) } } @@ -246,7 +313,7 @@ mod tests { #[test] fn derive_traits_skip_one() { let meta = create_meta(KindedAttributes { - skip_derive: Some(vec![format_ident!("Clone")]), + skip_derive: Some(vec![Trait::Clone]), ..Default::default() }); let traits: Vec = meta @@ -260,11 +327,7 @@ mod tests { #[test] fn derive_traits_skip_multiple() { let meta = create_meta(KindedAttributes { - skip_derive: Some(vec![ - format_ident!("Clone"), - format_ident!("Copy"), - format_ident!("Eq"), - ]), + skip_derive: Some(vec![Trait::Clone, Trait::Copy, Trait::Eq]), ..Default::default() }); let traits: Vec = meta @@ -279,11 +342,11 @@ mod tests { fn derive_traits_skip_all() { let meta = create_meta(KindedAttributes { skip_derive: Some(vec![ - format_ident!("Debug"), - format_ident!("Clone"), - format_ident!("Copy"), - format_ident!("PartialEq"), - format_ident!("Eq"), + Trait::Debug, + Trait::Clone, + Trait::Copy, + Trait::PartialEq, + Trait::Eq, ]), ..Default::default() }); @@ -299,7 +362,7 @@ mod tests { fn derive_traits_skip_and_add() { let meta = create_meta(KindedAttributes { derive: Some(vec![parse_quote!(Hash)]), - skip_derive: Some(vec![format_ident!("Clone"), format_ident!("Copy")]), + skip_derive: Some(vec![Trait::Clone, Trait::Copy]), ..Default::default() }); let traits: Vec = meta diff --git a/kinded_macros/src/parse.rs b/kinded_macros/src/parse.rs index 3300946..2ce0359 100644 --- a/kinded_macros/src/parse.rs +++ b/kinded_macros/src/parse.rs @@ -1,4 +1,4 @@ -use crate::models::{DisplayCase, FieldsType, KindedAttributes, Meta, Variant}; +use crate::models::{DisplayCase, FieldsType, KindedAttributes, Meta, Trait, Variant}; use proc_macro2::Ident; use quote::ToTokens; use syn::{ @@ -171,27 +171,22 @@ impl Parse for KindedAttributes { } else if attr_name == "skip_derive" { let skip_input; parenthesized!(skip_input in input); - let parsed_traits = skip_input.parse_terminated(Ident::parse, Token![,])?; - let traits: Vec = parsed_traits.into_iter().collect(); - - // Validate that only allowed traits are specified - const ALLOWED_SKIP_DERIVE: &[&str] = &[ - "Debug", - "Clone", - "Copy", - "PartialEq", - "Eq", - "Display", - "FromStr", - "From", - ]; - for trait_name in &traits { - if !ALLOWED_SKIP_DERIVE.contains(&trait_name.to_string().as_str()) { - let msg = format!( - "Unknown trait to skip: `{trait_name}`. Allowed traits: {}", - ALLOWED_SKIP_DERIVE.join(", ") - ); - return Err(syn::Error::new(trait_name.span(), msg)); + let parsed_idents = skip_input.parse_terminated(Ident::parse, Token![,])?; + + // Convert Idents to Trait enum values with validation + let mut traits: Vec = Vec::new(); + for ident in parsed_idents { + match Trait::from_str(&ident.to_string()) { + Some(t) => traits.push(t), + None => { + let allowed: Vec<&str> = + Trait::all().iter().map(|t| t.as_str()).collect(); + let msg = format!( + "Unknown trait to skip: `{ident}`. Allowed traits: {}", + allowed.join(", ") + ); + return Err(syn::Error::new(ident.span(), msg)); + } } } @@ -274,26 +269,16 @@ mod tests { #[test] fn parse_skip_derive_single() { let attrs = parse_kinded_attrs(quote! { #[kinded(skip_derive(Clone))] }).unwrap(); - let skip: Vec = attrs - .skip_derive - .unwrap() - .iter() - .map(|i| i.to_string()) - .collect(); - assert_eq!(skip, vec!["Clone"]); + let skip = attrs.skip_derive.unwrap(); + assert_eq!(skip, vec![Trait::Clone]); } #[test] fn parse_skip_derive_multiple() { let attrs = parse_kinded_attrs(quote! { #[kinded(skip_derive(Clone, Copy, Debug))] }).unwrap(); - let skip: Vec = attrs - .skip_derive - .unwrap() - .iter() - .map(|i| i.to_string()) - .collect(); - assert_eq!(skip, vec!["Clone", "Copy", "Debug"]); + let skip = attrs.skip_derive.unwrap(); + assert_eq!(skip, vec![Trait::Clone, Trait::Copy, Trait::Debug]); } #[test] @@ -302,23 +287,18 @@ mod tests { #[kinded(skip_derive(Debug, Clone, Copy, PartialEq, Eq, Display, FromStr, From))] }) .unwrap(); - let skip: Vec = attrs - .skip_derive - .unwrap() - .iter() - .map(|i| i.to_string()) - .collect(); + let skip = attrs.skip_derive.unwrap(); assert_eq!( skip, vec![ - "Debug", - "Clone", - "Copy", - "PartialEq", - "Eq", - "Display", - "FromStr", - "From" + Trait::Debug, + Trait::Clone, + Trait::Copy, + Trait::PartialEq, + Trait::Eq, + Trait::Display, + Trait::FromStr, + Trait::From, ] ); } @@ -332,13 +312,8 @@ mod tests { assert_eq!(attrs.kind.unwrap().to_string(), "MyKind"); - let skip: Vec = attrs - .skip_derive - .unwrap() - .iter() - .map(|i| i.to_string()) - .collect(); - assert_eq!(skip, vec!["Clone", "Copy"]); + let skip = attrs.skip_derive.unwrap(); + assert_eq!(skip, vec![Trait::Clone, Trait::Copy]); let derive: Vec = attrs .derive From baae43f74479a831ec999ab2c757883316e292ac Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 20:05:36 +0100 Subject: [PATCH 3/8] Refactor: Use HashSet to be more semantically precise instead of Vec --- kinded_macros/src/models.rs | 16 +++++++++------- kinded_macros/src/parse.rs | 20 +++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/kinded_macros/src/models.rs b/kinded_macros/src/models.rs index 7b54308..d177b89 100644 --- a/kinded_macros/src/models.rs +++ b/kinded_macros/src/models.rs @@ -1,9 +1,10 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; +use std::collections::HashSet; use syn::{Generics, Meta as SynMeta, Path, Visibility}; /// Traits that are automatically implemented for the generated kind enum. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Trait { // Derived via #[derive(...)] Debug, @@ -165,7 +166,7 @@ pub struct KindedAttributes { /// Opt out default derives/implementations for traits like Debug, Clone, Copy, PartialEq, Eq, /// FromStr, Display, etc. It may be needed in some cases for compatibility with other crates /// that provide similar macros. See https://github.com/greyblake/kinded/pull/19 - pub skip_derive: Option>, + pub skip_derive: Option>, /// Attributes to customize implementation for Display trait pub display: Option, @@ -262,6 +263,7 @@ impl DisplayCase { #[cfg(test)] mod tests { use super::*; + use std::collections::HashSet; use syn::parse_quote; fn create_meta(kinded_attrs: KindedAttributes) -> Meta { @@ -313,7 +315,7 @@ mod tests { #[test] fn derive_traits_skip_one() { let meta = create_meta(KindedAttributes { - skip_derive: Some(vec![Trait::Clone]), + skip_derive: Some(HashSet::from([Trait::Clone])), ..Default::default() }); let traits: Vec = meta @@ -327,7 +329,7 @@ mod tests { #[test] fn derive_traits_skip_multiple() { let meta = create_meta(KindedAttributes { - skip_derive: Some(vec![Trait::Clone, Trait::Copy, Trait::Eq]), + skip_derive: Some(HashSet::from([Trait::Clone, Trait::Copy, Trait::Eq])), ..Default::default() }); let traits: Vec = meta @@ -341,13 +343,13 @@ mod tests { #[test] fn derive_traits_skip_all() { let meta = create_meta(KindedAttributes { - skip_derive: Some(vec![ + skip_derive: Some(HashSet::from([ Trait::Debug, Trait::Clone, Trait::Copy, Trait::PartialEq, Trait::Eq, - ]), + ])), ..Default::default() }); let traits: Vec = meta @@ -362,7 +364,7 @@ mod tests { fn derive_traits_skip_and_add() { let meta = create_meta(KindedAttributes { derive: Some(vec![parse_quote!(Hash)]), - skip_derive: Some(vec![Trait::Clone, Trait::Copy]), + skip_derive: Some(HashSet::from([Trait::Clone, Trait::Copy])), ..Default::default() }); let traits: Vec = meta diff --git a/kinded_macros/src/parse.rs b/kinded_macros/src/parse.rs index 2ce0359..17c58f2 100644 --- a/kinded_macros/src/parse.rs +++ b/kinded_macros/src/parse.rs @@ -1,6 +1,7 @@ use crate::models::{DisplayCase, FieldsType, KindedAttributes, Meta, Trait, Variant}; use proc_macro2::Ident; use quote::ToTokens; +use std::collections::HashSet; use syn::{ Attribute, Data, DeriveInput, LitStr, Meta as SynMeta, Path, Token, bracketed, parenthesized, parse::{Parse, ParseStream}, @@ -174,10 +175,12 @@ impl Parse for KindedAttributes { let parsed_idents = skip_input.parse_terminated(Ident::parse, Token![,])?; // Convert Idents to Trait enum values with validation - let mut traits: Vec = Vec::new(); + let mut traits: HashSet = HashSet::new(); for ident in parsed_idents { match Trait::from_str(&ident.to_string()) { - Some(t) => traits.push(t), + Some(t) => { + traits.insert(t); + } None => { let allowed: Vec<&str> = Trait::all().iter().map(|t| t.as_str()).collect(); @@ -270,7 +273,7 @@ mod tests { fn parse_skip_derive_single() { let attrs = parse_kinded_attrs(quote! { #[kinded(skip_derive(Clone))] }).unwrap(); let skip = attrs.skip_derive.unwrap(); - assert_eq!(skip, vec![Trait::Clone]); + assert_eq!(skip, HashSet::from([Trait::Clone])); } #[test] @@ -278,7 +281,10 @@ mod tests { let attrs = parse_kinded_attrs(quote! { #[kinded(skip_derive(Clone, Copy, Debug))] }).unwrap(); let skip = attrs.skip_derive.unwrap(); - assert_eq!(skip, vec![Trait::Clone, Trait::Copy, Trait::Debug]); + assert_eq!( + skip, + HashSet::from([Trait::Clone, Trait::Copy, Trait::Debug]) + ); } #[test] @@ -290,7 +296,7 @@ mod tests { let skip = attrs.skip_derive.unwrap(); assert_eq!( skip, - vec![ + HashSet::from([ Trait::Debug, Trait::Clone, Trait::Copy, @@ -299,7 +305,7 @@ mod tests { Trait::Display, Trait::FromStr, Trait::From, - ] + ]) ); } @@ -313,7 +319,7 @@ mod tests { assert_eq!(attrs.kind.unwrap().to_string(), "MyKind"); let skip = attrs.skip_derive.unwrap(); - assert_eq!(skip, vec![Trait::Clone, Trait::Copy]); + assert_eq!(skip, HashSet::from([Trait::Clone, Trait::Copy])); let derive: Vec = attrs .derive From c5e03722f6e9932ce7e292d980752c533137ebfb Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 20:48:39 +0100 Subject: [PATCH 4/8] Add basic example --- Cargo.lock | 7 +++++ Cargo.toml | 1 + examples/basic/Cargo.toml | 7 +++++ examples/basic/src/main.rs | 63 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 examples/basic/Cargo.toml create mode 100644 examples/basic/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index a536904..5df0b84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,13 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "basic" +version = "0.1.0" +dependencies = [ + "kinded", +] + [[package]] name = "convert_case" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index e14247b..4e6f580 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ members = [ "kinded_macros", "sandbox", "test_suite", + "examples/basic", ] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml new file mode 100644 index 0000000..afe369c --- /dev/null +++ b/examples/basic/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "basic" +version = "0.1.0" +edition = "2024" + +[dependencies] +kinded = { path = "../../kinded" } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs new file mode 100644 index 0000000..8009f29 --- /dev/null +++ b/examples/basic/src/main.rs @@ -0,0 +1,63 @@ +use kinded::Kinded; + +#[derive(Kinded)] +enum Drink { + Mate, + Coffee(String), + Tea { variety: String, caffeine: bool }, +} + +fn main() { + // Create enum variants with associated data + let espresso = Drink::Coffee("Espresso".to_owned()); + let green_tea = Drink::Tea { + variety: "Sencha".to_owned(), + caffeine: true, + }; + let mate = Drink::Mate; + + // Use .kind() to get the kind without associated data + assert_eq!(espresso.kind(), DrinkKind::Coffee); + assert_eq!(green_tea.kind(), DrinkKind::Tea); + assert_eq!(mate.kind(), DrinkKind::Mate); + + // Get all kind variants + assert_eq!(DrinkKind::all(), [DrinkKind::Mate, DrinkKind::Coffee, DrinkKind::Tea]); + + // Pattern match on kind and access original data + let description = match &espresso { + Drink::Mate => "Just mate".to_owned(), + Drink::Coffee(name) => format!("Coffee: {name}"), + Drink::Tea { variety, caffeine } => { + format!("Tea: {variety}, caffeine: {caffeine}") + } + }; + assert_eq!(description, "Coffee: Espresso"); + + // Display trait + assert_eq!(DrinkKind::Coffee.to_string(), "Coffee"); + assert_eq!(DrinkKind::Tea.to_string(), "Tea"); + assert_eq!(DrinkKind::Mate.to_string(), "Mate"); + + // FromStr trait + assert_eq!("Tea".parse::().unwrap(), DrinkKind::Tea); + assert_eq!("Coffee".parse::().unwrap(), DrinkKind::Coffee); + assert_eq!("Mate".parse::().unwrap(), DrinkKind::Mate); + + // FromStr is case-insensitive + assert_eq!("tea".parse::().unwrap(), DrinkKind::Tea); + assert_eq!("TEA".parse::().unwrap(), DrinkKind::Tea); + + // From trait - convert from Drink to DrinkKind + assert_eq!(DrinkKind::from(&espresso), DrinkKind::Coffee); + assert_eq!(DrinkKind::from(&green_tea), DrinkKind::Tea); + assert_eq!(DrinkKind::from(&mate), DrinkKind::Mate); + + // Kind implements Copy, Clone, PartialEq, Eq, Debug + let kind = DrinkKind::Coffee; + let kind_copy = kind; // Copy + let kind_clone = kind.clone(); // Clone + assert_eq!(kind, kind_copy); // PartialEq + assert_eq!(kind, kind_clone); + assert_eq!(format!("{:?}", kind), "Coffee"); // Debug +} From 9c3717208a465a2534bfcf2ab3d6d531043701a3 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 21:35:04 +0100 Subject: [PATCH 5/8] Add enumset_example --- Cargo.lock | 79 ++++++++++++++++- Cargo.toml | 1 + examples/enumset_example/Cargo.toml | 8 ++ examples/enumset_example/src/main.rs | 122 +++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 examples/enumset_example/Cargo.toml create mode 100644 examples/enumset_example/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5df0b84..00cf1f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,81 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset_example" +version = "0.1.0" +dependencies = [ + "enumset", + "kinded", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "itoa" version = "1.0.10" @@ -43,9 +118,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 4e6f580..20dee46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ members = [ "sandbox", "test_suite", "examples/basic", + "examples/enumset_example", ] diff --git a/examples/enumset_example/Cargo.toml b/examples/enumset_example/Cargo.toml new file mode 100644 index 0000000..63b748d --- /dev/null +++ b/examples/enumset_example/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "enumset_example" +version = "0.1.0" +edition = "2024" + +[dependencies] +kinded = { path = "../../kinded" } +enumset = "1" diff --git a/examples/enumset_example/src/main.rs b/examples/enumset_example/src/main.rs new file mode 100644 index 0000000..151027f --- /dev/null +++ b/examples/enumset_example/src/main.rs @@ -0,0 +1,122 @@ +//! This example demonstrates how to use `kinded` with `enumset`. +//! +//! The problem: `enumset`'s `EnumSetType` only works on fieldless enums, +//! but you may have an enum with associated data that you want to use with EnumSet. +//! +//! The solution: Use `kinded` to generate a fieldless "kind" enum, then derive +//! `EnumSetType` on that kind enum. +//! +//! The catch: Both `kinded` and `enumset` implement `Copy`, `Clone`, `PartialEq`, `Eq` +//! by default, which causes trait implementation conflicts. +//! +//! The fix: Use `skip_derive` to prevent `kinded` from implementing those traits, +//! letting `enumset` handle them instead. + +use enumset::{EnumSet, EnumSetType}; +use kinded::Kinded; + +/// A permission with associated data - cannot directly use EnumSetType. +/// +/// We use kinded to generate `PermissionKind` (a fieldless enum), and: +/// - `skip_derive(Clone, Copy, PartialEq, Eq)` to avoid conflicts with enumset +/// - `derive(EnumSetType)` to make the kind enum work with EnumSet +/// - `attrs(enumset(...), repr(u8))` to configure enumset +#[derive(Kinded)] +#[kinded( + skip_derive(Clone, Copy, PartialEq, Eq), + derive(EnumSetType), + attrs(enumset(repr = "u8"), repr(u8)) +)] +enum Permission { + Read { path: String }, + Write { path: String }, + Execute { command: String }, + Admin, +} + +/// Check if a permission is allowed by a permission set, and return a description. +fn check_permission(perm: &Permission, allowed: EnumSet) -> String { + if allowed.contains(perm.kind()) { + match perm { + Permission::Read { path } => format!("Allowed to read: {path}"), + Permission::Write { path } => format!("Allowed to write: {path}"), + Permission::Execute { command } => format!("Allowed to execute: {command}"), + Permission::Admin => "Admin access granted".to_owned(), + } + } else { + format!("Permission denied: {:?}", perm.kind()) + } +} + +fn main() { + // Create permissions with associated data + let read_home = Permission::Read { + path: "/home".to_owned(), + }; + let write_tmp = Permission::Write { + path: "/tmp".to_owned(), + }; + let exec_ls = Permission::Execute { + command: "ls".to_owned(), + }; + let admin = Permission::Admin; + + // Extract kinds from permissions + assert_eq!(read_home.kind(), PermissionKind::Read); + assert_eq!(write_tmp.kind(), PermissionKind::Write); + assert_eq!(exec_ls.kind(), PermissionKind::Execute); + assert_eq!(admin.kind(), PermissionKind::Admin); + + // Now use EnumSet with the kind enum + let user_permissions: EnumSet = + PermissionKind::Read | PermissionKind::Write; + + let admin_permissions: EnumSet = EnumSet::all(); + + // Check if a permission kind is in the set + assert!(user_permissions.contains(PermissionKind::Read)); + assert!(user_permissions.contains(PermissionKind::Write)); + assert!(!user_permissions.contains(PermissionKind::Admin)); + + // Admin has all permissions + assert!(admin_permissions.contains(PermissionKind::Admin)); + assert!(admin_permissions.contains(PermissionKind::Execute)); + + // Check if a specific permission instance is allowed + assert!(user_permissions.contains(read_home.kind())); + assert!(user_permissions.contains(write_tmp.kind())); + assert!(!user_permissions.contains(exec_ls.kind())); + + // Set operations + let execute_only: EnumSet = EnumSet::only(PermissionKind::Execute); + let combined = user_permissions | execute_only; + assert_eq!(combined.len(), 3); + assert!(combined.contains(PermissionKind::Read)); + assert!(combined.contains(PermissionKind::Write)); + assert!(combined.contains(PermissionKind::Execute)); + + // Iterate over permission kinds in a set + let mut count = 0; + for _kind in user_permissions { + count += 1; + } + assert_eq!(count, 2); + + // Use the helper function to check permissions with their data + assert_eq!( + check_permission(&read_home, user_permissions), + "Allowed to read: /home" + ); + assert_eq!( + check_permission(&write_tmp, user_permissions), + "Allowed to write: /tmp" + ); + assert_eq!( + check_permission(&exec_ls, user_permissions), + "Permission denied: Execute" + ); + assert_eq!( + check_permission(&admin, admin_permissions), + "Admin access granted" + ); +} From fe70e37765b6e6935414eeb3b0bfb839d95d488c Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 22:17:59 +0100 Subject: [PATCH 6/8] Add Justfile to run checks locally --- Justfile | 35 ++++++++++++++++++++++++++++ examples/basic/src/main.rs | 6 ++++- examples/enumset_example/src/main.rs | 3 +-- kinded/src/lib.rs | 2 +- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 Justfile diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..59cf45a --- /dev/null +++ b/Justfile @@ -0,0 +1,35 @@ +all: fmt test-all clippy examples typos + +test-all: test test-doc + +test: + cargo test --workspace + +test-doc: + cd kinded && cargo test --doc + cd kinded_macros && cargo test --doc + +fmt: + cargo fmt + +clippy: + cargo clippy --workspace -- -D warnings + +examples: + #!/usr/bin/env bash + set -euxo pipefail + ROOT_DIR=$(pwd) + for EXAMPLE in $(ls examples); do + cd "$ROOT_DIR/examples/$EXAMPLE" + cargo run + done + +watch: + cargo watch -x 'test --workspace' + +watch-sandbox: + cargo watch -s "cd sandbox && cargo run" + +typos: + which typos >/dev/null || cargo install typos-cli + typos diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 8009f29..18c76c8 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -22,7 +22,10 @@ fn main() { assert_eq!(mate.kind(), DrinkKind::Mate); // Get all kind variants - assert_eq!(DrinkKind::all(), [DrinkKind::Mate, DrinkKind::Coffee, DrinkKind::Tea]); + assert_eq!( + DrinkKind::all(), + [DrinkKind::Mate, DrinkKind::Coffee, DrinkKind::Tea] + ); // Pattern match on kind and access original data let description = match &espresso { @@ -56,6 +59,7 @@ fn main() { // Kind implements Copy, Clone, PartialEq, Eq, Debug let kind = DrinkKind::Coffee; let kind_copy = kind; // Copy + #[allow(clippy::clone_on_copy)] let kind_clone = kind.clone(); // Clone assert_eq!(kind, kind_copy); // PartialEq assert_eq!(kind, kind_clone); diff --git a/examples/enumset_example/src/main.rs b/examples/enumset_example/src/main.rs index 151027f..0794cf4 100644 --- a/examples/enumset_example/src/main.rs +++ b/examples/enumset_example/src/main.rs @@ -68,8 +68,7 @@ fn main() { assert_eq!(admin.kind(), PermissionKind::Admin); // Now use EnumSet with the kind enum - let user_permissions: EnumSet = - PermissionKind::Read | PermissionKind::Write; + let user_permissions: EnumSet = PermissionKind::Read | PermissionKind::Write; let admin_permissions: EnumSet = EnumSet::all(); diff --git a/kinded/src/lib.rs b/kinded/src/lib.rs index 2603040..7b2a78b 100644 --- a/kinded/src/lib.rs +++ b/kinded/src/lib.rs @@ -309,7 +309,7 @@ //! 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. //! My parents, my relatives and my friends had to survive the artillery and air attack, living for over a month in basements. //! -//! Some of them have managed to evacuate to EU. Some others are trying to live "normal lifes" in Kharkiv, doing there daily duties. +//! Some of them have managed to evacuate to EU. Some others are trying to live "normal lives" in Kharkiv, doing there daily duties. //! And some are at the front line right now, risking their lives every second to protect the rest. //! //! I encourage you to donate to [Charity foundation of Serhiy Prytula](https://prytulafoundation.org/en). From 55f490cf7d3cfaaa76a9f93c7571b0da9f5edd30 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 23:02:34 +0100 Subject: [PATCH 7/8] Prefer bacon over cargo watch --- Justfile | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Justfile b/Justfile index 59cf45a..0b959c4 100644 --- a/Justfile +++ b/Justfile @@ -25,10 +25,10 @@ examples: done watch: - cargo watch -x 'test --workspace' + bacon watch-sandbox: - cargo watch -s "cd sandbox && cargo run" + bacon run --path sandbox typos: which typos >/dev/null || cargo install typos-cli diff --git a/README.md b/README.md index 42dbc57..81fa550 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ But I am Ukrainian. The first 25 years of my life I spent in [Kharkiv](https://e 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. My parents, my relatives and my friends had to survive the artillery and air attack, living for over a month in basements. -Some of them have managed to evacuate to EU. Some others are trying to live "normal lifes" in Kharkiv, doing there daily duties. +Some of them have managed to evacuate to EU. Some others are trying to live "normal lives" in Kharkiv, doing there daily duties. And some are at the front line right now, risking their lives every second to protect the rest. I encourage you to donate to [Charity foundation of Serhiy Prytula](https://prytulafoundation.org/en). From 3efd8d262d7bc53c16a27dde4ecab8495261d4cc Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Mon, 2 Feb 2026 23:57:28 +0100 Subject: [PATCH 8/8] Adjus github CI to run examples --- .github/workflows/ci.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92c683b..c9de397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,4 +61,23 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: --workspace -- -D warnings + + examples: + name: Examples + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Run examples + run: | + for example in examples/*/; do + echo "Running $example" + cargo run --manifest-path "${example}Cargo.toml" + done