diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b53b8..c7356df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - **[FIX]** Fix misleading error for value-type mismatches in validators (see [#241](https://github.com/greyblake/nutype/issues/241)). - **[FIX]** Improve rust-analyzer resilience: when `#[nutype(...)]` arguments fail to parse (e.g. while still being typed), emit a best-effort type skeleton alongside the error so the newtype stays resolvable and downstream completions keep working (see [#178](https://github.com/greyblake/nutype/issues/178)). - **[FIX]** Correct the validation error `Display` message for float newtypes: `less` now reads "The value must be less than ..." and `less_or_equal` reads "The value must be less or equal to ..." (the two were previously swapped). Integer and decimal were already correct. +- **[FIX]** Macro expansion is now reproducible: traits were collected into a `HashSet`, whose iteration order varies between expansions, so the order of derives and generated impls in the emitted code was nondeterministic. Traits are now kept in a `BTreeSet` and emitted in a stable order. - **[INTERNAL]** Consolidate the integer, float and decimal backends onto a shared numeric code-generation and validation layer (`common/generate/numeric.rs` and shared helpers in `common/validate.rs`), removing a large amount of duplicated code. No change to generated code or public API. ### v0.7.0 - 2026-04-25 diff --git a/nutype_macros/src/any/generate/mod.rs b/nutype_macros/src/any/generate/mod.rs index c719e59..1d96702 100644 --- a/nutype_macros/src/any/generate/mod.rs +++ b/nutype_macros/src/any/generate/mod.rs @@ -1,7 +1,7 @@ mod error; mod traits; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::quote; @@ -122,7 +122,7 @@ impl GenerateNewtype for AnyNewtype { type_name: &TypeName, generics: &Generics, inner_type: &Self::InnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &AnyGuard, @@ -148,7 +148,7 @@ impl GenerateNewtype for AnyNewtype { _inner_type: &Self::InnerType, maybe_default_value: &Option, guard: &Guard, - _traits: &HashSet, + _traits: &BTreeSet, ) -> TokenStream { let test_valid_default_value = gen_test_should_have_valid_default_value( type_name, diff --git a/nutype_macros/src/any/generate/traits/mod.rs b/nutype_macros/src/any/generate/traits/mod.rs index 6f0efe3..cc5cb00 100644 --- a/nutype_macros/src/any/generate/traits/mod.rs +++ b/nutype_macros/src/any/generate/traits/mod.rs @@ -1,9 +1,9 @@ pub mod arbitrary; pub mod into_iter; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; -use std::collections::HashSet; use crate::{ any::models::{AnyDeriveTrait, AnyGuard, AnyInnerType}, @@ -128,7 +128,7 @@ pub fn gen_traits( type_name: &TypeName, generics: &syn::Generics, inner_type: &AnyInnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &AnyGuard, diff --git a/nutype_macros/src/any/models.rs b/nutype_macros/src/any/models.rs index c60297d..8be6bda 100644 --- a/nutype_macros/src/any/models.rs +++ b/nutype_macros/src/any/models.rs @@ -24,7 +24,9 @@ pub enum AnyValidator { pub type SpannedAnyValidator = SpannedItem; -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +// Note that the order in which the variants are declared here is the order +// in which traits are derived and implemented in the generated code. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] pub enum AnyDeriveTrait { // Standard Debug, diff --git a/nutype_macros/src/common/generate/mod.rs b/nutype_macros/src/common/generate/mod.rs index 65fc419..d5f11d1 100644 --- a/nutype_macros/src/common/generate/mod.rs +++ b/nutype_macros/src/common/generate/mod.rs @@ -6,8 +6,8 @@ pub mod parse_error; pub mod tests; pub mod traits; +use alloc::collections::BTreeSet; use core::hash::Hash; -use std::collections::HashSet; use self::traits::GeneratedTraits; @@ -220,7 +220,7 @@ pub trait GenerateNewtype { type_name: &TypeName, generics: &Generics, inner_type: &Self::InnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &Guard, @@ -539,7 +539,7 @@ pub trait GenerateNewtype { inner_type: &Self::InnerType, maybe_default_value: &Option, guard: &Guard, - traits: &HashSet, + traits: &BTreeSet, ) -> TokenStream; } @@ -548,7 +548,7 @@ pub trait GenerateNewtype { /// because nutype weaves the custom functions into its own generated impls. fn validate_serde_customization( serde_customization: &SerdeCustomization, - traits: &HashSet, + traits: &BTreeSet, conditional_derives: &[ConditionalDeriveGroup], ) -> Result<(), syn::Error> { let has_serialize = traits.iter().any(|t| t.is_serde_serialize()) diff --git a/nutype_macros/src/common/generate/traits.rs b/nutype_macros/src/common/generate/traits.rs index 2c3bf4f..09466e7 100644 --- a/nutype_macros/src/common/generate/traits.rs +++ b/nutype_macros/src/common/generate/traits.rs @@ -1,5 +1,4 @@ -use core::hash::Hash; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -47,7 +46,7 @@ pub struct GeneratableTraits { } pub fn split_into_generatable_traits( - input_traits: HashSet, + input_traits: BTreeSet, ) -> GeneratableTraits where GeneratableTrait: From, @@ -109,7 +108,7 @@ pub fn process_conditional_derives gen_impl_traits: impl Fn(Vec) -> Result, ) -> Result where - InputTrait: Eq + Hash + Clone, + InputTrait: Ord + Clone, TransparentTrait: ToTokens, IrregularTrait: HasGeneratedParseError, GeneratableTrait: From, @@ -121,7 +120,7 @@ where for group in conditional_derives { let pred = &group.predicate; - let cond_traits: HashSet = group.typed_traits.iter().cloned().collect(); + let cond_traits: BTreeSet = group.typed_traits.iter().cloned().collect(); let GeneratableTraits { transparent_traits: cond_transparent, irregular_traits: cond_irregular, diff --git a/nutype_macros/src/common/models.rs b/nutype_macros/src/common/models.rs index 9c51f17..aec0b7f 100644 --- a/nutype_macros/src/common/models.rs +++ b/nutype_macros/src/common/models.rs @@ -1,8 +1,8 @@ mod error_type_path; +use alloc::collections::BTreeSet; use core::{fmt::Debug, ops::Add}; use kinded::Kinded; -use std::collections::HashSet; use syn::Generics; use proc_macro2::{Span, TokenStream}; @@ -493,7 +493,7 @@ pub struct CfgAttrEntry { /// and conditional derive entries. pub struct ValidatedDerives { /// Typed traits from unconditional `derive(...)`. - pub unconditional: HashSet, + pub unconditional: BTreeSet, /// Typed traits from `cfg_attr(...)` entries, grouped by predicate. pub conditional: Vec>, @@ -696,7 +696,7 @@ impl ToTokens for ConstructorVisibility { pub struct GenerateParams { pub inner_type: IT, pub doc_attrs: Vec, - pub traits: HashSet, + pub traits: BTreeSet, pub unsafe_traits: Vec, pub vis: syn::Visibility, pub type_name: TypeName, diff --git a/nutype_macros/src/common/parse/meta.rs b/nutype_macros/src/common/parse/meta.rs index 8c1e584..38e7a32 100644 --- a/nutype_macros/src/common/parse/meta.rs +++ b/nutype_macros/src/common/parse/meta.rs @@ -14,7 +14,7 @@ use crate::{ }; pub fn parse_meta(token_stream: TokenStream) -> Result { - let input: DeriveInput = syn::parse(token_stream.into())?; + let input: DeriveInput = syn::parse2(token_stream)?; let input_span = input.span(); let DeriveInput { diff --git a/nutype_macros/src/common/validate.rs b/nutype_macros/src/common/validate.rs index d6095fe..27ad17f 100644 --- a/nutype_macros/src/common/validate.rs +++ b/nutype_macros/src/common/validate.rs @@ -1,4 +1,4 @@ -use core::hash::Hash; +use alloc::collections::BTreeSet; use kinded::Kinded; use proc_macro2::Span; use std::collections::HashSet; @@ -282,7 +282,7 @@ pub fn validate_all_derive_traits( convert: impl Fn(DeriveTrait, bool, Span) -> Result, ) -> Result, syn::Error> where - TypedTrait: Eq + Hash + TypeTrait, + TypedTrait: Ord + TypeTrait, { // 0. Check for unconditional-vs-conditional duplicates check_cfg_attr_no_duplicates(&derive_traits, cfg_attr_entries)?; @@ -302,7 +302,7 @@ where let unconditional = derive_traits .iter() .map(|st| convert(st.item, has_validation, st.span)) - .collect::, _>>()?; + .collect::, _>>()?; // 4. Convert conditional traits (same conversion, per entry) let conditional = cfg_attr_entries diff --git a/nutype_macros/src/decimal/generate/mod.rs b/nutype_macros/src/decimal/generate/mod.rs index 5c6a439..35e16e7 100644 --- a/nutype_macros/src/decimal/generate/mod.rs +++ b/nutype_macros/src/decimal/generate/mod.rs @@ -1,6 +1,6 @@ pub mod traits; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -70,7 +70,7 @@ where type_name: &TypeName, generics: &Generics, inner_type: &Self::InnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &DecimalGuard, @@ -96,7 +96,7 @@ where _inner_type: &Self::InnerType, maybe_default_value: &Option, guard: &Guard, - _traits: &HashSet, + _traits: &BTreeSet, ) -> TokenStream { let test_lower_vs_upper = guard.standard_validators().and_then(|validators| { gen_test_should_have_consistent_lower_and_upper_boundaries(type_name, validators) diff --git a/nutype_macros/src/decimal/generate/traits/mod.rs b/nutype_macros/src/decimal/generate/traits/mod.rs index 5a9b644..8f32832 100644 --- a/nutype_macros/src/decimal/generate/traits/mod.rs +++ b/nutype_macros/src/decimal/generate/traits/mod.rs @@ -1,6 +1,6 @@ mod arbitrary; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -28,7 +28,7 @@ pub fn gen_traits( type_name: &TypeName, generics: &Generics, inner_type: &DecimalInnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &DecimalGuard, diff --git a/nutype_macros/src/decimal/models.rs b/nutype_macros/src/decimal/models.rs index f57e7ec..94f1f93 100644 --- a/nutype_macros/src/decimal/models.rs +++ b/nutype_macros/src/decimal/models.rs @@ -153,7 +153,10 @@ pub type SpannedDecimalValidator = SpannedItem>; // * no `ValuableValuable` (`rust_decimal::Decimal` does not implement `Valuable`), // * `ArbitraryArbitrary` is supported (requires the user to enable // `rust_decimal/rust-fuzz`). -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +// +// Note that the order in which the variants are declared here is the order +// in which traits are derived and implemented in the generated code. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] pub enum DecimalDeriveTrait { // Standard Debug, diff --git a/nutype_macros/src/expansion_tests.rs b/nutype_macros/src/expansion_tests.rs new file mode 100644 index 0000000..71a2c04 --- /dev/null +++ b/nutype_macros/src/expansion_tests.rs @@ -0,0 +1,153 @@ +use crate::expand_nutype; +use proc_macro2::TokenStream; +use quote::quote; + +/// Expand the same input repeatedly and assert every expansion is byte-identical. +/// +/// Each collection built during expansion gets its own hasher state, so a `HashSet` +/// regression shows up as a difference between repeated expansions. +fn assert_reproducible(case: &str, attrs: TokenStream, type_definition: TokenStream) { + let expected = expand_nutype(attrs.clone(), type_definition.clone()) + .unwrap_or_else(|err| panic!("{case}: expansion should succeed: {err}")) + .to_string(); + + for _ in 0..100 { + let actual = expand_nutype(attrs.clone(), type_definition.clone()) + .unwrap_or_else(|err| panic!("{case}: expansion should succeed: {err}")) + .to_string(); + assert_eq!(actual, expected, "{case}: expansion is not reproducible"); + } +} + +/// `Serialize`/`Deserialize` are rejected unless the `serde` feature is on, so they are +/// only included in the derive list when the tests are run with that feature. +fn serde_derives() -> TokenStream { + #[cfg(feature = "serde")] + return quote!(Serialize, Deserialize,); + #[cfg(not(feature = "serde"))] + return TokenStream::new(); +} + +/// Same as [`serde_derives`], for the `arbitrary` feature. +fn arbitrary_derive() -> TokenStream { + #[cfg(feature = "arbitrary")] + return quote!(Arbitrary,); + #[cfg(not(feature = "arbitrary"))] + return TokenStream::new(); +} + +#[test] +fn string_expansion_is_reproducible() { + let serde = serde_derives(); + let arbitrary = arbitrary_derive(); + assert_reproducible( + "string", + quote!( + derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Default, AsRef, + Deref, Borrow, TryFrom, Into, FromStr, #serde #arbitrary + ), + default = "abc", + sanitize(trim, lowercase), + validate(len_char_min = 3, len_char_max = 5) + ), + quote!( + struct Name(String); + ), + ); +} + +#[test] +fn integer_expansion_is_reproducible() { + let serde = serde_derives(); + let arbitrary = arbitrary_derive(); + assert_reproducible( + "integer", + quote!( + derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Default, + AsRef, Deref, Borrow, TryFrom, Into, FromStr, #serde #arbitrary + ), + default = 5, + validate(greater_or_equal = 1, less_or_equal = 100) + ), + quote!( + struct Age(u8); + ), + ); +} + +#[test] +fn float_expansion_is_reproducible() { + let serde = serde_derives(); + let arbitrary = arbitrary_derive(); + // No `Hash`: it cannot be derived for float-based types. + assert_reproducible( + "float", + quote!( + derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Display, AsRef, Deref, + Borrow, TryFrom, Into, FromStr, #serde #arbitrary + ), + validate(finite) + ), + quote!( + struct Distance(f64); + ), + ); +} + +#[test] +fn any_expansion_is_reproducible() { + assert_reproducible( + "any", + quote!(derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, From, Into, AsRef, Deref, Borrow + )), + quote!( + struct Payload(Vec); + ), + ); +} + +#[cfg(feature = "rust_decimal")] +#[test] +fn decimal_expansion_is_reproducible() { + let serde = serde_derives(); + // No `Arbitrary`: it additionally requires `rust_decimal/rust-fuzz`. + assert_reproducible( + "decimal", + quote!( + derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Default, AsRef, + Deref, Borrow, TryFrom, Into, FromStr, #serde + ), + default = 1, + validate(greater_or_equal = 0, less_or_equal = 1000) + ), + quote!( + struct Price(Decimal); + ), + ); +} + +/// Conditional derives take a separate code path (`process_conditional_derives`), so they +/// need their own guard against a `HashSet` sneaking back in. +#[test] +fn conditional_derives_expansion_is_reproducible() { + assert_reproducible( + "cfg_attr", + quote!( + derive( + Debug, Clone, PartialEq, Eq, Hash, Display, AsRef, Deref, TryFrom, Into + ), + cfg_attr(unix, derive(FromStr, Borrow)), + cfg_attr(test, derive(PartialOrd, Ord, Default)), + default = "abc", + validate(len_char_min = 3) + ), + quote!( + struct Name(String); + ), + ); +} diff --git a/nutype_macros/src/float/generate/mod.rs b/nutype_macros/src/float/generate/mod.rs index 7b8e2a0..dd42a4d 100644 --- a/nutype_macros/src/float/generate/mod.rs +++ b/nutype_macros/src/float/generate/mod.rs @@ -1,6 +1,6 @@ pub mod traits; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -70,7 +70,7 @@ where type_name: &TypeName, generics: &Generics, inner_type: &Self::InnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &FloatGuard, @@ -96,7 +96,7 @@ where _inner_type: &Self::InnerType, maybe_default_value: &Option, guard: &Guard, - _traits: &HashSet, + _traits: &BTreeSet, ) -> TokenStream { let test_lower_vs_upper = guard.standard_validators().and_then(|validators| { gen_test_should_have_consistent_lower_and_upper_boundaries(type_name, validators) diff --git a/nutype_macros/src/float/generate/traits/mod.rs b/nutype_macros/src/float/generate/traits/mod.rs index 469aeeb..4f6c837 100644 --- a/nutype_macros/src/float/generate/traits/mod.rs +++ b/nutype_macros/src/float/generate/traits/mod.rs @@ -1,5 +1,5 @@ pub mod arbitrary; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -141,7 +141,7 @@ pub fn gen_traits( generics: &Generics, inner_type: &FloatInnerType, maybe_default_value: Option, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], guard: &FloatGuard, conditional_derives: &[ConditionalDeriveGroup], diff --git a/nutype_macros/src/float/models.rs b/nutype_macros/src/float/models.rs index 35008f2..6b2988d 100644 --- a/nutype_macros/src/float/models.rs +++ b/nutype_macros/src/float/models.rs @@ -65,7 +65,9 @@ pub type SpannedFloatValidator = SpannedItem>; // Traits // -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +// Note that the order in which the variants are declared here is the order +// in which traits are derived and implemented in the generated code. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] pub enum FloatDeriveTrait { // Standard Debug, diff --git a/nutype_macros/src/integer/generate/mod.rs b/nutype_macros/src/integer/generate/mod.rs index 6a19e8f..697129c 100644 --- a/nutype_macros/src/integer/generate/mod.rs +++ b/nutype_macros/src/integer/generate/mod.rs @@ -1,6 +1,6 @@ pub mod traits; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -70,7 +70,7 @@ where type_name: &TypeName, generics: &Generics, inner_type: &Self::InnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &IntegerGuard, @@ -96,7 +96,7 @@ where _inner_type: &Self::InnerType, maybe_default_value: &Option, guard: &Guard, - _traits: &HashSet, + _traits: &BTreeSet, ) -> TokenStream { let test_lower_vs_upper = guard.standard_validators().and_then(|validators| { gen_test_should_have_consistent_lower_and_upper_boundaries(type_name, validators) diff --git a/nutype_macros/src/integer/generate/traits/mod.rs b/nutype_macros/src/integer/generate/traits/mod.rs index 378296c..24d6893 100644 --- a/nutype_macros/src/integer/generate/traits/mod.rs +++ b/nutype_macros/src/integer/generate/traits/mod.rs @@ -1,6 +1,6 @@ mod arbitrary; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -28,7 +28,7 @@ pub fn gen_traits( type_name: &TypeName, generics: &Generics, inner_type: &IntegerInnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &IntegerGuard, diff --git a/nutype_macros/src/integer/models.rs b/nutype_macros/src/integer/models.rs index ddee2c6..7aa3854 100644 --- a/nutype_macros/src/integer/models.rs +++ b/nutype_macros/src/integer/models.rs @@ -63,7 +63,9 @@ pub type SpannedIntegerValidator = SpannedItem>; // Traits // -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +// Note that the order in which the variants are declared here is the order +// in which traits are derived and implemented in the generated code. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] pub enum IntegerDeriveTrait { // Standard Debug, diff --git a/nutype_macros/src/lib.rs b/nutype_macros/src/lib.rs index c38cb26..8a2f7b8 100644 --- a/nutype_macros/src/lib.rs +++ b/nutype_macros/src/lib.rs @@ -4,6 +4,8 @@ //! //! For more information please refer to [nutype](https://docs.rs/nutype) documentation. +extern crate alloc; + mod any; mod common; #[cfg(feature = "rust_decimal")] @@ -13,6 +15,9 @@ mod integer; mod string; mod utils; +#[cfg(test)] +mod expansion_tests; + use any::AnyNewtype; use common::{ models::{InnerType, Newtype, TypedMeta}, diff --git a/nutype_macros/src/string/generate/mod.rs b/nutype_macros/src/string/generate/mod.rs index 2457e5c..9f50a14 100644 --- a/nutype_macros/src/string/generate/mod.rs +++ b/nutype_macros/src/string/generate/mod.rs @@ -2,7 +2,7 @@ pub mod error; pub mod tests; pub mod traits; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::quote; @@ -208,7 +208,7 @@ impl GenerateNewtype for StringNewtype { type_name: &TypeName, generics: &Generics, _inner_type: &Self::InnerType, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &StringGuard, @@ -233,7 +233,7 @@ impl GenerateNewtype for StringNewtype { _inner_type: &Self::InnerType, maybe_default_value: &Option, guard: &Guard, - _traits: &HashSet, + _traits: &BTreeSet, ) -> TokenStream { let test_len_char_min_vs_max = guard.standard_validators().and_then(|validators| { tests::gen_test_should_have_consistent_len_char_boundaries(type_name, validators) diff --git a/nutype_macros/src/string/generate/traits/mod.rs b/nutype_macros/src/string/generate/traits/mod.rs index 189ea59..e7cbec2 100644 --- a/nutype_macros/src/string/generate/traits/mod.rs +++ b/nutype_macros/src/string/generate/traits/mod.rs @@ -1,6 +1,6 @@ pub mod arbitrary; -use std::collections::HashSet; +use alloc::collections::BTreeSet; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -158,7 +158,7 @@ impl ToTokens for StringTransparentTrait { pub fn gen_traits( type_name: &TypeName, generics: &Generics, - traits: HashSet, + traits: BTreeSet, unsafe_traits: &[SpannedDeriveUnsafeTrait], maybe_default_value: Option, guard: &StringGuard, @@ -338,3 +338,51 @@ fn gen_impl_borrow_str_and_string(type_name: &TypeName) -> TokenStream { #impl_borrow_str } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Traits travel through a `BTreeSet`, so the declaration order of the + /// `StringDeriveTrait` variants determines the order in which traits are derived and + /// implemented in the generated code. + #[test] + fn traits_are_split_in_declaration_order() { + // Deliberately built in an order that matches neither the declaration order nor + // alphabetical order. + let input: BTreeSet = [ + StringDeriveTrait::Display, + StringDeriveTrait::Eq, + StringDeriveTrait::FromStr, + StringDeriveTrait::Debug, + StringDeriveTrait::Hash, + StringDeriveTrait::Clone, + StringDeriveTrait::AsRef, + ] + .into_iter() + .collect(); + + let GeneratableTraits { + transparent_traits, + irregular_traits, + } = split_into_generatable_traits(input); + + assert_eq!( + transparent_traits, + vec![ + StringTransparentTrait::Debug, + StringTransparentTrait::Clone, + StringTransparentTrait::Eq, + StringTransparentTrait::Hash, + ] + ); + assert_eq!( + irregular_traits, + vec![ + StringIrregularTrait::FromStr, + StringIrregularTrait::AsRef, + StringIrregularTrait::Display, + ] + ); + } +} diff --git a/nutype_macros/src/string/models.rs b/nutype_macros/src/string/models.rs index b13f3a3..44ec8df 100644 --- a/nutype_macros/src/string/models.rs +++ b/nutype_macros/src/string/models.rs @@ -53,7 +53,9 @@ pub enum RegexDef { // Traits // -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +// Note that the order in which the variants are declared here is the order +// in which traits are derived and implemented in the generated code. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum StringDeriveTrait { // Standard Debug,