Skip to content

Commit

Permalink
Merge pull request #79 from jprochazk/newtype
Browse files Browse the repository at this point in the history
Newtype
  • Loading branch information
jprochazk authored Nov 26, 2023
2 parents 64d4b64 + 81344ca commit 8b1955e
Show file tree
Hide file tree
Showing 21 changed files with 289 additions and 32 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ jobs:
rust: 1.69
# Fails on pinned version because the output changed,
# so we're excluding it, but it's still tested on stable and nightly.
EXCLUDE_UI_TESTS: "pattern_mismatched_types"
EXCLUDE_UI_TESTS: "pattern_mismatched_types,newtype"
- build: stable
os: ubuntu-20.04
rust: stable
EXCLUDE_UI_TESTS: ""
EXCLUDE_UI_TESTS: "newtype"
- build: nightly
os: ubuntu-20.04
rust: nightly
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A Rust validation library
- [Basic usage example](#basic-usage-example)
- [Validation rules](#available-validation-rules)
- [Inner type validation](#inner-type-validation)
- [Newtypes](#newtypes)
- [Handling Option](#handling-option)
- [Custom validation](#custom-validation)
- [Context/Self access](#contextself-access)
Expand Down Expand Up @@ -148,6 +149,46 @@ The above type would fail validation if:
- any of the inner `String` elements is empty
- any of the inner `String` elements contains non-ASCII characters

### Newtypes

The best way to re-use validation rules on a field is to use the [newtype idiom](https://doc.rust-lang.org/rust-by-example/generics/new_types.html)
with `#[garde(transparent)]`:

```rust
#[derive(garde::Validate)]
#[garde(transparent)]
struct Username(#[garde(length(min = 3, max = 20))] String);

#[derive(garde::Validate)]
struct User {
// later used with `dive`:
#[garde(dive)]
username: Username,
}
```

The `username` field in the above example will inherit all the validation rules from the `String` field on `Username`. The result is that the error path will be flattened by one level, resulting in cleaner error messages:

```rust,ignore
User {
username: Username("")
}.validate(&())
"username: length is lower than 3"
```

Without the `#[garde(transparent)]` attribute, it would instead be:

```rust,ignore
User {
username: Username("")
}.validate(&())
"username[0]: length is lower than 3"
```

Structs with the `#[garde(transparent)]` attribute may have more than one field, but there must be only one unskipped field. That means every field other than the one you wish to validate must be `#[garde(skip)]`.

### Handling Option

Every rule works on `Option<T>` fields. The field will only be validated if it is `Some`. If you additionally want to validate that the `Option<T>` field is `Some`, use the `required` rule:
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/ascii.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[derive(Debug, garde::Validate)]
struct Test<'a> {
#[garde(ascii)]
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/credit_card.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[derive(Debug, garde::Validate)]
struct Test<'a> {
#[garde(credit_card)]
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/custom.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

struct Context {
needle: String,
}
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/email.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[derive(Debug, garde::Validate)]
struct Test<'a> {
#[garde(email)]
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/ip.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[derive(Debug, garde::Validate)]
struct TestIpAny<'a> {
#[garde(ip)]
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod inner;
mod ip;
mod length;
mod multi_rule;
mod newtype;
mod option;
mod pattern;
mod phone_number;
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/multi_rule.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[derive(Debug, garde::Validate)]
struct Test<'a> {
#[garde(prefix("test"), ascii, length(min = 10, max = 100))]
Expand Down
45 changes: 45 additions & 0 deletions garde/tests/rules/newtype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#![allow(non_camel_case_types)]

use super::util;

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct NonEmptyStr_Struct<'a> {
#[garde(length(min = 1))]
v: &'a str,
}

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct NonEmptyStr_Tuple<'a>(#[garde(length(min = 1))] &'a str);

#[derive(Debug, garde::Validate)]

struct Test<'a> {
#[garde(dive)]
a: NonEmptyStr_Struct<'a>,
#[garde(dive)]
b: NonEmptyStr_Tuple<'a>,
}

#[test]
fn newtype_valid() {
util::check_ok(
&[Test {
a: NonEmptyStr_Struct { v: "test" },
b: NonEmptyStr_Tuple("test"),
}],
&(),
);
}

#[test]
fn newtype_invalid() {
util::check_fail!(
&[Test {
a: NonEmptyStr_Struct { v: "" },
b: NonEmptyStr_Tuple(""),
}],
&()
);
}
1 change: 1 addition & 0 deletions garde/tests/rules/phone_number.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[derive(Debug, garde::Validate)]
struct Test<'a> {
#[garde(phone_number)]
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/range.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[derive(Debug, garde::Validate)]
struct Test<'a> {
#[garde(range(min = 10, max = 100))]
Expand Down
1 change: 1 addition & 0 deletions garde/tests/rules/skip.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::util;

#[allow(dead_code)]
#[derive(Debug, garde::Validate)]
struct Struct {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: garde/tests/./rules/newtype.rs
expression: snapshot
---
Test {
a: NonEmptyStr_Struct {
v: "",
},
b: NonEmptyStr_Tuple(
"",
),
}
a: length is lower than 1
b: length is lower than 1


24 changes: 24 additions & 0 deletions garde/tests/ui/compile-fail/non_unary_newtype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#![allow(dead_code)]

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct EmptyTuple();

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct EmptyStruct {}

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct NonUnaryTuple<'a>(#[garde(ascii)] &'a str, #[garde(ascii)] &'a str);

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct NonUnaryStruct<'a> {
#[garde(ascii)]
a: &'a str,
#[garde(ascii)]
b: &'a str,
}

fn main() {}
23 changes: 23 additions & 0 deletions garde/tests/ui/compile-fail/non_unary_newtype.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
error: transparent structs must have exactly one field
--> tests/ui/compile-fail/non_unary_newtype.rs
|
| #[garde(transparent)]
| ^^^^^^^^^^^^^^^^^^^^^

error: transparent structs must have exactly one field
--> tests/ui/compile-fail/non_unary_newtype.rs
|
| #[garde(transparent)]
| ^^^^^^^^^^^^^^^^^^^^^

error: transparent structs must have exactly one field
--> tests/ui/compile-fail/non_unary_newtype.rs
|
| #[garde(transparent)]
| ^^^^^^^^^^^^^^^^^^^^^

error: transparent structs must have exactly one field
--> tests/ui/compile-fail/non_unary_newtype.rs
|
| #[garde(transparent)]
| ^^^^^^^^^^^^^^^^^^^^^
27 changes: 27 additions & 0 deletions garde/tests/ui/compile-pass/newtype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#![allow(dead_code)]

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct UnaryTuple<'a>(#[garde(ascii)] &'a str);

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct UnaryStruct<'a> {
#[garde(ascii)]
a: &'a str,
}

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct TupleWithSkippedField<'a>(#[garde(ascii)] &'a str, #[garde(skip)] &'a str);

#[derive(Debug, garde::Validate)]
#[garde(transparent)]
struct StructWithSkippedField<'a> {
#[garde(ascii)]
a: &'a str,
#[garde(skip)]
b: &'a str,
}

fn main() {}
39 changes: 39 additions & 0 deletions garde_derive/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub fn check(input: model::Input) -> syn::Result<model::Validate> {
}
};

let transparent = get_transparent_attr(&attrs);

let options = get_options(&attrs);

let kind = match kind {
Expand Down Expand Up @@ -61,6 +63,15 @@ pub fn check(input: model::Input) -> syn::Result<model::Validate> {
}
};

if let Some(span) = transparent {
if !is_unary_struct(&kind) {
error.maybe_fold(syn::Error::new(
span,
"transparent structs must have exactly one field",
));
}
}

if let Some(error) = error {
return Err(error);
}
Expand All @@ -69,6 +80,7 @@ pub fn check(input: model::Input) -> syn::Result<model::Validate> {
ident,
generics,
context,
is_transparent: transparent.is_some(),
kind,
options,
})
Expand Down Expand Up @@ -118,6 +130,32 @@ fn get_context(attrs: &[(Span, model::Attr)]) -> syn::Result<(syn::Type, syn::Id
}
}

fn get_transparent_attr(attrs: &[(Span, model::Attr)]) -> Option<Span> {
for (span, attr) in attrs {
if let model::Attr::Transparent = attr {
return Some(*span);
}
}

None
}

fn is_unary_struct(k: &model::ValidateKind) -> bool {
match k {
model::ValidateKind::Struct(model::ValidateVariant::Tuple(fields)) => {
fields.iter().filter(|field| field.skip.is_none()).count() == 1
}
model::ValidateKind::Struct(model::ValidateVariant::Struct(fields)) => {
fields
.iter()
.filter(|(_, field)| field.skip.is_none())
.count()
== 1
}
_ => false,
}
}

fn get_options(attrs: &[(Span, model::Attr)]) -> model::Options {
let mut options = model::Options {
allow_unvalidated: false,
Expand All @@ -127,6 +165,7 @@ fn get_options(attrs: &[(Span, model::Attr)]) -> model::Options {
match attr {
model::Attr::Context(..) => {}
model::Attr::AllowUnvalidated => options.allow_unvalidated = true,
_ => {}
}
}

Expand Down
Loading

0 comments on commit 8b1955e

Please sign in to comment.