Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## Unreleased
- Add `attrs` attribute to pass extra attributes to the generated kind enum (e.g., `#[kinded(attrs(serde(rename_all = "snake_case")))]`)

## v0.4.1 - 2026-01-29
- Add `#[kinded(rename = "...")]` attribute for variants to customize display/parse names.
This is useful when the automatic case conversion doesn't produce the desired result.
Expand Down
57 changes: 51 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,35 @@ let mut drink_kinds = HashSet::new();
drink_kinds.insert(DrinkKind::Mate);
```

### Extra attributes

If you're using derive macros from other libraries like Serde or Strum, you may want to add
extra attributes specific to those libraries to the generated kind enum. You can do this using the `attrs` attribute:

```rs
use kinded::Kinded;
use serde::Serialize;

#[derive(Kinded, Serialize)]
#[kinded(derive(Serialize), attrs(serde(rename_all = "snake_case")))]
enum Drink {
VeryHotBlackTea,
Milk { fat: f64 },
}

let json = serde_json::to_string(&DrinkKind::VeryHotBlackTea).unwrap();
assert_eq!(json, r#""very_hot_black_tea""#);
```

You can pass multiple attributes:

```rs
#[kinded(attrs(
serde(rename_all = "camelCase"),
doc = "Kind of drink"
))]
```

### Display trait

Implementation of `Display` trait can be customized in the `serde` fashion:
Expand Down
21 changes: 21 additions & 0 deletions kinded/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@
//! drink_kinds.insert(DrinkKind::Mate);
//! ```
//!
//! ### Generic attributes
//!
//! If you're using derive traits from other libraries like Serde or Sqlx, you might want to add
//! extra attributes specific to those libraries. You can add these by using the `attrs` attribute:
//!
//! ```ignore
//! use kinded::Kinded;
//! use serde::Serialize;
//! use serde_json::json;
//!
//! #[derive(Kinded, Serialize)]
//! #[kinded(display = "snake_case", attrs(serde(rename_all = "snake_case")))]
//! enum Drink {
//! VeryHotBlackTea,
//! Milk { fat: f64 },
//! }
//!
//! let json_value = serde_json::to_value(&DrinkKind::VeryHotBlackTea);
//! assert_eq!(json_value, json!("very_hot_black_tea"));
//! ```
//!
//! ### Customize Display trait
//!
//! Implementation of `Display` trait can be customized in the `serde` fashion:
Expand Down
2 changes: 2 additions & 0 deletions kinded_macros/src/generate/kind_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ fn gen_definition(meta: &Meta) -> TokenStream {
let kind_name = meta.kind_name();
let variant_names: Vec<&Ident> = meta.variants.iter().map(|v| &v.ident).collect();
let traits = meta.derive_traits();
let attrs = meta.meta_attrs();

quote!(
#[derive(#(#traits),*)] // #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#(#[#attrs])* // #[serde(rename_all = "camelCase")]
#vis enum #kind_name { // pub enum DrinkKind {
#(#variant_names),* // Mate, Coffee, Tea
} // }
Expand Down
9 changes: 8 additions & 1 deletion kinded_macros/src/models.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{Generics, Path, Visibility};
use syn::{Generics, Meta as SynMeta, Path, Visibility};

#[derive(Debug)]
pub struct Meta {
Expand Down Expand Up @@ -55,6 +55,10 @@ impl Meta {

quote!(#type_name #generics)
}

pub fn meta_attrs(&self) -> Vec<SynMeta> {
self.kinded_attrs.meta_attrs.clone().unwrap_or_default()
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -90,6 +94,9 @@ pub struct KindedAttributes {

/// Attributes to customize implementation for Display trait
pub display: Option<DisplayCase>,

/// Extra attributes to apply to the generated kind enum (e.g., `#[serde(rename_all = "camelCase")]`).
pub meta_attrs: Option<Vec<SynMeta>>,
}

/// This uses the same names as serde + "Title Case" variant.
Expand Down
8 changes: 7 additions & 1 deletion kinded_macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::models::{DisplayCase, FieldsType, KindedAttributes, Meta, Variant};
use proc_macro2::Ident;
use quote::ToTokens;
use syn::{
Attribute, Data, DeriveInput, LitStr, Path, Token, bracketed, parenthesized,
Attribute, Data, DeriveInput, LitStr, Meta as SynMeta, Path, Token, bracketed, parenthesized,
parse::{Parse, ParseStream},
spanned::Spanned,
};
Expand Down Expand Up @@ -174,6 +174,12 @@ impl Parse for KindedAttributes {
let msg = format!("Duplicated attribute: {attr_name}");
return Err(syn::Error::new(attr_name.span(), msg));
}
} else if attr_name == "attrs" {
let derive_input;
parenthesized!(derive_input in input);

let parsed_attr = derive_input.parse_terminated(SynMeta::parse, Token![,])?;
kinded_attrs.meta_attrs = Some(parsed_attr.into_iter().collect());
} else {
let msg = format!("Unknown attribute: {attr_name}");
return Err(syn::Error::new(attr_name.span(), msg));
Expand Down
2 changes: 2 additions & 0 deletions test_suite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ edition = "2024"

[dependencies]
kinded = { path = "../kinded" }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
124 changes: 123 additions & 1 deletion test_suite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
extern crate alloc;

use kinded::Kinded;
use serde::{Deserialize, Serialize};

#[derive(Kinded)]
#[derive(Kinded, Serialize, Deserialize)]
#[kinded(derive(Serialize), attrs(serde(rename_all = "camelCase")))]
enum Role {
Guest,
User(i32),
Expand Down Expand Up @@ -295,6 +297,126 @@ mod kind_enum {
)
}
}

mod attrs {
use crate::RoleKind;
use alloc::string::ToString;
use serde::{Deserialize, Serialize};
use serde_json::json;

#[test]
fn should_apply_single_attr() {
// RoleKind has serde(rename_all = "camelCase") applied
let value = serde_json::to_value(&RoleKind::Guest).unwrap();
assert_eq!(value, json!("guest"));
}

#[test]
fn should_apply_multiple_attrs() {
#[derive(kinded::Kinded, Serialize, Deserialize)]
#[kinded(
derive(Serialize, Deserialize),
attrs(
serde(rename_all = "snake_case"),
doc = "This is a generated kind enum"
)
)]
enum Vehicle {
SportsCar,
PickupTruck,
MotorCycle,
}

// Test serialization with snake_case
let value = serde_json::to_value(&VehicleKind::SportsCar).unwrap();
assert_eq!(value, json!("sports_car"));

let value = serde_json::to_value(&VehicleKind::PickupTruck).unwrap();
assert_eq!(value, json!("pickup_truck"));

// Test deserialization
let kind: VehicleKind = serde_json::from_str(r#""motor_cycle""#).unwrap();
assert_eq!(kind, VehicleKind::MotorCycle);
}

#[test]
fn should_work_with_name_value_attr() {
#[derive(kinded::Kinded)]
#[kinded(attrs(doc = "A beverage kind"))]
enum Beverage {
Water,
Juice,
}

// If it compiles, the doc attribute was applied correctly
let _ = BeverageKind::Water;
}

#[test]
fn should_combine_with_kind_attr() {
#[derive(kinded::Kinded, Serialize)]
#[kinded(
kind = AnimalType,
derive(Serialize),
attrs(serde(rename_all = "SCREAMING_SNAKE_CASE"))
)]
enum Animal {
DomesticCat,
WildDog,
}

let value = serde_json::to_value(&AnimalType::DomesticCat).unwrap();
assert_eq!(value, json!("DOMESTIC_CAT"));

let value = serde_json::to_value(&AnimalType::WildDog).unwrap();
assert_eq!(value, json!("WILD_DOG"));
}

#[test]
fn should_combine_with_display_attr() {
#[derive(kinded::Kinded, Serialize)]
#[kinded(
display = "kebab-case",
derive(Serialize),
attrs(serde(rename_all = "kebab-case"))
)]
enum Fruit {
GreenApple,
RedCherry,
}

// Display should use kebab-case
assert_eq!(FruitKind::GreenApple.to_string(), "green-apple");

// Serde should also use kebab-case
let value = serde_json::to_value(&FruitKind::RedCherry).unwrap();
assert_eq!(value, json!("red-cherry"));
}

#[test]
fn should_support_deserialization() {
#[derive(kinded::Kinded, Serialize, Deserialize, PartialEq, Debug)]
#[kinded(derive(Serialize, Deserialize), attrs(serde(rename_all = "camelCase")))]
enum Planet {
MilkyWayEarth,
RedMars,
}

// Serialize
let json = serde_json::to_string(&PlanetKind::MilkyWayEarth).unwrap();
assert_eq!(json, r#""milkyWayEarth""#);

// Deserialize
let kind: PlanetKind = serde_json::from_str(r#""redMars""#).unwrap();
assert_eq!(kind, PlanetKind::RedMars);

// Round-trip
let original = PlanetKind::MilkyWayEarth;
let json = serde_json::to_string(&original).unwrap();
let restored: PlanetKind = serde_json::from_str(&json).unwrap();
assert_eq!(original, restored);
}
}
}

#[test]
Expand Down
Loading