Skip to content

Commit

Permalink
Merge pull request #132 from greyblake/arbitrary-string
Browse files Browse the repository at this point in the history
Allow deriving Arbitrary for String types
  • Loading branch information
greyblake committed Apr 6, 2024
2 parents fd09132 + 799b306 commit b8a0f0d
Show file tree
Hide file tree
Showing 13 changed files with 438 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Support integration with [`arbitrary`](https://crates.io/crates/arbitrary) crate (see `arbitrary` feature).
* Support `Arbitrary` for integer types
* Support `Arbitrary` for float types
* Support `Arbitrary` for string inner types
* Support `Arbitrary` for any inner types
* Ability to specify boundaries (`greater`, `greater_or_equal`, `less`, `less_or_equal`, `len_char_min`, `len_char_max`) with expressions or named constants.
* Add `#[inline]` attribute to trivial functions
Expand Down
27 changes: 23 additions & 4 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ members = [
"examples/serde_complex",
"examples/string_bounded_len",
"examples/string_regex_email",
"examples/string_arbitrary",
]
1 change: 1 addition & 0 deletions dummy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ regex = "*"
once_cell = "*"
lazy_static = "*"
ron = "0.8.1"
arbitrary = "1.3.2"
11 changes: 11 additions & 0 deletions examples/string_arbitrary/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "string_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.3.1"
nutype = { path = "../../nutype", features = ["arbitrary"] }
81 changes: 81 additions & 0 deletions examples/string_arbitrary/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use arbitrary::Arbitrary;
use arbtest::arbtest;
use nutype::nutype;

fn main() {
should_generate_arbitrary_string_without_validation_with_respect_to_sanitizers();
should_generate_arbitrary_string_with_trim_and_min_len_validation();
should_respect_not_empty_validation_with_trim();
should_respect_not_empty_validation_without_trim();
should_respect_len_char_max();
should_respec_both_len_boundaries();
}

fn should_generate_arbitrary_string_without_validation_with_respect_to_sanitizers() {
#[nutype(sanitize(lowercase), derive(Arbitrary, Debug))]
struct LowercaseString(String);

arbtest(|u| {
let s = LowercaseString::arbitrary(u)?.into_inner();
assert_eq!(s.to_lowercase(), s);
Ok(())
});
}

fn should_generate_arbitrary_string_with_trim_and_min_len_validation() {
#[nutype(sanitize(trim), validate(len_char_min = 3), derive(Arbitrary, Debug))]
struct Name(String);

arbtest(|u| {
let s = Name::arbitrary(u)?.into_inner();
assert_eq!(s.trim(), s);
assert!(s.chars().count() >= 3);
Ok(())
});
}

fn should_respect_not_empty_validation_with_trim() {
#[nutype(sanitize(trim), validate(not_empty), derive(Arbitrary, Debug))]
struct Title(String);

arbtest(|u| {
let s = Title::arbitrary(u)?.into_inner();
assert_eq!(s.trim(), s);
assert!(!s.is_empty());
Ok(())
});
}

fn should_respect_not_empty_validation_without_trim() {
#[nutype(validate(not_empty), derive(Arbitrary, Debug))]
struct Description(String);

arbtest(|u| {
let s = Description::arbitrary(u)?.into_inner();
assert!(!s.is_empty());
Ok(())
});
}

fn should_respect_len_char_max() {
#[nutype(validate(len_char_max = 7), derive(Arbitrary, Debug))]
struct Text(String);

arbtest(|u| {
let s = Text::arbitrary(u)?.into_inner();
assert!(s.chars().count() <= 7);
Ok(())
});
}

fn should_respec_both_len_boundaries() {
#[nutype(validate(len_char_min = 3, len_char_max = 5), derive(Arbitrary, Debug))]
struct Text(String);

arbtest(|u| {
let s = Text::arbitrary(u)?.into_inner();
assert!(s.chars().count() >= 3);
assert!(s.chars().count() <= 5);
Ok(())
});
}
22 changes: 10 additions & 12 deletions nutype_macros/src/any/gen/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,19 +162,17 @@ fn gen_implemented_traits(
AnyIrregularTrait::TryFrom => Ok(
gen_impl_trait_try_from(type_name, inner_type, maybe_error_type_name.as_ref())
),
AnyIrregularTrait::Default => Ok(
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)
}
None => {
panic!(
"Default trait is derived for type {type_name}, but `default = ` is missing"
);
}
AnyIrregularTrait::Default => match maybe_default_value {
Some(ref default_value) => {
let has_validation = maybe_error_type_name.is_some();
Ok(gen_impl_trait_default(type_name, default_value, has_validation))
}
),
None => {
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))
}
},
AnyIrregularTrait::SerdeSerialize => Ok(
gen_impl_trait_serde_serialize(type_name)
),
Expand Down
22 changes: 21 additions & 1 deletion nutype_macros/src/common/models.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use kinded::Kinded;
use std::ops::Add;
use std::{collections::HashSet, fmt::Debug};

use proc_macro2::{Span, TokenStream};
Expand Down Expand Up @@ -219,7 +220,7 @@ pub struct Attributes<G, DT> {

/// Represents a value known at compile time or an expression.
/// Knowing value at compile time allows to run some extra validations to prevent potential errors.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum ValueOrExpr<T> {
Value(T),
Expr(syn::Expr),
Expand All @@ -238,6 +239,25 @@ impl<T: ToTokens> ToTokens for ValueOrExpr<T> {
}
}

impl<T> Add<T> for ValueOrExpr<T>
where
T: Add<T, Output = T> + ToTokens,
{
type Output = ValueOrExpr<T>;

fn add(self, rhs: T) -> Self::Output {
match self {
Self::Value(lhs) => Self::Value(lhs + rhs),
Self::Expr(lhs) => {
let token_stream = quote!(#lhs + #rhs);
let expr = syn::parse2(token_stream)
.expect("Failed to parse token stream in ValueOrExpr::add");
Self::Expr(expr)
}
}
}
}

impl<Sanitizer, Validator> Guard<Sanitizer, Validator> {
pub fn has_validation(&self) -> bool {
match self {
Expand Down
7 changes: 4 additions & 3 deletions nutype_macros/src/string/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,15 @@ impl GenerateNewtype for StringNewtype {
maybe_error_type_name: Option<ErrorTypeName>,
traits: HashSet<Self::TypedTrait>,
maybe_default_value: Option<syn::Expr>,
_guard: &StringGuard,
guard: &StringGuard,
) -> Result<GeneratedTraits, syn::Error> {
Ok(gen_traits(
gen_traits(
type_name,
maybe_error_type_name,
traits,
maybe_default_value,
))
guard,
)
}

fn gen_tests(
Expand Down

0 comments on commit b8a0f0d

Please sign in to comment.