Skip to content
Open
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 @@ -27,7 +27,7 @@
"uses": "actions-rs/cargo@v1",
"with": {
"command": "clippy",
"args": "--workspace --all-targets --features macro,arbitrary1,bytemuck1,num-traits02,serde1,zerocopy,std -- -Dwarnings",
"args": "--workspace --all-targets --features macro,arbitrary1,bytemuck1,num-traits02,serde1,schemars1,zerocopy,std -- -Dwarnings",
},
},
],
Expand All @@ -49,7 +49,7 @@
"uses": "actions-rs/cargo@v1",
"with": {
"command": "test",
"args": "--workspace --features macro,arbitrary1,bytemuck1,num-traits02,serde1,zerocopy,std",
"args": "--workspace --features macro,arbitrary1,bytemuck1,num-traits02,serde1,schemars1,zerocopy,std",
},
},
],
Expand Down
40 changes: 40 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ bounded-integer-macro = { path = "./macro", version = "=0.6.0", optional = true
arbitrary1 = { package = "arbitrary", version = "1.0.2", optional = true }
bytemuck1 = { package = "bytemuck", version = "1.7.2", optional = true }
num-traits02 = { package = "num-traits", version = "0.2.14", default-features = false, features = ["i128"], optional = true }
schemars1 = { package = "schemars", version = "1.0.0", default-features = false, optional = true }
serde1 = { package = "serde", version = "1.0.124", default-features = false, optional = true }
serde_json1 = { package = "serde_json", version = "1.0.142", default-features = false, optional = true }
zerocopy = { version = "0.8.14", features = ["derive"], optional = true }

[profile.test.package.optimization-tests]
Expand All @@ -28,6 +30,8 @@ macro = ["bounded-integer-macro"]

step_trait = []

schemars1 = ["alloc", "dep:schemars1", "dep:serde_json1"]

# `__doc` is set when building the crate locally, but users should not set it.
# There are a few reasons we use it instead of `cfg(doc)`:
# + The examples are gated based on this feature, and we want them to typecheck.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ By default, no crate features are enabled.
[`SaturatingSub`] for all bounded integers.
- `serde1`: Implement [`Serialize`] and [`Deserialize`] for the bounded integers, making sure all
values will never be out of bounds.
- `schemars1`: Implement [`JsonSchema`] for the bounded integers, describing them as their
underlying primitive constrained to the type’s range via `minimum` and `maximum`. Bounds of
`i128`/`u128`-backed integers that fall outside the `i64`/`u64` range are omitted unless
`serde_json`’s `arbitrary_precision` feature is enabled (which has a significant runtime cost).
- `zerocopy`: Implement [`IntoBytes`] and [`Immutable`] for all bounded integers,
[`Unaligned`] for ones backed by `u8` or `i8`,
and [`FromZeros`] for suitable macro-generated ones.
Expand Down Expand Up @@ -83,6 +87,7 @@ By default, no crate features are enabled.
[`SaturatingSub`]: https://docs.rs/num-traits/0.2/num_traits/ops/saturating/trait.SaturatingSub.html
[`Serialize`]: https://docs.rs/serde/1/serde/trait.Serialize.html
[`Deserialize`]: https://docs.rs/serde/1/serde/trait.Deserialize.html
[`JsonSchema`]: https://docs.rs/schemars/1/schemars/trait.JsonSchema.html
[`IntoBytes`]: https://docs.rs/zerocopy/0.8/zerocopy/trait.IntoBytes.html
[`FromZeros`]: https://docs.rs/zerocopy/0.8/zerocopy/trait.FromZeros.html
[`Immutable`]: https://docs.rs/zerocopy/0.8/zerocopy/trait.Immutable.html
Expand Down
12 changes: 12 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
//! [`SaturatingSub`] for all bounded integers.
//! - `serde1`: Implement [`Serialize`] and [`Deserialize`] for the bounded integers, making sure all
//! values will never be out of bounds.
//! - `schemars1`: Implement [`JsonSchema`] for the bounded integers, describing them as their
//! underlying primitive constrained to the type’s range via `minimum` and `maximum`. Bounds of
//! `i128`/`u128`-backed integers that fall outside the `i64`/`u64` range are omitted unless
//! `serde_json`’s `arbitrary_precision` feature is enabled (which has a significant runtime cost).
//! - `zerocopy`: Implement [`IntoBytes`] and [`Immutable`] for all bounded integers,
//! [`Unaligned`] for ones backed by `u8` or `i8`,
//! and [`FromZeros`] for suitable macro-generated ones.
Expand Down Expand Up @@ -87,6 +91,7 @@
//! [`SaturatingSub`]: https://docs.rs/num-traits/0.2/num_traits/ops/saturating/trait.SaturatingSub.html
//! [`Serialize`]: https://docs.rs/serde/1/serde/trait.Serialize.html
//! [`Deserialize`]: https://docs.rs/serde/1/serde/trait.Deserialize.html
//! [`JsonSchema`]: https://docs.rs/schemars/1/schemars/trait.JsonSchema.html
//! [`IntoBytes`]: https://docs.rs/zerocopy/0.8/zerocopy/trait.IntoBytes.html
//! [`FromZeros`]: https://docs.rs/zerocopy/0.8/zerocopy/trait.FromZeros.html
//! [`Immutable`]: https://docs.rs/zerocopy/0.8/zerocopy/trait.Immutable.html
Expand Down Expand Up @@ -122,6 +127,9 @@ pub use prim_int::TryFromError;
// Not public API.
#[doc(hidden)]
pub mod __private {
#[cfg(feature = "alloc")]
pub extern crate alloc;

#[cfg(feature = "arbitrary1")]
pub use ::arbitrary1;

Expand All @@ -131,6 +139,9 @@ pub mod __private {
#[cfg(feature = "num-traits02")]
pub use ::num_traits02;

#[cfg(feature = "schemars1")]
pub use ::{schemars1, serde_json1};

#[cfg(feature = "serde1")]
pub use ::serde1;

Expand All @@ -155,6 +166,7 @@ pub mod __private {
__cfg_arbitrary1 "arbitrary1",
__cfg_bytemuck1 "bytemuck1",
__cfg_num_traits02 "num-traits02",
__cfg_schemars1 "schemars1",
__cfg_serde1 "serde1",
__cfg_step_trait "step_trait",
__cfg_zerocopy "zerocopy",
Expand Down
100 changes: 100 additions & 0 deletions src/macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,106 @@ mod tests {
assert_one::<A>();
}

#[test]
#[cfg(feature = "schemars1")]
fn schemars() {
use schemars1::{generate::SchemaSettings, schema_for};
use serde_json1::json;

let meta = SchemaSettings::default().meta_schema.unwrap();

bounded_integer!(struct A(3, 12););
assert_eq!(
schema_for!(A),
json!({
"$schema": meta,
"type": "integer",
"title": "uint8",
"format": "uint8",
"minimum": 3,
"maximum": 12,
})
);

// Signed ranges keep their negative bounds.
bounded_integer!(struct B(-10_000_000_005, -10_000_000_001););
assert_eq!(
schema_for!(B),
json!({
"$schema": meta,
"type": "integer",
"title": "int64",
"format": "int64",
"minimum": -10_000_000_005i64,
"maximum": -10_000_000_001i64,
})
);

// Enums work just like structs.
bounded_integer!(enum C(-1, 1););
assert_eq!(
schema_for!(C),
json!({
"$schema": meta,
"type": "integer",
"title": "int8",
"format": "int8",
"minimum": -1,
"maximum": 1,
})
);

// Including enums with named variants.
bounded_integer! {
enum Sign {
#[allow(unused)]
Negative = -1,
#[allow(unused)]
Zero,
#[allow(unused)]
Positive,
}
}
assert_eq!(
schema_for!(Sign),
json!({
"$schema": meta,
"type": "integer",
"title": "int8",
"format": "int8",
"minimum": -1,
"maximum": 1,
})
);

// A custom `repr` is reflected in the schema’s `format`.
bounded_integer!(#[repr(u16)] struct D(2, 4););
assert_eq!(
schema_for!(D),
json!({
"$schema": meta,
"type": "integer",
"title": "uint16",
"format": "uint16",
"minimum": 2,
"maximum": 4,
})
);

// Assuming serde_json/arbitrary_precision is disabled:
// Bounds that fall outside the i64/u64 range are omitted but the type is still supported.
bounded_integer!(struct E(0x1_0000_0000_0000_0000, 0x2_0000_0000_0000_0000););
assert_eq!(
schema_for!(E),
json!({
"$schema": meta,
"type": "integer",
"title": "uint128",
"format": "uint128",
})
);
}

#[test]
#[cfg(feature = "zerocopy")]
fn unaligned() {
Expand Down
43 changes: 43 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,49 @@ mod tests {
<BoundedI8<2, 5>>::try_from(8_u16).unwrap_err();
}

#[test]
#[cfg(feature = "schemars1")]
fn schemars() {
use schemars1::{generate::SchemaSettings, schema_for};
use serde_json1::json;

let meta = SchemaSettings::default().meta_schema.unwrap();

assert_eq!(
schema_for!(BoundedU16<10, 20>),
json!({
"$schema": meta,
"type": "integer",
"title": "uint16",
"format": "uint16",
"minimum": 10,
"maximum": 20,
})
);

assert_eq!(
schema_for!(BoundedI32<-8, 8>),
json!({
"$schema": meta,
"type": "integer",
"title": "int32",
"format": "int32",
"minimum": -8,
"maximum": 8,
})
);

assert_eq!(
schema_for!(BoundedI128<-0x1_0000_0000_0000_0000, 0x1_0000_0000_0000_0000>),
json!({
"$schema": meta,
"type": "integer",
"title": "int128",
"format": "int128",
})
);
}

#[test]
#[cfg(feature = "num-traits02")]
#[expect(clippy::too_many_lines)]
Expand Down
42 changes: 42 additions & 0 deletions src/unsafe_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,48 @@ macro_rules! __unsafe_api_internal {
}
}
}

// === Schemars ===

$crate::__private::__cfg_schemars1! {
use $crate::__private::alloc::borrow::{Cow, ToOwned};
use $crate::__private::schemars1::{JsonSchema, Schema, SchemaGenerator};
use $crate::__private::serde_json1::to_value;

#[automatically_derived]
impl<$($generics)*> JsonSchema for $ty where $($where)* {
// Like the primitive integers, a bounded integer is a simple constrained scalar
// that is clearer inlined than referenced through `$defs`.
fn inline_schema() -> bool {
true
}

fn schema_name() -> Cow<'static, str> {
<$inner as JsonSchema>::schema_name()
}

fn json_schema(generator: &mut SchemaGenerator) -> Schema {
let mut schema = <$inner as JsonSchema>::json_schema(generator);

// `to_value` only fails for `i128`/`u128` bounds outside the `i64`/`u64`
// range, which serde_json can represent solely with its `arbitrary_precision`
// feature (at significant runtime cost); when it fails we leave the bound
// unspecified.
if let Ok(minimum) = to_value(Self::MIN_VALUE) {
schema.insert("minimum".to_owned(), minimum);
} else {
schema.remove("minimum");
}
if let Ok(maximum) = to_value(Self::MAX_VALUE) {
schema.insert("maximum".to_owned(), maximum);
} else {
schema.remove("maximum");
}

schema
}
}
}
}; };

(@repr u8, $($rest:tt)*) => { $crate::__unsafe_api_internal! {
Expand Down
11 changes: 9 additions & 2 deletions ui/bad_range.stderr
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
error[E0080]: evaluation panicked: range minimum should be less than maximum
--> src/types.rs
--> src/unsafe_api.rs
|
| pub const MIN: Self = Self::new(Self::MIN_VALUE)
| ___________________________________^
| | .expect("range minimum should be less than maximum");
| |____________________________________________________________________^ evaluation of `bounded_integer::types::_::<impl bounded_integer::BoundedUsize<5, 4>>::MIN` failed here
|
::: src/types.rs
|
| / define_bounded_integers! {
| | BoundedU8(u8, unaligned, unsigned),
Expand All @@ -8,7 +15,7 @@ error[E0080]: evaluation panicked: range minimum should be less than maximum
... |
| | BoundedIsize(isize),
| | }
| |_^ evaluation of `bounded_integer::types::_::<impl bounded_integer::BoundedUsize<5, 4>>::MIN` failed here
| |_- in this macro invocation
|
= note: this error originates in the macro `$crate::__unsafe_api_internal` which comes from the expansion of the macro `define_bounded_integers` (in Nightly builds, run with -Z macro-backtrace for more info)

Expand Down
Loading
Loading