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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ jobs:
cargo build
else
cargo run
cargo test
fi
done

Expand Down
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
### v0.x.x - 202x-xx-xx
- **[BREAKING]** Rename `derive_unsafe` to `derive_unchecked` (both the feature flag and the attribute).
- **[FEATURE]** Ability to derive [`Valuable`](https://docs.rs/valuable/0.1.1/valuable/trait.Valuable.html) (requires `valuable` feature).
- **[FEATURE]** Support `cfg_attr` for conditional derives, e.g. `cfg_attr(feature = "serde", derive(Serialize, Deserialize))`. Supports complex predicates and multiple entries.
- **[FEATURE]** Support `where` clauses in generic newtypes, including Higher-Ranked Trait Bounds (HRTB) like `for<'a> &'a C: IntoIterator` (see [#160](https://github.com/greyblake/nutype/issues/160)).
- **[FEATURE]** Ability to control constructor visibility with `constructor(visibility = ...)` attribute (see [#211](https://github.com/greyblake/nutype/issues/211)).
- **[FEATURE]** Add `len_utf16_min` and `len_utf16_max` validators for string types to validate UTF-16 code unit length (useful for JavaScript interop) (see [#162](https://github.com/greyblake/nutype/issues/162)).
- **[FEATURE]** Support `where` clauses in generic newtypes, including Higher-Ranked Trait Bounds (HRTB) like `for<'a> &'a C: IntoIterator` (see [#160](https://github.com/greyblake/nutype/issues/160)).
- **[FEATURE]** Ability to derive [`Valuable`](https://docs.rs/valuable/0.1.1/valuable/trait.Valuable.html) (requires `valuable` feature).

### v0.6.2 - 2025-06-30
- **[FEATURE]** Introduce `derive_unsafe(..)` attribute to derive any arbitrary trait (requires `derive_unsafe` feature to be enabled).
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ members = [
"examples/any_generics",
"examples/custom_error",
"examples/const_example", "examples/valuable_example",
"examples/cfg_attr_example",
]
1 change: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ examples:
cargo build
else
cargo run
cargo test
fi
done

Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,44 @@ However, **use this with caution**: `nutype` cannot verify that these traits pre
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).


### `cfg_attr`

You can use `cfg_attr` to conditionally derive traits based on `cfg` predicates:

```rust
#[nutype(
derive(Debug, PartialEq),
cfg_attr(feature = "serde", derive(Serialize, Deserialize)),
)]
pub struct Email(String);
```

Only `derive(...)` and `derive_unchecked(...)` are supported inside `cfg_attr`.

Complex predicates work as well:

```rust
#[nutype(
derive(Debug),
cfg_attr(all(test, debug_assertions), derive(Clone, Display)),
)]
pub struct Label(String);
```

Multiple `cfg_attr` entries are allowed:

```rust
#[nutype(
derive(Debug),
cfg_attr(test, derive(Clone)),
cfg_attr(feature = "serde", derive(Serialize, Deserialize)),
)]
pub struct Tag(String);
```

Note that a trait cannot appear in both unconditional `derive` and `cfg_attr` `derive` at the same time.


## 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
13 changes: 13 additions & 0 deletions examples/cfg_attr_example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "cfg_attr_example"
version = "0.1.0"
edition = "2024"
publish = false

[features]
serde = ["dep:serde", "dep:serde_json"]

[dependencies]
nutype = { path = "../../nutype", features = ["serde"] }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
106 changes: 106 additions & 0 deletions examples/cfg_attr_example/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use nutype::nutype;

// 1. Conditional serde behind a feature flag
// Serialize and Deserialize are only derived when the "serde" feature is enabled.
// Note: nutype/serde must be enabled at compile time so the macro accepts these traits,
// but the actual derive is gated by cfg_attr.
#[nutype(
sanitize(trim, lowercase),
validate(not_empty, len_char_max = 100),
derive(Debug, Clone, PartialEq, AsRef),
cfg_attr(feature = "serde", derive(Serialize, Deserialize))
)]
pub struct Email(String);

// 2. Conditional Default for tests
// Default is only derived under `cfg(test)`, but the default value is always specified.
#[nutype(
validate(greater_or_equal = 1, less_or_equal = 65535),
default = 8080,
derive(Debug, Clone, Copy, PartialEq, Into),
cfg_attr(test, derive(Default))
)]
pub struct Port(u16);

// 3. Complex predicate
// Clone and Display are only derived when both `test` and `debug_assertions` are active.
#[nutype(
sanitize(trim),
validate(not_empty, len_char_max = 50),
derive(Debug, PartialEq, AsRef),
cfg_attr(all(test, debug_assertions), derive(Clone, Display))
)]
pub struct Label(String);

// 4. Multiple cfg_attr entries
// Each cfg_attr line is independent and can gate different traits behind different predicates.
#[nutype(
validate(not_empty),
derive(Debug),
cfg_attr(test, derive(Clone)),
cfg_attr(feature = "serde", derive(Serialize, Deserialize))
)]
pub struct Tag(String);

fn main() {
// Exercise Email
let email = Email::try_new(" Alice@Example.COM ").unwrap();
assert_eq!(email.as_ref(), "alice@example.com");
println!("Email: {email:?}");

// Exercise Email with serde (only when the feature is enabled)
#[cfg(feature = "serde")]
{
let json = serde_json::to_string(&email).unwrap();
println!("Email as JSON: {json}");

let parsed: Email = serde_json::from_str(&json).unwrap();
assert_eq!(email, parsed);
println!("Round-tripped email: {parsed:?}");
}

// Exercise Port
let port = Port::try_new(3000).unwrap();
let port_val: u16 = port.into();
assert_eq!(port_val, 3000u16);
println!("Port: {port:?}");

// Exercise Label
let label = Label::try_new(" Rust ").unwrap();
assert_eq!(label.as_ref(), "Rust");
println!("Label: {label:?}");

// Exercise Tag
let tag = Tag::try_new("nutype").unwrap();
println!("Tag: {tag:?}");

// Exercise Tag with serde (only when the feature is enabled)
#[cfg(feature = "serde")]
{
let json = serde_json::to_string(&tag).unwrap();
println!("Tag as JSON: {json}");
}

println!("All cfg_attr examples passed!");
}

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

#[test]
fn test_port_default() {
// Default is conditionally derived under cfg(test), so this works in tests.
let port = Port::default();
let port_val: u16 = port.into();
assert_eq!(port_val, 8080u16);
}

#[test]
fn test_tag_clone() {
// Clone is conditionally derived under cfg(test).
let tag = Tag::try_new("example").unwrap();
let tag2 = tag.clone();
assert_eq!(format!("{tag:?}"), format!("{tag2:?}"));
}
}
44 changes: 44 additions & 0 deletions nutype/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,50 @@
//! 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).
//!
//!
//! ### `cfg_attr`
//!
//! You can use `cfg_attr` to conditionally derive traits based on `cfg` predicates:
//!
//! ```rust
//! use nutype::nutype;
//!
//! #[nutype(
//! derive(Debug, PartialEq),
//! cfg_attr(test, derive(Clone)),
//! )]
//! pub struct Email(String);
//! ```
//!
//! Only `derive(...)` and `derive_unchecked(...)` are supported inside `cfg_attr`.
//!
//! Complex predicates work as well:
//!
//! ```rust
//! use nutype::nutype;
//!
//! #[nutype(
//! derive(Debug),
//! cfg_attr(all(test, debug_assertions), derive(Clone, Display)),
//! )]
//! pub struct Label(String);
//! ```
//!
//! Multiple `cfg_attr` entries are allowed:
//!
//! ```rust
//! use nutype::nutype;
//!
//! #[nutype(
//! derive(Debug),
//! cfg_attr(test, derive(Clone)),
//! cfg_attr(test, derive(Display)),
//! )]
//! pub struct Tag(String);
//! ```
//!
//! Note that a trait cannot appear in both unconditional `derive` and `cfg_attr` `derive` at the same time.
//!
//!
//! ## 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
5 changes: 4 additions & 1 deletion nutype_macros/src/any/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use crate::common::{
GenerateNewtype, tests::gen_test_should_have_valid_default_value, traits::GeneratedTraits,
},
models::{
ConstFn, ErrorTypePath, Guard, SpannedDeriveUnsafeTrait, TypeName, TypedCustomFunction,
ConditionalDeriveGroup, ConstFn, ErrorTypePath, Guard, SpannedDeriveUnsafeTrait, TypeName,
TypedCustomFunction,
},
};

