From 75535d1bd5a4407eab797671b930e932c93164ec Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 2 Dec 2023 13:50:16 +0100 Subject: [PATCH 1/5] Implement Arbitrary for integer types --- Cargo.lock | 13 ++- examples/integer_arbitrary/Cargo.toml | 11 +++ examples/integer_arbitrary/src/main.rs | 73 +++++++++++++++ nutype/Cargo.toml | 1 + nutype_macros/Cargo.toml | 1 + nutype_macros/src/any/gen/mod.rs | 3 +- nutype_macros/src/any/validate.rs | 5 ++ nutype_macros/src/common/gen/mod.rs | 2 + nutype_macros/src/common/models.rs | 3 + .../src/common/parse/derive_trait.rs | 9 ++ nutype_macros/src/float/gen/mod.rs | 3 +- nutype_macros/src/float/validate.rs | 5 ++ nutype_macros/src/integer/gen/mod.rs | 5 +- .../src/integer/gen/traits/arbitrary.rs | 89 +++++++++++++++++++ .../integer/gen/{traits.rs => traits/mod.rs} | 18 +++- nutype_macros/src/integer/models.rs | 2 +- nutype_macros/src/integer/validate.rs | 1 + nutype_macros/src/string/gen/mod.rs | 6 +- nutype_macros/src/string/validate.rs | 5 ++ 19 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 examples/integer_arbitrary/Cargo.toml create mode 100644 examples/integer_arbitrary/src/main.rs create mode 100644 nutype_macros/src/integer/gen/traits/arbitrary.rs rename nutype_macros/src/integer/gen/{traits.rs => traits/mod.rs} (93%) diff --git a/Cargo.lock b/Cargo.lock index abf7b0c..3ed75a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" [[package]] name = "arbtest" @@ -116,6 +116,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "integer_arbitrary" +version = "0.1.0" +dependencies = [ + "arbitrary", + "arbtest", + "nutype", +] + [[package]] name = "integer_bounded" version = "0.1.0" diff --git a/examples/integer_arbitrary/Cargo.toml b/examples/integer_arbitrary/Cargo.toml new file mode 100644 index 0000000..2fae226 --- /dev/null +++ b/examples/integer_arbitrary/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "integer_arbitrary" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +arbitrary = "1.3.2" +arbtest = "0.2.0" +nutype = { path = "../../nutype", features = ["arbitrary"] } diff --git a/examples/integer_arbitrary/src/main.rs b/examples/integer_arbitrary/src/main.rs new file mode 100644 index 0000000..e59481d --- /dev/null +++ b/examples/integer_arbitrary/src/main.rs @@ -0,0 +1,73 @@ +use arbitrary::Arbitrary; +use nutype::nutype; + +// Inclusive boundaries. 1 and 6 are included, so the value can only be 1, 2, 3, 4, 5 or 6. +#[nutype( + validate(greater_or_equal = 1, less_or_equal = 6), + derive(Arbitrary, Debug) +)] +struct GermanTaxClass(i128); + +// Exclusive boundaries. +// +// -2 and 2 are excluded, so the value can only be -1, 0 or 1. +#[nutype( + validate(greater = -2, less = 2), + derive(Arbitrary, Debug), +)] +struct MinusOneOrZeroOrOne(i128); + +// Since the upper limit for i8 is 127, the GreaterThan125 can only be 126 or 127. +#[nutype(validate(greater = 125), derive(Arbitrary, Debug))] +struct GreaterThan125(i8); + +// Since the upper limit for i8 is 127, the GreaterOrEqual125 can only be 125, 126 or 127. +#[nutype(validate(greater_or_equal = 125), derive(Arbitrary, Debug))] +struct GreaterOrEqual125(i8); + +// u128::MIN is 0, so the LessThan2 can only be 0, 1 +#[nutype(validate(less = 2), derive(Arbitrary, Debug))] +struct LessThan2(u128); + +// u128::MIN is 0, so the LessOrEqual2 can only be 0, 1, 2 +#[nutype(validate(less = 2), derive(Arbitrary, Debug))] +struct LessOrEqual2(u128); + +fn main() { + arbtest::builder().run(|u| { + let tax_class = GermanTaxClass::arbitrary(u)?.into_inner(); + assert!(tax_class >= 1); + assert!(tax_class <= 6); + Ok(()) + }); + + arbtest::builder().run(|u| { + let value = GreaterThan125::arbitrary(u)?.into_inner(); + assert!(value == 126 || value == 127); + Ok(()) + }); + + arbtest::builder().run(|u| { + let value = GreaterOrEqual125::arbitrary(u)?.into_inner(); + assert!(value == 125 || value == 126 || value == 127); + Ok(()) + }); + + arbtest::builder().run(|u| { + let value = MinusOneOrZeroOrOne::arbitrary(u)?.into_inner(); + assert!(value == -1 || value == 0 || value == 1); + Ok(()) + }); + + arbtest::builder().run(|u| { + let value = LessThan2::arbitrary(u)?.into_inner(); + assert!(value == 0 || value == 1); + Ok(()) + }); + + arbtest::builder().run(|u| { + let value = LessOrEqual2::arbitrary(u)?.into_inner(); + assert!(value == 0 || value == 1 || value == 2); + Ok(()) + }); +} diff --git a/nutype/Cargo.toml b/nutype/Cargo.toml index f8b666f..a65ed26 100644 --- a/nutype/Cargo.toml +++ b/nutype/Cargo.toml @@ -22,3 +22,4 @@ serde = ["nutype_macros/serde"] regex = ["nutype_macros/regex"] schemars08 = ["nutype_macros/schemars08"] new_unchecked = ["nutype_macros/new_unchecked"] +arbitrary = ["nutype_macros/arbitrary"] diff --git a/nutype_macros/Cargo.toml b/nutype_macros/Cargo.toml index efd047b..e916667 100644 --- a/nutype_macros/Cargo.toml +++ b/nutype_macros/Cargo.toml @@ -32,6 +32,7 @@ proc-macro = true serde = [] schemars08 = [] new_unchecked = [] +arbitrary = [] # nutype_test is set when unit tests for nutype is running. # Why: we don't want to generate unit tests when we're already within a unit test, because it results diff --git a/nutype_macros/src/any/gen/mod.rs b/nutype_macros/src/any/gen/mod.rs index 1a37ce8..750e3d7 100644 --- a/nutype_macros/src/any/gen/mod.rs +++ b/nutype_macros/src/any/gen/mod.rs @@ -15,7 +15,7 @@ use crate::common::{ use self::error::gen_validation_error_type; use super::{ - models::{AnyDeriveTrait, AnyInnerType, AnySanitizer, AnyValidator}, + models::{AnyDeriveTrait, AnyGuard, AnyInnerType, AnySanitizer, AnyValidator}, AnyNewtype, }; @@ -105,6 +105,7 @@ impl GenerateNewtype for AnyNewtype { maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, + _guard: &AnyGuard, ) -> GeneratedTraits { gen_traits( type_name, diff --git a/nutype_macros/src/any/validate.rs b/nutype_macros/src/any/validate.rs index ed4730b..31f6bf2 100644 --- a/nutype_macros/src/any/validate.rs +++ b/nutype_macros/src/any/validate.rs @@ -94,6 +94,11 @@ fn to_any_derive_trait( DeriveTrait::SerdeSerialize => Ok(AnyDeriveTrait::SerdeSerialize), DeriveTrait::SerdeDeserialize => Ok(AnyDeriveTrait::SerdeDeserialize), DeriveTrait::Hash => Ok(AnyDeriveTrait::Hash), + DeriveTrait::ArbitraryArbitrary => { + // TODO: Allow deriving Arbitrary if there is no validation + let msg = "Deriving Arbitrary trait for any type is not yet possible"; + Err(syn::Error::new(span, msg)) + } DeriveTrait::SchemarsJsonSchema => { let msg = format!("Deriving of trait `{tr:?}` is not (yet) supported for an arbitrary type"); diff --git a/nutype_macros/src/common/gen/mod.rs b/nutype_macros/src/common/gen/mod.rs index c444cfe..6e3d90f 100644 --- a/nutype_macros/src/common/gen/mod.rs +++ b/nutype_macros/src/common/gen/mod.rs @@ -180,6 +180,7 @@ pub trait GenerateNewtype { maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, + guard: &Guard, ) -> GeneratedTraits; fn gen_new_with_validation( @@ -328,6 +329,7 @@ pub trait GenerateNewtype { maybe_error_type_name, traits, maybe_default_value, + &guard, ); quote!( diff --git a/nutype_macros/src/common/models.rs b/nutype_macros/src/common/models.rs index 25692e0..c1ef359 100644 --- a/nutype_macros/src/common/models.rs +++ b/nutype_macros/src/common/models.rs @@ -263,6 +263,9 @@ pub enum DeriveTrait { #[cfg_attr(not(feature = "schemars08"), allow(dead_code))] SchemarsJsonSchema, + + #[cfg_attr(not(feature = "arbitrary"), allow(dead_code))] + ArbitraryArbitrary, } pub type SpannedDeriveTrait = SpannedItem; diff --git a/nutype_macros/src/common/parse/derive_trait.rs b/nutype_macros/src/common/parse/derive_trait.rs index 2d97c5d..740ce46 100644 --- a/nutype_macros/src/common/parse/derive_trait.rs +++ b/nutype_macros/src/common/parse/derive_trait.rs @@ -53,6 +53,15 @@ impl Parse for SpannedDeriveTrait { } } } + "Arbitrary" => { + cfg_if! { + if #[cfg(feature = "arbitrary")] { + DeriveTrait::ArbitraryArbitrary + } else { + return Err(syn::Error::new(ident.span(), "To derive Arbitrary, the feature `arbitrary` of the crate `nutype` needs to be enabled.")); + } + } + } _ => { return Err(syn::Error::new( ident.span(), diff --git a/nutype_macros/src/float/gen/mod.rs b/nutype_macros/src/float/gen/mod.rs index b6f4bdf..21e063d 100644 --- a/nutype_macros/src/float/gen/mod.rs +++ b/nutype_macros/src/float/gen/mod.rs @@ -8,7 +8,7 @@ use quote::{quote, ToTokens}; use self::error::gen_validation_error_type; use super::{ - models::{FloatDeriveTrait, FloatSanitizer, FloatType, FloatValidator}, + models::{FloatDeriveTrait, FloatGuard, FloatSanitizer, FloatType, FloatValidator}, FloatNewtype, }; use crate::{ @@ -132,6 +132,7 @@ where maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, + _guard: &FloatGuard, ) -> GeneratedTraits { gen_traits( type_name, diff --git a/nutype_macros/src/float/validate.rs b/nutype_macros/src/float/validate.rs index 7f379d9..58d4e40 100644 --- a/nutype_macros/src/float/validate.rs +++ b/nutype_macros/src/float/validate.rs @@ -196,5 +196,10 @@ fn to_float_derive_trait( DeriveTrait::SerdeSerialize => Ok(FloatDeriveTrait::SerdeSerialize), DeriveTrait::SerdeDeserialize => Ok(FloatDeriveTrait::SerdeDeserialize), DeriveTrait::SchemarsJsonSchema => Ok(FloatDeriveTrait::SchemarsJsonSchema), + DeriveTrait::ArbitraryArbitrary => { + // TODO: Implement deriving Arbitrary + let msg = "Deriving Arbitrary trait for float types is not yet implemented"; + Err(syn::Error::new(span, msg)) + } } } diff --git a/nutype_macros/src/integer/gen/mod.rs b/nutype_macros/src/integer/gen/mod.rs index 9f10224..d50192a 100644 --- a/nutype_macros/src/integer/gen/mod.rs +++ b/nutype_macros/src/integer/gen/mod.rs @@ -9,7 +9,8 @@ use quote::{quote, ToTokens}; use self::{error::gen_validation_error_type, traits::gen_traits}; use super::{ models::{ - IntegerDeriveTrait, IntegerInnerType, IntegerSanitizer, IntegerType, IntegerValidator, + IntegerDeriveTrait, IntegerGuard, IntegerInnerType, IntegerSanitizer, IntegerType, + IntegerValidator, }, IntegerNewtype, }; @@ -123,6 +124,7 @@ where maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, + guard: &IntegerGuard, ) -> GeneratedTraits { gen_traits( type_name, @@ -130,6 +132,7 @@ where maybe_error_type_name, traits, maybe_default_value, + guard, ) } } diff --git a/nutype_macros/src/integer/gen/traits/arbitrary.rs b/nutype_macros/src/integer/gen/traits/arbitrary.rs new file mode 100644 index 0000000..9a28a50 --- /dev/null +++ b/nutype_macros/src/integer/gen/traits/arbitrary.rs @@ -0,0 +1,89 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + common::models::TypeName, + integer::models::{IntegerGuard, IntegerInnerType, IntegerValidator}, +}; + +pub fn gen_impl_trait_arbitrary( + type_name: &TypeName, + inner_type: &IntegerInnerType, + guard: &IntegerGuard, +) -> TokenStream { + let Boundary { min, max } = guard_to_boundary(inner_type, guard); + + let construct_value = if guard.has_validation() { + // TODO: + // * Use a constant instead of hardcoded URL + // * Add predefined `title` and `body` parameters so there is already some text in the issue + // * Move it all into some helper function that can be reused + let error_text = format!("Arbitrary generated an invalid value for {type_name}.\nPlease report the issue at https://github.com/greyblake/nutype/issues/new"); + quote!( + Self::new(inner_value).expect(#error_text) + ) + } else { + quote!(Self::new(inner_value)) + }; + + quote!( + impl ::arbitrary::Arbitrary<'_> for #type_name { + fn arbitrary(u: &mut ::arbitrary::Unstructured<'_>) -> ::arbitrary::Result { + let inner_value: #inner_type = u.int_in_range((#min)..=(#max))?; + Ok(#construct_value) + } + } + ) +} + +#[derive(Debug)] +struct Boundary { + min: TokenStream, + max: TokenStream, +} + +fn guard_to_boundary( + inner_type: &IntegerInnerType, + guard: &IntegerGuard, +) -> Boundary { + let mut boundary = Boundary { + min: quote!(#inner_type::MIN), + max: quote!(#inner_type::MAX), + }; + + match guard { + IntegerGuard::WithoutValidation { sanitizers: _ } => { + // Nothing to validate, so every possible value for the inner type is valid. + } + IntegerGuard::WithValidation { + sanitizers: _, + validators, + } => { + // Apply validators to the boundaries. + // Since validators were already were validated, it's guaranteed that they're not + // contradicting each other. + for validator in validators { + match validator { + IntegerValidator::Greater(gt) => { + boundary.min = quote!(#gt + 1); + } + IntegerValidator::GreaterOrEqual(gte) => { + boundary.min = quote!(#gte); + } + IntegerValidator::Less(lt) => { + boundary.max = quote!(#lt - 1); + } + IntegerValidator::LessOrEqual(lte) => { + boundary.max = quote!(#lte); + } + IntegerValidator::Predicate(_) => { + // TODO: turn into an error + panic!("Cannot derive Arbitrary for a type with a predicate validator"); + } + } + } + } + } + + boundary +} diff --git a/nutype_macros/src/integer/gen/traits.rs b/nutype_macros/src/integer/gen/traits/mod.rs similarity index 93% rename from nutype_macros/src/integer/gen/traits.rs rename to nutype_macros/src/integer/gen/traits/mod.rs index 0a8d503..f720726 100644 --- a/nutype_macros/src/integer/gen/traits.rs +++ b/nutype_macros/src/integer/gen/traits/mod.rs @@ -1,3 +1,5 @@ +mod arbitrary; + use std::collections::HashSet; use proc_macro2::TokenStream; @@ -14,17 +16,18 @@ use crate::{ }, models::{ErrorTypeName, TypeName}, }, - integer::models::{IntegerDeriveTrait, IntegerInnerType}, + integer::models::{IntegerDeriveTrait, IntegerGuard, IntegerInnerType}, }; type IntegerGeneratableTrait = GeneratableTrait; -pub fn gen_traits( +pub fn gen_traits( type_name: &TypeName, inner_type: &IntegerInnerType, maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, + guard: &IntegerGuard, ) -> GeneratedTraits { let GeneratableTraits { transparent_traits, @@ -43,6 +46,7 @@ pub fn gen_traits( maybe_error_type_name, irregular_traits, maybe_default_value, + guard, ); GeneratedTraits { @@ -114,6 +118,9 @@ impl From for IntegerGeneratableTrait { IntegerDeriveTrait::SchemarsJsonSchema => { IntegerGeneratableTrait::Transparent(IntegerTransparentTrait::SchemarsJsonSchema) } + IntegerDeriveTrait::ArbitraryArbitrary => { + IntegerGeneratableTrait::Irregular(IntegerIrregularTrait::ArbitraryArbitrary) + } } } } @@ -147,6 +154,7 @@ enum IntegerIrregularTrait { Default, SerdeSerialize, SerdeDeserialize, + ArbitraryArbitrary, } impl ToTokens for IntegerTransparentTrait { @@ -166,12 +174,13 @@ impl ToTokens for IntegerTransparentTrait { } } -fn gen_implemented_traits( +fn gen_implemented_traits( type_name: &TypeName, inner_type: &IntegerInnerType, maybe_error_type_name: Option, impl_traits: Vec, maybe_default_value: Option, + guard: &IntegerGuard, ) -> TokenStream { impl_traits .iter() @@ -205,6 +214,9 @@ fn gen_implemented_traits( inner_type, maybe_error_type_name.as_ref(), ), + IntegerIrregularTrait::ArbitraryArbitrary => { + arbitrary::gen_impl_trait_arbitrary(type_name, inner_type, guard) + } }) .collect() } diff --git a/nutype_macros/src/integer/models.rs b/nutype_macros/src/integer/models.rs index cdeb74c..d7acbdf 100644 --- a/nutype_macros/src/integer/models.rs +++ b/nutype_macros/src/integer/models.rs @@ -61,7 +61,7 @@ pub enum IntegerDeriveTrait { SerdeSerialize, SerdeDeserialize, SchemarsJsonSchema, - // Arbitrary, + ArbitraryArbitrary, } impl TypeTrait for IntegerDeriveTrait { diff --git a/nutype_macros/src/integer/validate.rs b/nutype_macros/src/integer/validate.rs index df6d4d1..8381dbf 100644 --- a/nutype_macros/src/integer/validate.rs +++ b/nutype_macros/src/integer/validate.rs @@ -105,6 +105,7 @@ fn to_integer_derive_trait( DeriveTrait::SerdeSerialize => Ok(IntegerDeriveTrait::SerdeSerialize), DeriveTrait::SerdeDeserialize => Ok(IntegerDeriveTrait::SerdeDeserialize), DeriveTrait::SchemarsJsonSchema => Ok(IntegerDeriveTrait::SchemarsJsonSchema), + DeriveTrait::ArbitraryArbitrary => Ok(IntegerDeriveTrait::ArbitraryArbitrary), DeriveTrait::TryFrom => Ok(IntegerDeriveTrait::TryFrom), DeriveTrait::From => { if has_validation { diff --git a/nutype_macros/src/string/gen/mod.rs b/nutype_macros/src/string/gen/mod.rs index 996d664..ba946d5 100644 --- a/nutype_macros/src/string/gen/mod.rs +++ b/nutype_macros/src/string/gen/mod.rs @@ -16,7 +16,10 @@ use crate::{ use self::{error::gen_validation_error_type, traits::gen_traits}; -use super::{models::StringDeriveTrait, StringNewtype}; +use super::{ + models::{StringDeriveTrait, StringGuard}, + StringNewtype, +}; impl GenerateNewtype for StringNewtype { type Sanitizer = StringSanitizer; @@ -172,6 +175,7 @@ impl GenerateNewtype for StringNewtype { maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, + _guard: &StringGuard, ) -> GeneratedTraits { gen_traits( type_name, diff --git a/nutype_macros/src/string/validate.rs b/nutype_macros/src/string/validate.rs index 635006b..734ea77 100644 --- a/nutype_macros/src/string/validate.rs +++ b/nutype_macros/src/string/validate.rs @@ -167,6 +167,11 @@ fn to_string_derive_trait( } } DeriveTrait::TryFrom => Ok(StringDeriveTrait::TryFrom), + DeriveTrait::ArbitraryArbitrary => { + // TODO: Implement deriving Arbitrary + let msg = "Deriving Arbitrary trait for string types is not yet implemented"; + Err(syn::Error::new(span, msg)) + } } } From 288bb221cbdea2975ef6889619077005a84bd91d Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 2 Dec 2023 15:07:11 +0100 Subject: [PATCH 2/5] Introduce issue_reporter util --- Cargo.lock | 7 ++ dummy/src/main.rs | 43 ++----------- nutype_macros/Cargo.toml | 1 + .../src/integer/gen/traits/arbitrary.rs | 14 ++-- nutype_macros/src/integer/models.rs | 10 +++ nutype_macros/src/lib.rs | 1 + nutype_macros/src/utils/issue_reporter.rs | 64 +++++++++++++++++++ nutype_macros/src/utils/mod.rs | 1 + 8 files changed, 97 insertions(+), 44 deletions(-) create mode 100644 nutype_macros/src/utils/issue_reporter.rs create mode 100644 nutype_macros/src/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3ed75a8..5b16077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,6 +204,7 @@ dependencies = [ "quote", "regex", "syn 2.0.39", + "urlencoding", ] [[package]] @@ -466,6 +467,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "winapi" version = "0.3.9" diff --git a/dummy/src/main.rs b/dummy/src/main.rs index 7afc085..3c137c8 100644 --- a/dummy/src/main.rs +++ b/dummy/src/main.rs @@ -1,44 +1,9 @@ use nutype::nutype; #[nutype( - derive(Debug, PartialEq, Deref, AsRef), - sanitize(with = |mut guests| { guests.sort(); guests }), - validate(predicate = |guests| !guests.is_empty() ), + validate(greater = 0, less = 24,), + derive(Debug, PartialEq, Deref, AsRef) )] -pub struct GuestList(Vec); +pub struct Hour(i32); -fn main() { - // Empty list is not allowed - assert_eq!( - GuestList::new(vec![]), - Err(GuestListError::PredicateViolated) - ); - - // Create the list of our guests - let guest_list = GuestList::new(vec![ - "Seneca".to_string(), - "Marcus Aurelius".to_string(), - "Socrates".to_string(), - "Epictetus".to_string(), - ]) - .unwrap(); - - // The list is sorted (thanks to sanitize) - assert_eq!( - guest_list.as_ref(), - &[ - "Epictetus".to_string(), - "Marcus Aurelius".to_string(), - "Seneca".to_string(), - "Socrates".to_string(), - ] - ); - - // Since GuestList derives Deref, we can use methods from `Vec` - // due to deref coercion (if it's a good idea or not, it's left up to you to decide!). - assert_eq!(guest_list.len(), 4); - - for guest in guest_list.iter() { - println!("{guest}"); - } -} +fn main() {} diff --git a/nutype_macros/Cargo.toml b/nutype_macros/Cargo.toml index e916667..5b29631 100644 --- a/nutype_macros/Cargo.toml +++ b/nutype_macros/Cargo.toml @@ -24,6 +24,7 @@ syn = { version = "2.0", features = ["extra-traits", "full"] } regex = { version = "1", optional = true } cfg-if = "1.0.0" kinded = "0.3.0" +urlencoding = "2.0" [lib] proc-macro = true diff --git a/nutype_macros/src/integer/gen/traits/arbitrary.rs b/nutype_macros/src/integer/gen/traits/arbitrary.rs index 9a28a50..c53c820 100644 --- a/nutype_macros/src/integer/gen/traits/arbitrary.rs +++ b/nutype_macros/src/integer/gen/traits/arbitrary.rs @@ -4,6 +4,7 @@ use quote::{quote, ToTokens}; use crate::{ common::models::TypeName, integer::models::{IntegerGuard, IntegerInnerType, IntegerValidator}, + utils::issue_reporter::{build_github_link_with_issue, Issue}, }; pub fn gen_impl_trait_arbitrary( @@ -14,11 +15,14 @@ pub fn gen_impl_trait_arbitrary( let Boundary { min, max } = guard_to_boundary(inner_type, guard); let construct_value = if guard.has_validation() { - // TODO: - // * Use a constant instead of hardcoded URL - // * Add predefined `title` and `body` parameters so there is already some text in the issue - // * Move it all into some helper function that can be reused - let error_text = format!("Arbitrary generated an invalid value for {type_name}.\nPlease report the issue at https://github.com/greyblake/nutype/issues/new"); + // If by some reason we generate an invalid value, make it very easy for user to report + let report_issue_msg = build_github_link_with_issue( + &Issue::ArbitraryGeneratedInvalidValue { + inner_type: inner_type.to_string(), + }, + ); + let error_text = + format!("Arbitrary generated an invalid value for {type_name}.\n\n{report_issue_msg}"); quote!( Self::new(inner_value).expect(#error_text) ) diff --git a/nutype_macros/src/integer/models.rs b/nutype_macros/src/integer/models.rs index d7acbdf..c1d3fba 100644 --- a/nutype_macros/src/integer/models.rs +++ b/nutype_macros/src/integer/models.rs @@ -102,6 +102,16 @@ macro_rules! define_integer_inner_type { type_stream.to_tokens(token_stream); } } + + impl ::core::fmt::Display for IntegerInnerType { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> Result<(), ::core::fmt::Error> { + match self { + $( + Self::$variant => stringify!($tp).fmt(f), + )* + } + } + } } } diff --git a/nutype_macros/src/lib.rs b/nutype_macros/src/lib.rs index a539873..3515091 100644 --- a/nutype_macros/src/lib.rs +++ b/nutype_macros/src/lib.rs @@ -9,6 +9,7 @@ mod common; mod float; mod integer; mod string; +mod utils; use any::AnyNewtype; use common::{ diff --git a/nutype_macros/src/utils/issue_reporter.rs b/nutype_macros/src/utils/issue_reporter.rs new file mode 100644 index 0000000..f5a4b83 --- /dev/null +++ b/nutype_macros/src/utils/issue_reporter.rs @@ -0,0 +1,64 @@ +/// Tools that facilitates reporting issues on Github. +/// With some refactoring it can be extracted into its own crate. + +pub fn build_github_link_with_issue(issue: &Issue) -> String { + let builder = GithubIssueBuilder::new("greyblake/nutype"); + let url = builder.render_url(issue); + format!("\nClick the following link to report the issue:\n\n{url}\n\n") +} + +pub enum Issue { + ArbitraryGeneratedInvalidValue { inner_type: String }, +} + +struct GithubIssueBuilder { + // Repo ID on Github, for example "greyblake/nutype" + repo_path: String, +} + +impl GithubIssueBuilder { + fn new(repo_path: &str) -> Self { + Self { + repo_path: repo_path.to_owned(), + } + } + + fn render_url(&self, issue: &Issue) -> String { + let RenderedIssue { title, body } = render_issue(issue); + + let encoded_title = urlencoding::encode(&title); + let encoded_body = urlencoding::encode(&body); + let repo_path = &self.repo_path; + format!( + "https://github.com/{repo_path}/issues/new?title={encoded_title}&body={encoded_body}&labels=bug" + ) + } +} + +struct RenderedIssue { + title: String, + body: String, +} + +fn render_issue(issue: &Issue) -> RenderedIssue { + match issue { + Issue::ArbitraryGeneratedInvalidValue { + inner_type: type_name, + } => RenderedIssue { + title: format!( + "Arbitrary generates an invalid value for {}", + type_name + ), + body: " +Having my type defined as: + +```rs +// Put the definition of your type with #[nutype] macro here +``` + +I got a panic when I tried to generate a value with Arbitrary. +" + .to_owned(), + }, + } +} diff --git a/nutype_macros/src/utils/mod.rs b/nutype_macros/src/utils/mod.rs new file mode 100644 index 0000000..d0616b4 --- /dev/null +++ b/nutype_macros/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod issue_reporter; From 4aeea3e0eb5ef99d9858b51a05ecd30c7cac8f66 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 2 Dec 2023 17:49:36 +0100 Subject: [PATCH 3/5] Return error when it is not possible to derive Arbitrary --- dummy/Cargo.toml | 2 +- dummy/src/main.rs | 5 ++- nutype_macros/src/any/gen/mod.rs | 6 +-- nutype_macros/src/any/mod.rs | 4 +- nutype_macros/src/common/gen/mod.rs | 10 ++--- nutype_macros/src/common/models.rs | 4 +- nutype_macros/src/float/gen/mod.rs | 6 +-- nutype_macros/src/float/mod.rs | 2 +- nutype_macros/src/integer/gen/mod.rs | 2 +- .../src/integer/gen/traits/arbitrary.rs | 31 +++++++-------- nutype_macros/src/integer/gen/traits/mod.rs | 38 ++++++++++--------- nutype_macros/src/integer/mod.rs | 2 +- nutype_macros/src/string/gen/mod.rs | 6 +-- nutype_macros/src/string/mod.rs | 2 +- nutype_macros/src/utils/issue_reporter.rs | 5 +-- 15 files changed, 64 insertions(+), 61 deletions(-) diff --git a/dummy/Cargo.toml b/dummy/Cargo.toml index c61cbd5..c13e58d 100644 --- a/dummy/Cargo.toml +++ b/dummy/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nutype = { path = "../nutype", features = ["serde", "new_unchecked", "schemars08", "regex"] } +nutype = { path = "../nutype", features = ["serde", "new_unchecked", "schemars08", "regex", "arbitrary"] } serde = "*" serde_json = "*" schemars = "*" diff --git a/dummy/src/main.rs b/dummy/src/main.rs index 3c137c8..0776741 100644 --- a/dummy/src/main.rs +++ b/dummy/src/main.rs @@ -1,8 +1,9 @@ use nutype::nutype; #[nutype( - validate(greater = 0, less = 24,), - derive(Debug, PartialEq, Deref, AsRef) + validate(greater = 0, less = 24, predicate = |x| x == &4), + derive(Debug, PartialEq, Deref, AsRef, Default), + default = 4 )] pub struct Hour(i32); diff --git a/nutype_macros/src/any/gen/mod.rs b/nutype_macros/src/any/gen/mod.rs index 750e3d7..afa0c1a 100644 --- a/nutype_macros/src/any/gen/mod.rs +++ b/nutype_macros/src/any/gen/mod.rs @@ -106,13 +106,13 @@ impl GenerateNewtype for AnyNewtype { traits: HashSet, maybe_default_value: Option, _guard: &AnyGuard, - ) -> GeneratedTraits { - gen_traits( + ) -> Result { + Ok(gen_traits( type_name, inner_type, maybe_error_type_name, traits, maybe_default_value, - ) + )) } } diff --git a/nutype_macros/src/any/mod.rs b/nutype_macros/src/any/mod.rs index 5803f89..687029b 100644 --- a/nutype_macros/src/any/mod.rs +++ b/nutype_macros/src/any/mod.rs @@ -34,7 +34,9 @@ impl Newtype for AnyNewtype { validate_any_derive_traits(guard, derive_traits) } - fn generate(params: GenerateParams) -> TokenStream { + fn generate( + params: GenerateParams, + ) -> Result { AnyNewtype::gen_nutype(params) } } diff --git a/nutype_macros/src/common/gen/mod.rs b/nutype_macros/src/common/gen/mod.rs index 6e3d90f..7b6a1c8 100644 --- a/nutype_macros/src/common/gen/mod.rs +++ b/nutype_macros/src/common/gen/mod.rs @@ -181,7 +181,7 @@ pub trait GenerateNewtype { traits: HashSet, maybe_default_value: Option, guard: &Guard, - ) -> GeneratedTraits; + ) -> Result; fn gen_new_with_validation( type_name: &TypeName, @@ -284,7 +284,7 @@ pub trait GenerateNewtype { Self::TypedTrait, Guard, >, - ) -> TokenStream { + ) -> Result { let GenerateParams { doc_attrs, traits, @@ -330,9 +330,9 @@ pub trait GenerateNewtype { traits, maybe_default_value, &guard, - ); + )?; - quote!( + Ok(quote!( #[doc(hidden)] mod #module_name { use super::*; @@ -345,6 +345,6 @@ pub trait GenerateNewtype { #implement_traits } #reimports - ) + )) } } diff --git a/nutype_macros/src/common/models.rs b/nutype_macros/src/common/models.rs index c1ef359..37f70b8 100644 --- a/nutype_macros/src/common/models.rs +++ b/nutype_macros/src/common/models.rs @@ -322,7 +322,7 @@ pub trait Newtype { Self::TypedTrait, Guard, >, - ) -> TokenStream; + ) -> Result; fn expand( typed_meta: TypedMeta, @@ -350,7 +350,7 @@ pub trait Newtype { new_unchecked, maybe_default_value, inner_type, - }); + })?; Ok(generated_output) } } diff --git a/nutype_macros/src/float/gen/mod.rs b/nutype_macros/src/float/gen/mod.rs index 21e063d..09b18bf 100644 --- a/nutype_macros/src/float/gen/mod.rs +++ b/nutype_macros/src/float/gen/mod.rs @@ -133,13 +133,13 @@ where traits: HashSet, maybe_default_value: Option, _guard: &FloatGuard, - ) -> GeneratedTraits { - gen_traits( + ) -> Result { + Ok(gen_traits( type_name, inner_type, maybe_error_type_name, maybe_default_value, traits, - ) + )) } } diff --git a/nutype_macros/src/float/mod.rs b/nutype_macros/src/float/mod.rs index 212b8cf..b68d740 100644 --- a/nutype_macros/src/float/mod.rs +++ b/nutype_macros/src/float/mod.rs @@ -56,7 +56,7 @@ where Self::TypedTrait, Guard, >, - ) -> TokenStream { + ) -> Result { FloatNewtype::gen_nutype(params) } } diff --git a/nutype_macros/src/integer/gen/mod.rs b/nutype_macros/src/integer/gen/mod.rs index d50192a..3dc2588 100644 --- a/nutype_macros/src/integer/gen/mod.rs +++ b/nutype_macros/src/integer/gen/mod.rs @@ -125,7 +125,7 @@ where traits: HashSet, maybe_default_value: Option, guard: &IntegerGuard, - ) -> GeneratedTraits { + ) -> Result { gen_traits( type_name, inner_type, diff --git a/nutype_macros/src/integer/gen/traits/arbitrary.rs b/nutype_macros/src/integer/gen/traits/arbitrary.rs index c53c820..4b9504b 100644 --- a/nutype_macros/src/integer/gen/traits/arbitrary.rs +++ b/nutype_macros/src/integer/gen/traits/arbitrary.rs @@ -11,16 +11,15 @@ pub fn gen_impl_trait_arbitrary( type_name: &TypeName, inner_type: &IntegerInnerType, guard: &IntegerGuard, -) -> TokenStream { - let Boundary { min, max } = guard_to_boundary(inner_type, guard); +) -> Result { + let Boundary { min, max } = guard_to_boundary(inner_type, guard)?; let construct_value = if guard.has_validation() { - // If by some reason we generate an invalid value, make it very easy for user to report - let report_issue_msg = build_github_link_with_issue( - &Issue::ArbitraryGeneratedInvalidValue { + // If by some reason we generate an invalid value, make it very easy for the user to report + let report_issue_msg = + build_github_link_with_issue(&Issue::ArbitraryGeneratedInvalidValue { inner_type: inner_type.to_string(), - }, - ); + }); let error_text = format!("Arbitrary generated an invalid value for {type_name}.\n\n{report_issue_msg}"); quote!( @@ -30,14 +29,14 @@ pub fn gen_impl_trait_arbitrary( quote!(Self::new(inner_value)) }; - quote!( + Ok(quote!( impl ::arbitrary::Arbitrary<'_> for #type_name { fn arbitrary(u: &mut ::arbitrary::Unstructured<'_>) -> ::arbitrary::Result { let inner_value: #inner_type = u.int_in_range((#min)..=(#max))?; Ok(#construct_value) } } - ) + )) } #[derive(Debug)] @@ -49,7 +48,7 @@ struct Boundary { fn guard_to_boundary( inner_type: &IntegerInnerType, guard: &IntegerGuard, -) -> Boundary { +) -> Result { let mut boundary = Boundary { min: quote!(#inner_type::MIN), max: quote!(#inner_type::MAX), @@ -63,8 +62,8 @@ fn guard_to_boundary( sanitizers: _, validators, } => { - // Apply validators to the boundaries. - // Since validators were already were validated, it's guaranteed that they're not + // Apply the validators to the boundaries. + // Since the validators were already validated, it's guaranteed that they're not // contradicting each other. for validator in validators { match validator { @@ -81,13 +80,15 @@ fn guard_to_boundary( boundary.max = quote!(#lte); } IntegerValidator::Predicate(_) => { - // TODO: turn into an error - panic!("Cannot derive Arbitrary for a type with a predicate validator"); + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "Cannot derive trait `Arbitrary` for a type with `predicate` validator", + )); } } } } } - boundary + Ok(boundary) } diff --git a/nutype_macros/src/integer/gen/traits/mod.rs b/nutype_macros/src/integer/gen/traits/mod.rs index f720726..91a5e74 100644 --- a/nutype_macros/src/integer/gen/traits/mod.rs +++ b/nutype_macros/src/integer/gen/traits/mod.rs @@ -28,7 +28,7 @@ pub fn gen_traits( traits: HashSet, maybe_default_value: Option, guard: &IntegerGuard, -) -> GeneratedTraits { +) -> Result { let GeneratableTraits { transparent_traits, irregular_traits, @@ -47,12 +47,12 @@ pub fn gen_traits( irregular_traits, maybe_default_value, guard, - ); + )?; - GeneratedTraits { + Ok(GeneratedTraits { derive_transparent_traits, implement_traits, - } + }) } impl From for IntegerGeneratableTrait { @@ -181,39 +181,41 @@ fn gen_implemented_traits( impl_traits: Vec, maybe_default_value: Option, guard: &IntegerGuard, -) -> TokenStream { +) -> Result { impl_traits .iter() .map(|t| match t { - IntegerIrregularTrait::AsRef => gen_impl_trait_as_ref(type_name, inner_type), - IntegerIrregularTrait::Deref => gen_impl_trait_deref(type_name, inner_type), + IntegerIrregularTrait::AsRef => Ok(gen_impl_trait_as_ref(type_name, inner_type)), + IntegerIrregularTrait::Deref => Ok(gen_impl_trait_deref(type_name, inner_type)), IntegerIrregularTrait::FromStr => { - gen_impl_trait_from_str(type_name, inner_type, maybe_error_type_name.as_ref()) + Ok(gen_impl_trait_from_str(type_name, inner_type, maybe_error_type_name.as_ref())) } - IntegerIrregularTrait::From => gen_impl_trait_from(type_name, inner_type), - IntegerIrregularTrait::Into => gen_impl_trait_into(type_name, inner_type), + IntegerIrregularTrait::From => Ok(gen_impl_trait_from(type_name, inner_type)), + IntegerIrregularTrait::Into => Ok(gen_impl_trait_into(type_name, inner_type)), IntegerIrregularTrait::TryFrom => { - gen_impl_trait_try_from(type_name, inner_type, maybe_error_type_name.as_ref()) + Ok(gen_impl_trait_try_from(type_name, inner_type, maybe_error_type_name.as_ref())) } - IntegerIrregularTrait::Borrow => gen_impl_trait_borrow(type_name, inner_type), - IntegerIrregularTrait::Display => gen_impl_trait_dislpay(type_name), + IntegerIrregularTrait::Borrow => Ok(gen_impl_trait_borrow(type_name, inner_type)), + IntegerIrregularTrait::Display => Ok(gen_impl_trait_dislpay(type_name)), IntegerIrregularTrait::Default => { match maybe_default_value { Some(ref default_value) => { let has_validation = maybe_error_type_name.is_some(); - gen_impl_trait_default(type_name, default_value, has_validation) + Ok(gen_impl_trait_default(type_name, default_value, has_validation)) }, None => { - panic!("Default trait is derived for type {type_name}, but `default = ` parameter is missing in #[nutype] macro"); + let span = proc_macro2::Span::call_site(); + let msg = format!("Trait `Default` is derived for type {type_name}, but `default = ` parameter is missing in #[nutype] macro"); + Err(syn::Error::new(span, msg)) } } } - IntegerIrregularTrait::SerdeSerialize => gen_impl_trait_serde_serialize(type_name), - IntegerIrregularTrait::SerdeDeserialize => gen_impl_trait_serde_deserialize( + IntegerIrregularTrait::SerdeSerialize => Ok(gen_impl_trait_serde_serialize(type_name)), + IntegerIrregularTrait::SerdeDeserialize => Ok(gen_impl_trait_serde_deserialize( type_name, inner_type, maybe_error_type_name.as_ref(), - ), + )), IntegerIrregularTrait::ArbitraryArbitrary => { arbitrary::gen_impl_trait_arbitrary(type_name, inner_type, guard) } diff --git a/nutype_macros/src/integer/mod.rs b/nutype_macros/src/integer/mod.rs index 314f286..a25dd0c 100644 --- a/nutype_macros/src/integer/mod.rs +++ b/nutype_macros/src/integer/mod.rs @@ -58,7 +58,7 @@ where Self::TypedTrait, Guard, >, - ) -> TokenStream { + ) -> Result { IntegerNewtype::gen_nutype(params) } } diff --git a/nutype_macros/src/string/gen/mod.rs b/nutype_macros/src/string/gen/mod.rs index ba946d5..50a79d4 100644 --- a/nutype_macros/src/string/gen/mod.rs +++ b/nutype_macros/src/string/gen/mod.rs @@ -176,12 +176,12 @@ impl GenerateNewtype for StringNewtype { traits: HashSet, maybe_default_value: Option, _guard: &StringGuard, - ) -> GeneratedTraits { - gen_traits( + ) -> Result { + Ok(gen_traits( type_name, maybe_error_type_name, traits, maybe_default_value, - ) + )) } } diff --git a/nutype_macros/src/string/mod.rs b/nutype_macros/src/string/mod.rs index a042596..c70c907 100644 --- a/nutype_macros/src/string/mod.rs +++ b/nutype_macros/src/string/mod.rs @@ -41,7 +41,7 @@ impl Newtype for StringNewtype { fn generate( params: GenerateParams, - ) -> TokenStream { + ) -> Result { StringNewtype::gen_nutype(params) } } diff --git a/nutype_macros/src/utils/issue_reporter.rs b/nutype_macros/src/utils/issue_reporter.rs index f5a4b83..5581120 100644 --- a/nutype_macros/src/utils/issue_reporter.rs +++ b/nutype_macros/src/utils/issue_reporter.rs @@ -45,10 +45,7 @@ fn render_issue(issue: &Issue) -> RenderedIssue { Issue::ArbitraryGeneratedInvalidValue { inner_type: type_name, } => RenderedIssue { - title: format!( - "Arbitrary generates an invalid value for {}", - type_name - ), + title: format!("Arbitrary generates an invalid value for {}", type_name), body: " Having my type defined as: From c262fad48ab41bb01bad95b796f7bc951a7ed2a7 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 2 Dec 2023 17:51:55 +0100 Subject: [PATCH 4/5] Update error in test_suite/tests/ui/integer/derive/default.stderr --- test_suite/tests/ui/integer/derive/default.stderr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_suite/tests/ui/integer/derive/default.stderr b/test_suite/tests/ui/integer/derive/default.stderr index df2214a..8750039 100644 --- a/test_suite/tests/ui/integer/derive/default.stderr +++ b/test_suite/tests/ui/integer/derive/default.stderr @@ -1,4 +1,4 @@ -error: custom attribute panicked +error: Trait `Default` is derived for type Count, but `default = ` parameter is missing in #[nutype] macro --> tests/ui/integer/derive/default.rs:3:1 | 3 | / #[nutype( @@ -7,4 +7,4 @@ error: custom attribute panicked 6 | | )] | |__^ | - = help: message: Default trait is derived for type Count, but `default = ` parameter is missing in #[nutype] macro + = note: this error originates in the attribute macro `nutype` (in Nightly builds, run with -Z macro-backtrace for more info) From e1a96e8c4267e9e257cc6beec8cd246ff9836bc2 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 2 Dec 2023 17:54:25 +0100 Subject: [PATCH 5/5] Update CHANGELOG --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce8789..e21124b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ +### v0.4.1 - xxxx-xx-xx + +* Support integration with [`arbitrary`](https://crates.io/crates/arbitrary) crate (see `arbitrary` feature). + * Support `Arbitrary` for integer types + ### v0.4.0 - 2023-11-21 -* [FEATURE] Support of arbitrary inner types with custom sanitizers and validators. -* [FEATURE] Add numeric validator `greater` -* [FEATURE] Add numeric validator `less` +* Support of arbitrary inner types with custom sanitizers and validators. +* Add numeric validator `greater` +* Add numeric validator `less` * [BREAKING] Removal of asterisk derive * [BREAKING] Use commas to separate high level attributes * [BREAKING] Traits are derived with `#[nutype(derive(Debug))]`. The regular `#[derive(Debug)]` syntax is not supported anymore.