Skip to content

Commit ee74cd9

Browse files
authored
Merge pull request #219 from greyblake/derive-unsafe
Derive unsafe
2 parents 00e2f25 + ad59da2 commit ee74cd9

File tree

29 files changed

+284
-18
lines changed

29 files changed

+284
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### v0.x.x - 2025-xx-xx
2-
- Update Rust edition: 2021 -> 2024
2+
- Introduce `derive_unsafe(..)` attribute to derive any arbitrary trait (requires `derive_unsafe` feature to be enabled).
3+
- Update Rust edition: 2021 -> 2024.
34
- Improve error messages for `len_char_max` and `len_char_min` validators.
45

56
### v0.6.1 - 2025-02-09

Cargo.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010

1111
# All examples except "no_std_example" are tested in the test suite
1212
"examples/any_arbitrary",
13+
"examples/derive_unsafe_example",
1314
"examples/float_arbitrary",
1415
"examples/float_sortable",
1516
"examples/integer_arbitrary",

GLOSSARY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ Example: trim trailing spaces and lowercase an email address.
55
* _Validator_ - a **fallible** operation, that checks if a piece of data matches some given rules.
66
Example: ensure that an email address contains `@` character. Validators come with associated error variants.
77

8-
* _Guard_ - a generic term that covers both sanitizers and validators.
8+
* _Guard_ - an umbrella term that covers both sanitizers and validators.
99

1010
* _Inner Type_ - typically a simple type that is wrapped by a newtype.
1111
Example: consider `Email` type defined as `Email(String)`. We have say that `Email` has inner type `String`.
1212

