diff --git a/README.md b/README.md index cd0f008..cec1ad6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Motivation ---------- Instead of trying to be everything or deriving dozens of unused trait implementations, -this crate provides unique, simple, yet powerful tools for your `newtypes`. +this crate provides unique, simple, yet powerful tools for the `newtypes`. The crate focuses on three main areas to make `newtype` usage more enjoyable: @@ -26,8 +26,6 @@ The crate focuses on three main areas to make `newtype` usage more enjoyable: Usage ----- -Adding the crate to your project: - ```bash cargo add newtype-tools ``` @@ -35,7 +33,37 @@ cargo add newtype-tools Examples -------- -Conversion between types: +The simplest way to use the crate is to declare a tuple struct as a `newtype` kind: + +```rust +# #[cfg(feature = "derive")] +# { +#[newtype_tools::newtype(Amount)] +struct Apples(u64); + +// Now the `Apples`behave pretty much as their inner type `u64`: +let apple1 = Apples(2); +// `Apples` can be converted from the inner type: +let apple2 = Apples::from(3); +// `Apples` can be added, subtracted and compared: +assert_eq!(apple1 + apple2, Apples(5)); +// `Apples` can be multiplied by the inner factor: +assert_eq!(apple1 * 2_u64, Apples(4)); +// `Apples` can be divided, returning a inner ratio: +assert_eq!(apple2 / apple1 , 1); +// `Apples` can be easily extended: +impl core::fmt::Display for Apples { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.0, f) + } +} +# } +``` + +The crate supports two kinds of `newtypes`: `Amount` and `Id`. See below for more details. + +Rather than using the predefined sets of derives, the implementation allows +for the derivation of only the necessary traits. Conversion between types: ```rust # #[cfg(feature = "derive")] @@ -50,6 +78,7 @@ struct Apples(u64); struct Oranges(u32); let apples = Apples(42); +// `Oranges` can now be created from `Apples`: let oranges = Oranges::from(apples); assert_eq!(oranges.0, 21); @@ -73,6 +102,7 @@ struct Oranges(u32); let apples = Apples(42); let oranges = Oranges(21); +// `Apples` and `Oranges` can now be compared: assert!(apples == oranges); # } ``` @@ -88,6 +118,7 @@ use newtype_tools::{Newtype, Iter}; struct Apples(u64); let range = Apples(0)..Apples(42); +// The range of `Apples` can now be iterated: for apple in range.iter() { println!("{apple:?}"); } @@ -96,6 +127,39 @@ for apple in range.iter() { This will become even more ergonomic once the [Step][step] trait is stabilized. +Newtype Kinds +------------- + +The crate supports predefined sets of newtype properties. The concept is similar +to the `phantom_newtype` crate but avoids its limitations, as the newtype +generated here is a distinct Rust type. This allows new traits +to be implemented easily for the type and makes the set of derived traits +simple to extend. + +The supported `newtype` kinds are: + +| Trait | `#[newtype(Amount)]` | `#[newtype(Id)]` | +| ----------------- | :------------------: | :--------------: | +| `Clone` | ✔ | ✔ | +| `Copy` | ✔ | ✔ | +| `Debug` | ✔ | ✔ | +| `Default` | ✔ | ✔ | +| `Eq`¹ | ✔ | ✔ | +| `Hash`¹ | ✔ | ✔ | +| `Ord`¹ | ✔ | ✔ | +| `PartialEq` | ✔ | ✔ | +| `PartialOrd` | ✔ | ✔ | +| `From` | ✔ | ✔ | +| `Add` | ✔ | ✘ | +| `AddAssign` | ✔ | ✘ | +| `Sub` | ✔ | ✘ | +| `SubAssign` | ✔ | ✘ | +| `Mul` | ✔ | ✘ | +| `MulAssign` | ✔ | ✘ | +| `Div` | ✔ | ✘ | + +1. `Eq`, `Ord` and `Hash` are only implemented for integer inner types. + Alternatives ------------ diff --git a/newtype-tools-derive/src/expand.rs b/newtype-tools-derive/src/expand.rs index dc6c180..632443f 100644 --- a/newtype-tools-derive/src/expand.rs +++ b/newtype-tools-derive/src/expand.rs @@ -13,10 +13,13 @@ pub(crate) fn expand_newtype( ) -> proc_macro::TokenStream { let newtype = &attr.newtype; let inner_ty = &attr.inner_ty; + let (_impl_generics, newtype_generics, r#_where) = &attr.generics.split_for_impl(); + let full_newtype = quote::quote! { #newtype #newtype_generics }; + let standard_derives = quote::quote!(Clone, Copy, Debug, Default, PartialEq, PartialOrd); let standard_derives = if is_int_type(inner_ty) { quote::quote!( - #standard_derives, Eq, Ord, Hash + #standard_derives, Eq, Hash, Ord ) } else { standard_derives @@ -26,18 +29,24 @@ pub(crate) fn expand_newtype( #[automatically_derived] #[derive(newtype_tools::Newtype, #standard_derives)] #[newtype( - add(#newtype, output = #newtype, with = |l, r| #newtype(l.0 + r.0)), - add_assign(#newtype, with = |this, other| this.0 += other.0), - sub(#newtype, output = #newtype, with = |l, r| #newtype(l.0 - r.0)), - sub_assign(#newtype, with = |this, other| this.0 -= other.0), - mul(#inner_ty, output = #newtype, with = |l, inner| #newtype(l.0 * inner)), - mul_assign(#inner_ty, with = |this, inner| this.0 *= inner), - div(#newtype, output = #inner_ty, with = |l, r| l.0 / r.0) + add(#full_newtype, output = #full_newtype, with = |l, r| #newtype(l.0 + r.0)), + add_assign(#full_newtype, with = |this, other| this.0 += other.0), + sub(#full_newtype, output = #full_newtype, with = |l, r| #newtype(l.0 - r.0)), + sub_assign(#full_newtype, with = |this, other| this.0 -= other.0), + mul(#inner_ty, output = #full_newtype, with = |l, inner| #newtype(l.0 * *inner)), + mul_assign(#inner_ty, with = |this, inner| this.0 *= *inner), + div(#full_newtype, output = #inner_ty, with = |l, r| l.0 / r.0) )] // Guarantees the memory layout is identical to the inner type. #[repr(transparent)] } .into(), + NewtypeKind::Id => quote::quote! { + #[automatically_derived] + #[derive(newtype_tools::Newtype, #standard_derives)] + #[repr(transparent)] + } + .into(), }; derives.extend(item); derives diff --git a/newtype-tools-derive/src/lib.rs b/newtype-tools-derive/src/lib.rs index 2447880..10761de 100644 --- a/newtype-tools-derive/src/lib.rs +++ b/newtype-tools-derive/src/lib.rs @@ -6,30 +6,18 @@ mod expand; mod parse; /// Parses and expands a `newtype` attribute kind into a token stream. -/// The idea is similar to the `phantom_newtype` crate, but has no its limitations, -/// as the `newtype` is a real new Rust type. New traits could be easily implemented -/// for such a `newtype`, and the set of derived traits could be easily extended: +/// +/// The crate supports predefined sets of newtype properties. The concept is similar +/// to the `phantom_newtype` crate but avoids its limitations, as the newtype +/// generated here is a distinct Rust type. This allows new traits +/// to be implemented easily for the type and makes the set of derived traits +/// simple to extend. /// /// ```ignore /// #[newtype(Amount)] /// #[derive(Default)] /// struct Apples(u64); /// ``` -/// -/// The supported `newtype` kinds are: -/// -/// | Trait | `#[newtype(Amount)]` | `#[newtype(Id)]` | -/// |-------------------|:--------------------:|:----------------:| -/// | `PartialEq` | ✔ | ✔ | -/// | `PartialOrd` | ✔ | ✔ | -/// | `From` | ✔ | ✔ | -/// | `Add` | ✔ | ✘ | -/// | `AddAssign` | ✔ | ✘ | -/// | `Sub` | ✔ | ✘ | -/// | `SubAssign` | ✔ | ✘ | -/// | `Mul` | ✔ | ✘ | -/// | `MulAssign` | ✔ | ✘ | -/// | `Div` | ✔ | ✘ | #[proc_macro_attribute] pub fn newtype(attr: TokenStream, item: TokenStream) -> TokenStream { let kind = match parse::parse_newtype_kind(attr.into()) { @@ -156,12 +144,14 @@ struct NewtypeDerives { #[derive(Debug, PartialEq)] enum NewtypeKind { Amount, + Id, } impl core::fmt::Display for NewtypeKind { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Amount => f.write_str("Amount"), + Self::Id => f.write_str("Id"), } } } @@ -172,7 +162,8 @@ impl TryFrom<&syn::Ident> for NewtypeKind { fn try_from(value: &syn::Ident) -> Result { match value { ident if ident == "Amount" => Ok(Self::Amount), - _ => Err(syn::Error::new_spanned(value, "expected 'Amount'")), + ident if ident == "Id" => Ok(Self::Id), + _ => Err(syn::Error::new_spanned(value, "expected 'Amount' or 'Id'")), } } } @@ -291,6 +282,7 @@ mod tests { fn newtype_kind_display_roundtrip() { use super::NewtypeKind; assert_eq!(format!("{}", NewtypeKind::Amount), "Amount"); + assert_eq!(format!("{}", NewtypeKind::Id), "Id"); } #[test] diff --git a/newtype-tools/tests/newtype_amount.rs b/newtype-tools/tests/newtype_amount.rs index 60ea5c2..707c155 100644 --- a/newtype-tools/tests/newtype_amount.rs +++ b/newtype-tools/tests/newtype_amount.rs @@ -162,36 +162,49 @@ where // Manual generic newtype trait definition. #[derive(Clone, Copy, Debug, newtype_tools::Newtype, PartialEq, PartialOrd)] #[newtype( - add(ManGeneric, output = "ManGeneric", with = "|a1, a2| ManGeneric(a1.0 + a2.0)"), - add_assign(ManGeneric, with = "|this, other| this.0 += other.0"), - sub(ManGeneric, output = "ManGeneric", with = "|a1, a2| ManGeneric(a1.0 - a2.0)"), - sub_assign(ManGeneric, with = "|this, other| this.0 -= other.0"), - mul(R, output = "ManGeneric", with = "|a, r| ManGeneric(a.0 * *r)"), - mul_assign(R, with = "|this, r| this.0 *= *r"), - div(ManGeneric, output = "R", with = "|a1, a2| a1.0 / a2.0") + add(ManGeneric, output = "ManGeneric", with = "|a1, a2| ManGeneric(a1.0 + a2.0)"), + add_assign(ManGeneric, with = "|this, other| this.0 += other.0"), + sub(ManGeneric, output = "ManGeneric", with = "|a1, a2| ManGeneric(a1.0 - a2.0)"), + sub_assign(ManGeneric, with = "|this, other| this.0 -= other.0"), + mul(T, output = "ManGeneric", with = "|a, r| ManGeneric(a.0 * *r)"), + mul_assign(T, with = "|this, r| this.0 *= *r"), + div(ManGeneric, output = "T", with = "|a1, a2| a1.0 / a2.0") )] #[repr(transparent)] -struct ManGeneric(T) +struct ManGeneric(T) where T: Clone + Copy + PartialEq + PartialOrd + core::fmt::Debug - + From + + From + core::ops::Add + core::ops::AddAssign + core::ops::Sub + core::ops::SubAssign - + core::ops::Mul - + core::ops::MulAssign - + core::ops::Div, - R: Copy + PartialOrd; + + core::ops::Mul + + core::ops::MulAssign + + core::ops::Div; // Attribute generic newtype trait definition. #[newtype_tools::newtype(Amount)] /// Doc comment. -struct AttrGeneric(f64); +struct AttrGeneric(T) +where + T: Clone + + Copy + + PartialEq + + PartialOrd + + core::fmt::Debug + + From + + core::ops::Add + + core::ops::AddAssign + + core::ops::Sub + + core::ops::SubAssign + + core::ops::Mul + + core::ops::MulAssign + + core::ops::Div; #[rstest::rstest] #[case::generic(2.0_f64, 3.0_f64, 2.0_f64)] diff --git a/newtype-tools/tests/newtype_id.rs b/newtype-tools/tests/newtype_id.rs new file mode 100644 index 0000000..d02b689 --- /dev/null +++ b/newtype-tools/tests/newtype_id.rs @@ -0,0 +1,101 @@ +#![cfg(feature = "derive")] + +// Manual integer newtype trait definition. +#[derive(Clone, Copy, Debug, Eq, Hash, newtype_tools::Newtype, Ord, PartialEq, PartialOrd)] +#[repr(transparent)] +struct ManInt(i64); + +// Attribute integer newtype trait definition. +#[newtype_tools::newtype(Id)] +/// Doc comment. +struct AttrInt(i64); + +#[rstest::rstest] +#[case::i64(2_i64, 3_i64, 2_i64)] +#[case::man_int(ManInt(2_i64), ManInt(3_i64), 2_i64)] +#[case::attr_int(AttrInt(2_i64), AttrInt(3_i64), 2_i64)] +#[timeout(core::time::Duration::from_secs(1))] +fn int_newtype_id(#[case] a: T, #[case] b: T, #[case] repr_a: R) +where + T: Clone + Copy + PartialEq + PartialOrd + Ord + core::hash::Hash + core::fmt::Debug + From, + R: Copy + PartialOrd, +{ + // PartialEq + assert!(a != b); + // PartialOrd + assert!(a < b); + // Ord + assert_eq!(a.min(b), a); + // Hash + let mut hasher = std::hash::DefaultHasher::new(); + let prev_a = a; + a.hash(&mut hasher); + assert_eq!(prev_a, a); + // From + let res = T::from(repr_a); + assert!(res == a); +} + +// Manual floating point newtype trait definition. +#[derive(Clone, Copy, Debug, newtype_tools::Newtype, PartialEq, PartialOrd)] +#[repr(transparent)] +struct ManFloat(f64); + +// Attribute floating point newtype trait definition. +#[newtype_tools::newtype(Id)] +/// Doc comment. +struct AttrFloat(f64); + +#[rstest::rstest] +#[case::f64(2.0_f64, 3.0_f64, 2.0_f64)] +#[case::man_float(ManFloat(2.0_f64), ManFloat(3.0_f64), 2.0_f64)] +#[case::attr_float(AttrFloat(2.0_f64), AttrFloat(3.0_f64), 2.0_f64)] +#[timeout(core::time::Duration::from_secs(1))] +fn float_newtype_id(#[case] a: T, #[case] b: T, #[case] repr_a: R) +where + T: Clone + Copy + PartialEq + PartialOrd + core::fmt::Debug + From, + R: Copy + PartialOrd, +{ + // PartialEq + assert!(a != b); + // PartialOrd + assert!(a < b); + // Ord: is not implemented for f64 + // Hash: is not implemented for f64 + // From + let res = T::from(repr_a); + assert!(res == a); +} + +// Manual generic newtype trait definition. +#[derive(Clone, Copy, Debug, newtype_tools::Newtype, PartialEq, PartialOrd)] +#[repr(transparent)] +struct ManGeneric(T) +where + T: Clone + Copy + PartialEq + PartialOrd + core::fmt::Debug + From; +// Attribute generic newtype trait definition. +#[newtype_tools::newtype(Id)] +/// Doc comment. +struct AttrGeneric(T) +where + T: Clone + Copy + PartialEq + PartialOrd + core::fmt::Debug + From; +#[rstest::rstest] +#[case::generic(2.0_f64, 3.0_f64, 2.0_f64)] +#[case::man_generic(ManGeneric(2.0_f64), ManGeneric(3.0_f64), 2.0_f64)] +#[case::attr_generic(AttrGeneric(2.0_f64), AttrGeneric(3.0_f64), 2.0_f64)] +#[timeout(core::time::Duration::from_secs(1))] +fn generic_float_newtype_id(#[case] a: T, #[case] b: T, #[case] repr_a: R) +where + T: Clone + Copy + PartialEq + PartialOrd + core::fmt::Debug + From, + R: Copy + PartialOrd + core::ops::Mul + core::ops::MulAssign, +{ + // PartialEq + assert!(a != b); + // PartialOrd + assert!(a < b); + // Ord: is not implemented for f64 + // Hash: is not implemented for f64 + // From + let res = T::from(repr_a); + assert!(res == a); +} diff --git a/newtype-tools/tests/trybuild/newtype_attribute.rs b/newtype-tools/tests/trybuild/newtype_attribute.rs index 5de9d7b..094c19f 100644 --- a/newtype-tools/tests/trybuild/newtype_attribute.rs +++ b/newtype-tools/tests/trybuild/newtype_attribute.rs @@ -1,6 +1,8 @@ /// `#[newtype(Amount)]` mod ok { + #[newtype_tools::newtype(Id)] + struct Apples(u64); #[newtype_tools::newtype(Amount)] struct Oranges(u32); } diff --git a/newtype-tools/tests/trybuild/newtype_attribute.stderr b/newtype-tools/tests/trybuild/newtype_attribute.stderr index a8e4f11..aaf0bad 100644 --- a/newtype-tools/tests/trybuild/newtype_attribute.stderr +++ b/newtype-tools/tests/trybuild/newtype_attribute.stderr @@ -1,39 +1,39 @@ error: expected `#[newtype(NewtypeKind)]` - --> tests/trybuild/newtype_attribute.rs:9:5 - | -9 | #[newtype_tools::newtype] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `newtype_tools::newtype` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/trybuild/newtype_attribute.rs:11:5 + | +11 | #[newtype_tools::newtype] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `newtype_tools::newtype` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected `#[newtype(NewtypeKind)]` - --> tests/trybuild/newtype_attribute.rs:14:5 + --> tests/trybuild/newtype_attribute.rs:16:5 | -14 | #[newtype_tools::newtype()] +16 | #[newtype_tools::newtype()] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `newtype_tools::newtype` (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected 'Amount' - --> tests/trybuild/newtype_attribute.rs:19:30 +error: expected 'Amount' or 'Id' + --> tests/trybuild/newtype_attribute.rs:21:30 | -19 | #[newtype_tools::newtype(InvalidKind)] +21 | #[newtype_tools::newtype(InvalidKind)] | ^^^^^^^^^^^ error: expected `struct Newtype(inner_type)` - --> tests/trybuild/newtype_attribute.rs:25:19 + --> tests/trybuild/newtype_attribute.rs:27:19 | -25 | struct Oranges(); +27 | struct Oranges(); | ^^ error: expected one of: `struct`, `enum`, `union` - --> tests/trybuild/newtype_attribute.rs:30:5 + --> tests/trybuild/newtype_attribute.rs:32:5 | -30 | fn not_a_struct() {} +32 | fn not_a_struct() {} | ^^ error: expected `#[newtype(NewtypeKind)]` - --> tests/trybuild/newtype_attribute.rs:34:30 + --> tests/trybuild/newtype_attribute.rs:36:30 | -34 | #[newtype_tools::newtype(Amount, Amount)] +36 | #[newtype_tools::newtype(Amount, Amount)] | ^^^^^^