Skip to content

Commit b8a0f0d

Browse files
authored
Merge pull request #132 from greyblake/arbitrary-string
Allow deriving Arbitrary for String types
2 parents fd09132 + 799b306 commit b8a0f0d

File tree

13 files changed

+438
-47
lines changed

13 files changed

+438
-47
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Support integration with [`arbitrary`](https://crates.io/crates/arbitrary) crate (see `arbitrary` feature).
55
* Support `Arbitrary` for integer types
66
* Support `Arbitrary` for float types
7+
* Support `Arbitrary` for string inner types
78
* Support `Arbitrary` for any inner types
89
* Ability to specify boundaries (`greater`, `greater_or_equal`, `less`, `less_or_equal`, `len_char_min`, `len_char_max`) with expressions or named constants.
910
* Add `#[inline]` attribute to trivial functions

Cargo.lock

+23-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ members = [
1919
"examples/serde_complex",
2020
"examples/string_bounded_len",
2121
"examples/string_regex_email",
22+
"examples/string_arbitrary",
2223
]

dummy/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ regex = "*"
1414
once_cell = "*"
1515
lazy_static = "*"
1616
ron = "0.8.1"
17+
arbitrary = "1.3.2"

examples/string_arbitrary/Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "string_arbitrary"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
arbitrary = "1.3.2"
10+
arbtest = "0.3.1"
11+
nutype = { path = "../../nutype", features = ["arbitrary"] }

examples/string_arbitrary/src/main.rs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use arbitrary::Arbitrary;
2+
use arbtest::arbtest;
3+
use nutype::nutype;
4+
5+
fn main() {
6+
should_generate_arbitrary_string_without_validation_with_respect_to_sanitizers();
7+
should_generate_arbitrary_string_with_trim_and_min_len_validation();
8+
should_respect_not_empty_validation_with_trim();
9+
should_respect_not_empty_validation_without_trim();
10+
should_respect_len_char_max();
11+
should_respec_both_len_boundaries();
12+
}
13+
14+
fn should_generate_arbitrary_string_without_validation_with_respect_to_sanitizers() {
15+
#[nutype(sanitize(lowercase), derive(Arbitrary, Debug))]
16+
struct LowercaseString(String);
17+
18+
arbtest(|u| {
19+
let s = LowercaseString::arbitrary(u)?.into_inner();
20+
assert_eq!(s.to_lowercase(), s);
21+
Ok(())
22+
});
23+
}
24+
25+
fn should_generate_arbitrary_string_with_trim_and_min_len_validation() {
26+
#[nutype(sanitize(trim), validate(len_char_min = 3), derive(Arbitrary, Debug))]
27+
struct Name(String);
28+
29+
arbtest(|u| {
30+
let s = Name::arbitrary(u)?.into_inner();
31+
assert_eq!(s.trim(), s);
32+
assert!(s.chars().count() >= 3);
33+
Ok(())
34+
});
35+
}
36+
37+
fn should_respect_not_empty_validation_with_trim() {
38+
#[nutype(sanitize(trim), validate(not_empty), derive(Arbitrary, Debug))]
39+
struct Title(String);
40+
41+
arbtest(|u| {
42+
let s = Title::arbitrary(u)?.into_inner();
43+
assert_eq!(s.trim(), s);
44+
assert!(!s.is_empty());
45+
Ok(())
46+
});
47+
}
48+
49+
fn should_respect_not_empty_validation_without_trim() {
50+
#[nutype(validate(not_empty), derive(Arbitrary, Debug))]
51+
struct Description(String);
52+
53+
arbtest(|u| {
54+
let s = Description::arbitrary(u)?.into_inner();
55+
assert!(!s.is_empty());
56+
Ok(())
57+
});
58+
}
59+
60+
fn should_respect_len_char_max() {
61+
#[nutype(validate(len_char_max = 7), derive(Arbitrary, Debug))]
62+
struct Text(String);
63+
64+
arbtest(|u| {
65+
let s = Text::arbitrary(u)?.into_inner();
66+
assert!(s.chars().count() <= 7);
67+
Ok(())
68+
});
69+
}
70+
71+
fn should_respec_both_len_boundaries() {
72+
#[nutype(validate(len_char_min = 3, len_char_max = 5), derive(Arbitrary, Debug))]
73+
struct Text(String);
74+
75+
arbtest(|u| {
76+
let s = Text::arbitrary(u)?.into_inner();
77+
assert!(s.chars().count() >= 3);
78+
assert!(s.chars().count() <= 5);
79+
Ok(())
80+
});
81+
}

nutype_macros/src/any/gen/traits/mod.rs

+10-12
Original file line numberDiff line numberDiff line change
@@ -162,19 +162,17 @@ fn gen_implemented_traits(
162162
AnyIrregularTrait::TryFrom => Ok(
163163
gen_impl_trait_try_from(type_name, inner_type, maybe_error_type_name.as_ref())
164164
),
165-
AnyIrregularTrait::Default => Ok(
166-
match maybe_default_value {
167-
Some(ref default_value) => {
168-
let has_validation = maybe_error_type_name.is_some();
169-
gen_impl_trait_default(type_name, default_value, has_validation)
170-
}
171-
None => {
172-
panic!(
173-
"Default trait is derived for type {type_name}, but `default = ` is missing"
174-
);
175-
}
165+
AnyIrregularTrait::Default => match maybe_default_value {
166+
Some(ref default_value) => {
167+
let has_validation = maybe_error_type_name.is_some();
168+
Ok(gen_impl_trait_default(type_name, default_value, has_validation))
176169
}
177-
),
170+
None => {
171+
let span = proc_macro2::Span::call_site();
172+
let msg = format!("Trait `Default` is derived for type {type_name}, but `default = ` parameter is missing in #[nutype] macro");
173+
Err(syn::Error::new(span, msg))
174+
}
175+
},
178176
AnyIrregularTrait::SerdeSerialize => Ok(
179177
gen_impl_trait_serde_serialize(type_name)
180178
),

nutype_macros/src/common/models.rs

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use kinded::Kinded;
2+
use std::ops::Add;
23
use std::{collections::HashSet, fmt::Debug};
34

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

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

242+
impl<T> Add<T> for ValueOrExpr<T>
243+
where
244+
T: Add<T, Output = T> + ToTokens,
245+
{
246+
type Output = ValueOrExpr<T>;
247+
248+
fn add(self, rhs: T) -> Self::Output {
249+
match self {
250+
Self::Value(lhs) => Self::Value(lhs + rhs),
251+
Self::Expr(lhs) => {
252+
let token_stream = quote!(#lhs + #rhs);
253+
let expr = syn::parse2(token_stream)
254+
.expect("Failed to parse token stream in ValueOrExpr::add");
255+
Self::Expr(expr)
256+
}
257+
}
258+
}
259+
}
260+
241261
impl<Sanitizer, Validator> Guard<Sanitizer, Validator> {
242262
pub fn has_validation(&self) -> bool {
243263
match self {

nutype_macros/src/string/gen/mod.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,15 @@ impl GenerateNewtype for StringNewtype {
179179
maybe_error_type_name: Option<ErrorTypeName>,
180180
traits: HashSet<Self::TypedTrait>,
181181
maybe_default_value: Option<syn::Expr>,
182-
_guard: &StringGuard,
182+
guard: &StringGuard,
183183
) -> Result<GeneratedTraits, syn::Error> {
184-
Ok(gen_traits(
184+
gen_traits(
185185
type_name,
186186
maybe_error_type_name,
187187
traits,
188188
maybe_default_value,
189-
))
189+
guard,
190+
)
190191
}
191192

192193
fn gen_tests(

0 commit comments

Comments
 (0)