Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Newtype #79

Merged
merged 7 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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