Skip to content
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
### v0.7.1 - Unreleased
### v0.8.0 - Unreleased
- **[FEATURE]** Attribute passthrough (see [#229](https://github.com/greyblake/nutype/issues/229)): attributes written on the struct and on the inner field are now forwarded verbatim onto the generated type, so they can be read by third-party derives and the compiler. Examples: `#[repr(transparent)]`, `#[sqlx(transparent)]` (with `derive_unchecked(sqlx::Type)`), `#[garde(length(min = 1))]` (with `derive_unchecked(garde::Validate)`). Resolves [#228](https://github.com/greyblake/nutype/issues/228) and the attribute half of [#191](https://github.com/greyblake/nutype/issues/191).
- **[FEATURE]** Serde customization (see [#201](https://github.com/greyblake/nutype/issues/201)): nutype's generated `Serialize`/`Deserialize` impls now honor field-level `#[serde(with = "...")]`, `#[serde(serialize_with = "...")]`, `#[serde(deserialize_with = "...")]` and struct-level `#[serde(transparent)]`. Sanitization and validation still always run on deserialization.
- **[FIX]** Field attributes on newtypes with custom ("any") inner types no longer leak into type positions of the generated code (previously they could produce confusing compile errors).
- **[BEHAVIOR]** Previously, non-doc attributes on the struct were rejected with an error and attributes on the inner field were silently dropped. Both are now forwarded (serde attributes are consumed and integrated instead; `#[cfg]` on the inner field is rejected explicitly).
- **[FEATURE]** Support `rust_decimal::Decimal` as an inner type behind the `rust_decimal` feature flag, with the standard numeric validators and sanitizers (see [#242](https://github.com/greyblake/nutype/issues/242)).
- **[FEATURE]** Friendlier error when a `#[nutype(...)]` attribute is mistyped: suggests the closest match (e.g. `validte` -> `validate`) and lists the available nutype attributes (see [#240](https://github.com/greyblake/nutype/issues/240)).
- **[FIX]** Fix misleading error for value-type mismatches in validators (see [#241](https://github.com/greyblake/nutype/issues/241)).
Expand Down
20 changes: 19 additions & 1 deletion Cargo.lock

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

41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,47 @@ pub struct Tag(String);
Note that a trait cannot appear in both unconditional `derive` and `cfg_attr` `derive` at the same time.


## Attribute passthrough

Attributes on the struct and on the inner field are forwarded verbatim onto
the generated type, so the compiler and third-party derives (via
`derive_unchecked`) can read them, e.g. `#[repr(transparent)]`,
`#[sqlx(transparent)]` or field-level garde validation:

```rust,ignore
#[nutype(
sanitize(trim),
derive(Debug, Clone),
derive_unchecked(garde::Validate),
)]
pub struct UserId(#[garde(length(min = 1))] String);
```

Exceptions: `#[derive(...)]` must go through `#[nutype(derive(...))]`,
`#[serde(...)]` is handled by nutype itself (see below), and `#[cfg(...)]` is
rejected on the inner field.

nutype forwards attributes verbatim and cannot verify what they do: an
attribute macro that rewrites the type can break nutype's guarantees, exactly
like `derive_unchecked`.

### Serde customization

nutype generates its own `Serialize`/`Deserialize` impls and understands
field-level `#[serde(with = "...")]`, `#[serde(serialize_with = "...")]`,
`#[serde(deserialize_with = "...")]` and type-level `#[serde(transparent)]`
natively. Sanitization and validation always run on deserialization, even
with a custom `deserialize_with` function:

```rust,ignore
#[nutype(
validate(predicate = |v| !v.is_empty()),
derive(Debug, Serialize, Deserialize),
)]
pub struct InvitationToken(#[serde(with = "base64_codec")] Vec<u8>);
```


## Constants

You can mark a type with the `const_fn` flag. In that case, its `new` and `try_new` functions will be declared as `const`:
Expand Down
3 changes: 2 additions & 1 deletion examples/derive_unchecked_example/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
derive_more = { version = "2.0.1", features = ["deref", "deref_mut"] }
arbitrary = { version = "1", features = ["derive"] }
derive_more = { version = "2.0.1", features = ["deref", "deref_mut", "display"] }
nutype = { path = "../../nutype", features = ["derive_unchecked"] }
33 changes: 33 additions & 0 deletions examples/derive_unchecked_example/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,36 @@ fn main() {
// OH no, we've just violated the validation rule!
assert_eq!(temperature.as_ref(), &2.5);
}

#[cfg(test)]
mod tests {
use super::*;

// Proof that field attributes are forwarded to the generated field and
// read by a real third-party derive: `arbitrary`'s derive honors
// `#[arbitrary(value = ...)]`. If the attribute were dropped (the old
// nutype behavior), `arbitrary` would produce 0 from empty input
// instead of 42.
#[test]
fn field_attr_reaches_third_party_derive() {
use arbitrary::{Arbitrary, Unstructured};

#[nutype(derive(Debug), derive_unchecked(::arbitrary::Arbitrary))]
struct Fixed(#[arbitrary(value = 42)] i32);

let mut u = Unstructured::new(&[]);
let value = Fixed::arbitrary(&mut u).unwrap();
assert_eq!(value.into_inner(), 42);
}

// Type-level attributes are forwarded as well: `derive_more::Display`
// reads the `#[display(...)]` attribute from the type.
#[test]
fn type_attr_reaches_third_party_derive() {
#[nutype(derive(Debug), derive_unchecked(derive_more::Display))]
#[display("ID-{_0}")]
struct Id(u32);

assert_eq!(Id::new(7).to_string(), "ID-7");
}
}
40 changes: 40 additions & 0 deletions nutype/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,46 @@
//! Note that a trait cannot appear in both unconditional `derive` and `cfg_attr` `derive` at the same time.
//!
//!
//! ## Attribute passthrough
//!
//! Attributes on the struct and on the inner field are forwarded verbatim
//! onto the generated type, so the compiler and third-party derives (via
//! `derive_unchecked`) can read them:
//!
//! ```ignore
//! #[nutype(
//! sanitize(trim),
//! derive(Debug, Clone),
//! derive_unchecked(garde::Validate),
//! )]
//! pub struct UserId(#[garde(length(min = 1))] String);
//! ```
//!
//! Exceptions: `#[derive(...)]` must go through `#[nutype(derive(...))]`,
//! `#[serde(...)]` is handled by nutype itself (see below), and `#[cfg(...)]`
//! is rejected on the inner field.
//!
//! nutype cannot verify what a forwarded attribute does: an attribute macro
//! that rewrites the type can break nutype's guarantees, exactly like
//! `derive_unchecked`.
//!
//! ### Serde customization
//!
//! nutype generates its own `Serialize`/`Deserialize` impls and understands
//! field-level `#[serde(with = "...")]`, `#[serde(serialize_with = "...")]`,
//! `#[serde(deserialize_with = "...")]` and type-level `#[serde(transparent)]`
//! natively. Sanitization and validation always run on deserialization, even
//! with a custom `deserialize_with` function:
//!
//! ```ignore
//! #[nutype(
//! validate(predicate = |v| !v.is_empty()),
//! derive(Debug, Serialize, Deserialize),
//! )]
//! pub struct InvitationToken(#[serde(with = "base64_codec")] Vec<u8>);
//! ```
//!
//!
//! ## Constants
//!
//! You can mark a type with the `const_fn` flag. In that case, its `new` and `try_new` functions will be declared as `const`:
Expand Down
6 changes: 4 additions & 2 deletions nutype_macros/src/any/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use crate::common::{
GenerateNewtype, tests::gen_test_should_have_valid_default_value, traits::GeneratedTraits,
},
models::{
ConditionalDeriveGroup, ConstFn, ErrorTypePath, Guard, SpannedDeriveUnsafeTrait, TypeName,
TypedCustomFunction,
ConditionalDeriveGroup, ConstFn, ErrorTypePath, Guard, SerdeCustomization,
SpannedDeriveUnsafeTrait, TypeName, TypedCustomFunction,
},
};

Expand Down Expand Up @@ -127,6 +127,7 @@ impl GenerateNewtype for AnyNewtype {
maybe_default_value: Option<syn::Expr>,
guard: &AnyGuard,
conditional_derives: &[ConditionalDeriveGroup<Self::TypedTrait>],
serde_customization: &SerdeCustomization,
) -> Result<GeneratedTraits, syn::Error> {
gen_traits(
type_name,
Expand All @@ -137,6 +138,7 @@ impl GenerateNewtype for AnyNewtype {
maybe_default_value,
guard,
conditional_derives,
serde_customization,
)
}

Expand Down
11 changes: 8 additions & 3 deletions nutype_macros/src/any/generate/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
gen_impl_trait_serde_deserialize, gen_impl_trait_serde_serialize,
gen_impl_trait_try_from, process_conditional_derives, split_into_generatable_traits,
},
models::{ConditionalDeriveGroup, SpannedDeriveUnsafeTrait, TypeName},
models::{ConditionalDeriveGroup, SerdeCustomization, SpannedDeriveUnsafeTrait, TypeName},
},
};

Expand Down Expand Up @@ -133,6 +133,7 @@ pub fn gen_traits(
maybe_default_value: Option<syn::Expr>,
guard: &AnyGuard,
conditional_derives: &[ConditionalDeriveGroup<AnyDeriveTrait>],
serde_customization: &SerdeCustomization,
) -> Result<GeneratedTraits, syn::Error> {
let GeneratableTraits {
transparent_traits,
Expand All @@ -153,6 +154,7 @@ pub fn gen_traits(
irregular_traits,
maybe_default_value.clone(),
guard,
serde_customization,
)?;

let ConditionalTraits {
Expand All @@ -167,6 +169,7 @@ pub fn gen_traits(
irregular,
maybe_default_value.clone(),
guard,
serde_customization,
)
})?;

Expand All @@ -179,13 +182,15 @@ pub fn gen_traits(
})
}

#[allow(clippy::too_many_arguments)]
fn gen_implemented_traits(
type_name: &TypeName,
generics: &syn::Generics,
inner_type: &AnyInnerType,
impl_traits: Vec<AnyIrregularTrait>,
maybe_default_value: Option<syn::Expr>,
guard: &AnyGuard,
serde_customization: &SerdeCustomization,
) -> Result<TokenStream, syn::Error> {
let maybe_error_type_name = guard.maybe_error_type_path();
impl_traits
Expand Down Expand Up @@ -218,10 +223,10 @@ fn gen_implemented_traits(
Ok(into_iter::gen_impl_trait_into_iter(type_name, generics, inner_type))
}
AnyIrregularTrait::SerdeSerialize => Ok(
gen_impl_trait_serde_serialize(type_name, generics)
gen_impl_trait_serde_serialize(type_name, generics, inner_type, serde_customization)
),
AnyIrregularTrait::SerdeDeserialize => Ok(
gen_impl_trait_serde_deserialize(type_name, generics, inner_type, maybe_error_type_name)
gen_impl_trait_serde_deserialize(type_name, generics, inner_type, maybe_error_type_name, serde_customization)
),
AnyIrregularTrait::ArbitraryArbitrary => arbitrary::gen_impl_trait_arbitrary(type_name, generics, inner_type, guard),
})
Expand Down
6 changes: 6 additions & 0 deletions nutype_macros/src/any/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ impl TypeTrait for AnyDeriveTrait {
fn is_default(&self) -> bool {
self == &AnyDeriveTrait::Default
}
fn is_serde_serialize(&self) -> bool {
self == &AnyDeriveTrait::SerdeSerialize
}
fn is_serde_deserialize(&self) -> bool {
self == &AnyDeriveTrait::SerdeDeserialize
}
}

pub type AnyRawGuard = RawGuard<SpannedAnySanitizer, SpannedAnyValidator>;
Expand Down
Loading
Loading