1313
* _Transparent trait_ - a trait that can be simply derived (e.g. `Debug`, `Clone`).
14-
* _Irregular trait_ a trait that requires a custom implementation to be generated (e.g. `TryFrom`).
14+
* _Irregular trait_ - a trait that requires a custom implementation to be generated (e.g. `TryFrom`).
15+
* _Unsafe trait_ - a trait that potentially may violate the constraints. Traits derive with `derive_unsafe(...)` are not validated by nutype.

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Nutype is a proc macro that allows adding extra constraints like _sanitization_
2121
* [Quick start](#quick-start)
2222
* [Inner types](#inner-types) ([String](#string) | [Integer](#integer) | [Float](#float) | [Other](#other-inner-types-and-generics))
2323
* [Custom](#custom-sanitizers) ([sanitizers](#custom-sanitizers) | [validators](#custom-validators) | [errors](#custom-validation-with-a-custom-error-type))
24+
* [Deriving traits](#deriving-traits)
2425
* [Constants](#constants)
2526
* [Recipes](#recipes)
2627
* [Breaking constraints with new_unchecked](#breaking-constraints-with-new_unchecked)
@@ -346,6 +347,40 @@ struct Name(String);
346347

347348
It's important to ensure that the type specified in the `error` attribute matches the error type returned by the validation function.
348349

350+
351+
## Deriving Traits
352+
353+
There are two ways to derive traits for a `nutype`.
354+
355+
### `derive`
356+
357+
The recommended approach is to use the `derive(..)` attribute within the `#[nutype(..)]` macro:
358+
359+
```rust
360+
#[nutype(derive(Debug))]
361+
pub struct Username(String);
362+
```
363+
364+
When using `derive`, `nutype` ensures that the derived traits do not compromise the type's invariants (i.e., validation constraints).
365+
366+
However, this approach has a limitation: only a predefined set of traits is supported. Deriving arbitrary third-party traits is not allowed via `derive`.
367+
368+
### `derive_unsafe`
369+
370+
To overcome this limitation, you can use the `derive_unsafe(..)` attribute (requires the corresponding feature flag to be enabled):
371+
372+
```rust
373+
use derive_more::DerefMut;
374+
375+
#[nutype(derive_unsafe(DerefMut))]
376+
pub struct Username(String);
377+
```
378+
379+
This enables deriving arbitrary traits, including those from third-party crates.
380+
However, **use this with caution**: `nutype` cannot verify that these traits preserve the invariants of the type.
381+
It is the developer’s responsibility to ensure that the derived traits do not introduce ways to bypass validation (e.g., by allowing mutable access to the inner value).
382+
383+
349384
## Constants
350385

351386
You can mark a type with the `const_fn` flag. In that case, its `new` and `try_new` functions will be declared as `const`:
@@ -446,6 +481,7 @@ assert_eq!(name.into_inner(), " boo ");
446481
## Feature flags
447482

448483
* `arbitrary` - enables derive of [`arbitrary::Arbitrary`](https://docs.rs/arbitrary/latest/arbitrary/trait.Arbitrary.html).
484+
* `derive_unsafe` - enables `derive_unsafe` attribute to derive any arbitrary trait.
449485
* `new_unchecked` - enables generation of unsafe `::new_unchecked()` function.
450486
* `regex` - allows to use `regex = ` validation on string-based types. Note: your crate also has to explicitly have `regex` within its dependencies.
451487
* `serde` - integrations with [`serde`](https://crates.io/crates/serde) crate. Allows to derive `Serialize` and `Deserialize` traits.
@@ -490,6 +526,7 @@ Thank you.
490526
* [refinement](https://docs.rs/refinement/latest/refinement/) - Convenient creation of type-safe refinement types (based on generics).
491527
* [semval](https://github.com/slowtec/semval) - Semantic validation for Rust.
492528
* [validator](https://github.com/Keats/validator) - Simple validation for Rust structs (powered by macros).
529+
* [derive_more](https://github.com/JelteF/derive_more) - If you just need to derive traits on a newtype without extra logic.
493530

494531
## License
495532

dummy/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9-
nutype = { path = "../nutype", features = ["serde", "new_unchecked", "schemars08", "regex", "arbitrary"] }
9+
nutype = { path = "../nutype", features = ["serde", "new_unchecked", "schemars08", "regex", "arbitrary", "derive_unsafe"] }
1010
serde = "*"
1111
serde_json = "*"
1212
schemars = "*"

dummy/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use nutype::nutype;
22

3-
#[nutype(derive(Debug), validate(len_char_max = 5, len_char_min = 3))]
3+
#[nutype(
4+
derive_unsafe(::std::fmt::Debug),
5+
validate(len_char_max = 5, len_char_min = 3)
6+
)]
47
struct Name(String);
58

69
fn main() {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "derive_unsafe_example"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
derive_more = { version = "2.0.1", features = ["deref", "deref_mut"] }
10+
nutype = { path = "../../nutype", features = ["derive_unsafe"] }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// WATCH OUT: derive_unsafe() allows to derive any trait even those that may create loopholes
2+
// in the validation and sanitization logic!
3+
use derive_more::{Deref, DerefMut};
4+
use nutype::nutype;
5+
6+
#[nutype(
7+
derive(Debug, AsRef),
8+
derive_unsafe(Deref, DerefMut),
9+
validate(greater_or_equal = 0.0, less_or_equal = 2.0)
10+
)]
11+
struct LlmTemperature(f64);
12+
13+
fn main() {
14+
let mut temperature = LlmTemperature::try_new(1.5).unwrap();
15+
16+
// This is not what nutype is designed for!
17+
*temperature = 2.5;
18+
19+
// OH no, we've just violated the validation rule!
20+
assert_eq!(temperature.as_ref(), &2.5);
21+
}

nutype/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ regex = ["nutype_macros/regex"]
3131
schemars08 = ["nutype_macros/schemars08"]
3232
new_unchecked = ["nutype_macros/new_unchecked"]
3333
arbitrary = ["nutype_macros/arbitrary"]
34+
derive_unsafe = ["nutype_macros/derive_unsafe"]

0 commit comments

Comments
 (0)