Expand Down Expand Up @@ -125,6 +126,7 @@ impl GenerateNewtype for AnyNewtype {
unsafe_traits: &[SpannedDeriveUnsafeTrait],
maybe_default_value: Option<syn::Expr>,
guard: &AnyGuard,
conditional_derives: &[ConditionalDeriveGroup<Self::TypedTrait>],
) -> Result<GeneratedTraits, syn::Error> {
gen_traits(
type_name,
Expand All @@ -134,6 +136,7 @@ impl GenerateNewtype for AnyNewtype {
unsafe_traits,
maybe_default_value,
guard,
conditional_derives,
)
}

Expand Down
43 changes: 36 additions & 7 deletions nutype_macros/src/any/generate/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ use crate::{
any::models::{AnyDeriveTrait, AnyGuard, AnyInnerType},
common::{
generate::traits::{
GeneratableTrait, GeneratableTraits, GeneratedTraits, gen_impl_trait_as_ref,
gen_impl_trait_borrow, gen_impl_trait_default, gen_impl_trait_deref,
gen_impl_trait_display, gen_impl_trait_from, gen_impl_trait_from_str,
gen_impl_trait_into, gen_impl_trait_serde_deserialize, gen_impl_trait_serde_serialize,
gen_impl_trait_try_from, split_into_generatable_traits,
ConditionalTraits, GeneratableTrait, GeneratableTraits, GeneratedTraits,
HasGeneratedParseError, gen_impl_trait_as_ref, gen_impl_trait_borrow,
gen_impl_trait_default, gen_impl_trait_deref, gen_impl_trait_display,
gen_impl_trait_from, gen_impl_trait_from_str, gen_impl_trait_into,
gen_impl_trait_serde_deserialize, gen_impl_trait_serde_serialize,
gen_impl_trait_try_from, process_conditional_derives, split_into_generatable_traits,
},
models::{SpannedDeriveUnsafeTrait, TypeName},
models::{ConditionalDeriveGroup, SpannedDeriveUnsafeTrait, TypeName},
},
};

Expand Down Expand Up @@ -114,6 +115,15 @@ enum AnyIrregularTrait {
ArbitraryArbitrary,
}

/// Any's `FromStr` generates a `ParseError` type via `gen_impl_trait_from_str` ->
/// `gen_def_parse_error`, which needs module-level re-export in conditional derives.
impl HasGeneratedParseError for AnyIrregularTrait {
fn has_generated_parse_error(&self) -> bool {
matches!(self, Self::FromStr)
}
}

#[allow(clippy::too_many_arguments)]
pub fn gen_traits(
type_name: &TypeName,
generics: &syn::Generics,
Expand All @@ -122,6 +132,7 @@ pub fn gen_traits(
unsafe_traits: &[SpannedDeriveUnsafeTrait],
maybe_default_value: Option<syn::Expr>,
guard: &AnyGuard,
conditional_derives: &[ConditionalDeriveGroup<AnyDeriveTrait>],
) -> Result<GeneratedTraits, syn::Error> {
let GeneratableTraits {
transparent_traits,
Expand All @@ -140,13 +151,31 @@ pub fn gen_traits(
generics,
inner_type,
irregular_traits,
maybe_default_value,
maybe_default_value.clone(),
guard,
)?;

let ConditionalTraits {
derive_transparent_traits: conditional_derive_transparent_traits,
implement_traits: conditional_implement_traits,
from_str_parse_errors: conditional_from_str_parse_errors,
} = process_conditional_derives(conditional_derives, type_name, |irregular| {
gen_implemented_traits(
type_name,
generics,
inner_type,
irregular,
maybe_default_value.clone(),
guard,
)
})?;

Ok(GeneratedTraits {
derive_transparent_traits,
implement_traits,
conditional_derive_transparent_traits,
conditional_implement_traits,
conditional_from_str_parse_errors,
})
}

Expand Down
Loading
Loading