diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bc93ab74a..f349ed0bc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -118,6 +118,7 @@ jobs:
- { feature: chrono-tz, crate: juniper }
- { feature: expose-test-schema, crate: juniper }
- { feature: jiff, crate: juniper }
+ - { feature: ruint, crate: juniper }
- { feature: rust_decimal, crate: juniper }
- { feature: schema-language, crate: juniper }
- { feature: time, crate: juniper }
diff --git a/book/src/types/scalars.md b/book/src/types/scalars.md
index 95690a955..4d81b0961 100644
--- a/book/src/types/scalars.md
+++ b/book/src/types/scalars.md
@@ -439,6 +439,12 @@ mod date_scalar {
| [`chrono::NaiveDateTime`] | [`LocalDateTime`] | [`chrono`] |
| [`chrono::DateTime`] | [`DateTime`] | [`chrono`] |
| [`chrono_tz::Tz`] | [`TimeZone`] | [`chrono-tz`] |
+| [`ruint::aliases::U8`] | `U8` | [`ruint`] |
+| [`ruint::aliases::U16`] | `U16` | [`ruint`] |
+| [`ruint::aliases::U32`] | `U32` | [`ruint`] |
+| [`ruint::aliases::U64`] | `U64` | [`ruint`] |
+| [`ruint::aliases::U128`] | `U128` | [`ruint`] |
+| [`ruint::aliases::U256`] | `U256` | [`ruint`] |
| [`rust_decimal::Decimal`] | `Decimal` | [`rust_decimal`] |
| [`jiff::civil::Date`] | [`LocalDate`] | [`jiff`] |
| [`jiff::civil::Time`] | [`LocalTime`] | [`jiff`] |
@@ -489,6 +495,13 @@ mod date_scalar {
[`LocalDateTime`]: https://graphql-scalars.dev/docs/scalars/local-date-time
[`LocalTime`]: https://graphql-scalars.dev/docs/scalars/local-time
[`ObjectID`]: https://the-guild.dev/graphql/scalars/docs/scalars/object-id
+[`ruint`]: https://docs.rs/ruint
+[`ruint::aliases::U8`]: https://docs.rs/ruint/latest/ruint/aliases/type.U8.html
+[`ruint::aliases::U16`]: https://docs.rs/ruint/latest/ruint/aliases/type.U16.html
+[`ruint::aliases::U32`]: https://docs.rs/ruint/latest/ruint/aliases/type.U32.html
+[`ruint::aliases::U64`]: https://docs.rs/ruint/latest/ruint/aliases/type.U64.html
+[`ruint::aliases::U128`]: https://docs.rs/ruint/latest/ruint/aliases/type.U128.html
+[`ruint::aliases::U256`]: https://docs.rs/ruint/latest/ruint/aliases/type.U256.html
[`rust_decimal`]: https://docs.rs/rust_decimal
[`rust_decimal::Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html
[`ScalarValue`]: https://docs.rs/juniper/0.17.0/juniper/trait.ScalarValue.html
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index ad02e14b6..b7a19c4e3 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -42,6 +42,14 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Support parsing descriptions on operations, fragments and variable definitions. ([#1349], [graphql/graphql-spec#1170])
- Support for [block strings][0180-1]. ([#1349])
- Support of `#[graphql(rename_all = "snake_case")]` attribute in macros. ([#1354])
+- [`ruint` crate] integration behind `ruint` [Cargo feature]: ([#1355], [#1350])
+ - `ruint::aliases::U8` as `U8` scalar.
+ - `ruint::aliases::U16` as `U16` scalar.
+ - `ruint::aliases::U32` as `U32` scalar.
+ - `ruint::aliases::U64` as `U64` scalar.
+ - `ruint::aliases::U128` as `U128` scalar.
+ - `ruint::aliases::U256` as `U256` scalar.
+ - `integrations::ruint::unit_scalar` module for declaring custom-sized `ruint::Unit` scalars.
### Changed
@@ -60,8 +68,10 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1347]: /../../issues/1347
[#1348]: /../../pull/1348
[#1349]: /../../pull/1349
+[#1350]: /../../issues/1350
[#1353]: /../../pull/1353
[#1354]: /../../pull/1354
+[#1355]: /../../pull/1355
[graphql/graphql-spec#525]: https://github.com/graphql/graphql-spec/pull/525
[graphql/graphql-spec#687]: https://github.com/graphql/graphql-spec/issues/687
[graphql/graphql-spec#805]: https://github.com/graphql/graphql-spec/pull/805
diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml
index cda6c2564..4f8a30e5f 100644
--- a/juniper/Cargo.toml
+++ b/juniper/Cargo.toml
@@ -35,6 +35,7 @@ chrono-tz = ["dep:chrono-tz", "dep:regex"]
expose-test-schema = ["dep:anyhow", "dep:serde_json"]
jiff = ["dep:jiff"]
js = ["chrono?/wasmbind", "time?/wasm-bindgen", "uuid?/js"]
+ruint = ["dep:ruint"]
rust_decimal = ["dep:rust_decimal"]
schema-language = ["dep:graphql-parser", "dep:void"]
time = ["dep:time"]
@@ -60,6 +61,7 @@ itertools = "0.14"
jiff = { version = "0.2", features = ["std"], default-features = false, optional = true }
juniper_codegen = { version = "0.17.0", path = "../juniper_codegen" }
ref-cast = "1.0"
+ruint = { version = "1.10", optional = true }
rust_decimal = { version = "1.20", default-features = false, optional = true }
ryu = { version = "1.0", optional = true }
serde = { version = "1.0.122", features = ["derive"] }
diff --git a/juniper/README.md b/juniper/README.md
index 4154d99d9..7a91a2599 100644
--- a/juniper/README.md
+++ b/juniper/README.md
@@ -49,6 +49,7 @@ As an exception to other [GraphQL] libraries for other languages, [Juniper] buil
- [`bson`]
- [`chrono`], [`chrono-tz`]
- [`jiff`]
+- [`ruint`]
- [`rust_decimal`]
- [`time`]
- [`url`]
@@ -94,6 +95,7 @@ This project is licensed under [BSD 2-Clause License](https://github.com/graphql
[`juniper_warp`]: https://docs.rs/juniper_warp
[`hyper`]: https://docs.rs/hyper
[`rocket`]: https://docs.rs/rocket
+[`ruint`]: https://docs.rs/ruint
[`rust_decimal`]: https://docs.rs/rust_decimal
[`time`]: https://docs.rs/time
[`url`]: https://docs.rs/url
diff --git a/juniper/src/integrations/mod.rs b/juniper/src/integrations/mod.rs
index 3f16037e6..942524a7b 100644
--- a/juniper/src/integrations/mod.rs
+++ b/juniper/src/integrations/mod.rs
@@ -12,6 +12,8 @@ pub mod chrono;
pub mod chrono_tz;
#[cfg(feature = "jiff")]
pub mod jiff;
+#[cfg(feature = "ruint")]
+pub mod ruint;
#[cfg(feature = "rust_decimal")]
pub mod rust_decimal;
#[doc(hidden)]
diff --git a/juniper/src/integrations/ruint.rs b/juniper/src/integrations/ruint.rs
new file mode 100644
index 000000000..3ded63824
--- /dev/null
+++ b/juniper/src/integrations/ruint.rs
@@ -0,0 +1,383 @@
+//! GraphQL support for [`ruint`] crate types.
+//!
+//! # Supported types
+//!
+//! | Rust type | GraphQL scalar |
+//! |----------------|----------------|
+//! | [`U8`] | `U8` |
+//! | [`U16`] | `U16` |
+//! | [`U32`] | `U32` |
+//! | [`U64`] | `U64` |
+//! | [`U128`] | `U128` |
+//! | [`U256`] | `U256` |
+//!
+//! # Custom-sized type
+//!
+//! Any custom variation of the [`ruint::Uint`] type could be made into a [GraphQL scalar][0] by
+//! reusing the [`integrations::ruint::uint_scalar`] module.
+//!
+//! However, to satisfy [orphan rules], a local [`ScalarValue`] implementation should be provided:
+//! ```rust
+//! # use derive_more::{Display, From, TryInto};
+//! # use juniper::{ScalarValue, graphql_scalar};
+//! # use serde::{Deserialize, Serialize};
+//! #
+//! #[derive(Clone, Debug, Deserialize, Display, From, PartialEq, ScalarValue, Serialize, TryInto)]
+//! #[serde(untagged)]
+//! enum CustomScalarValue {
+//! #[value(to_float, to_int)]
+//! Int(i32),
+//! #[value(to_float)]
+//! Float(f64),
+//! #[value(as_str, to_string)]
+//! String(String),
+//! #[value(to_bool)]
+//! Boolean(bool),
+//! }
+//!
+//! #[graphql_scalar]
+//! #[graphql(
+//! with = juniper::integrations::ruint::uint_scalar,
+//! specified_by_url = "https://docs.rs/ruint",
+//! scalar = CustomScalarValue,
+//! )]
+//! type U512 = ruint::Uint<512, 8>;
+//! ```
+//!
+//! [`ScalarValue`]: trait@crate::ScalarValue
+//! [`U256`]: ruint::aliases::U256
+//! [`U128`]: ruint::aliases::U128
+//! [`U64`]: ruint::aliases::U64
+//! [orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
+//! [0]: https://spec.graphql.org/October2021#sec-Scalars
+
+use crate::graphql_scalar;
+
+/// Unsigned integer type representing the ring of numbers modulo 28.
+///
+/// Always serializes as `String` in decimal notation. But may be deserialized both from `Int` and
+/// `String` values with standard Rust syntax for decimal, hexadecimal, binary and octal notation
+/// using prefixes `0x`, `0b` and `0o`.
+///
+/// See also [`ruint`] crate for details.
+///
+/// [`ruint`]: https://docs.rs/ruint
+#[graphql_scalar]
+#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")]
+pub type U8 = ruint::aliases::U8;
+
+/// Unsigned integer type representing the ring of numbers modulo 216.
+///
+/// Always serializes as `String` in decimal notation. But may be deserialized both from `Int` and
+/// `String` values with standard Rust syntax for decimal, hexadecimal, binary and octal notation
+/// using prefixes `0x`, `0b` and `0o`.
+///
+/// See also [`ruint`] crate for details.
+///
+/// [`ruint`]: https://docs.rs/ruint
+#[graphql_scalar]
+#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")]
+pub type U16 = ruint::aliases::U16;
+
+/// Unsigned integer type representing the ring of numbers modulo 232.
+///
+/// Always serializes as `String` in decimal notation. But may be deserialized both from `Int` and
+/// `String` values with standard Rust syntax for decimal, hexadecimal, binary and octal notation
+/// using prefixes `0x`, `0b` and `0o`.
+///
+/// See also [`ruint`] crate for details.
+///
+/// [`ruint`]: https://docs.rs/ruint
+#[graphql_scalar]
+#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")]
+pub type U32 = ruint::aliases::U32;
+
+/// Unsigned integer type representing the ring of numbers modulo 264.
+///
+/// Always serializes as `String` in decimal notation. But may be deserialized both from `Int` and
+/// `String` values with standard Rust syntax for decimal, hexadecimal, binary and octal notation
+/// using prefixes `0x`, `0b` and `0o`.
+///
+/// See also [`ruint`] crate for details.
+///
+/// [`ruint`]: https://docs.rs/ruint
+#[graphql_scalar]
+#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")]
+pub type U64 = ruint::aliases::U64;
+
+/// Unsigned integer type representing the ring of numbers modulo 2128.
+///
+/// Always serializes as `String` in decimal notation. But may be deserialized both from `Int` and
+/// `String` values with standard Rust syntax for decimal, hexadecimal, binary and octal notation
+/// using prefixes `0x`, `0b` and `0o`.
+///
+/// See also [`ruint`] crate for details.
+///
+/// [`ruint`]: https://docs.rs/ruint
+#[graphql_scalar]
+#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")]
+pub type U128 = ruint::aliases::U128;
+
+/// Unsigned integer type representing the ring of numbers modulo 2256.
+///
+/// Always serializes as `String` in decimal notation. But may be deserialized both from `Int` and
+/// `String` values with standard Rust syntax for decimal, hexadecimal, binary and octal notation
+/// using prefixes `0x`, `0b` and `0o`.
+///
+/// See also [`ruint`] crate for details.
+///
+/// [`ruint`]: https://docs.rs/ruint
+#[graphql_scalar]
+#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")]
+pub type U256 = ruint::aliases::U256;
+
+pub mod uint_scalar {
+ //! [GraphQL scalar][0] implementation for [`ruint::Uint`] type, suitable for specifying into
+ //! the `with` argument of the `#[graphql_scalar]`][1] macro.
+ //!
+ //! [0]: https://spec.graphql.org/October2021#sec-Scalars
+ //! [1]: macro@crate::graphql_scalar
+
+ use crate::{ParseScalarResult, ParseScalarValue, Scalar, ScalarToken, ScalarValue};
+
+ /// Parses an arbitrary [`ruint::Uint`] value from the provided [`ScalarValue`].
+ ///
+ /// Expects either `String` or `Int` GraphQL scalars as input, with standard Rust syntax for
+ /// decimal, hexadecimal, binary and octal notation using prefixes `0x`, `0b` and `0o`.
+ ///
+ /// # Errors
+ ///
+ /// If the [`ruint::Uint`] value cannot be parsed from the provided [`ScalarValue`].
+ pub fn from_input(
+ value: &Scalar,
+ ) -> Result, Box> {
+ if let Some(int) = value.try_to_int() {
+ return ruint::Uint::try_from(int).map_err(|e| {
+ format!("Failed to parse `ruint::Uint<{B}, {L}>` from `Int`: {e}").into()
+ });
+ }
+
+ let Some(s) = value.try_as_str() else {
+ return Err(format!(
+ "Failed to parse `ruint::Uint<{B}, {L}>`: input is neither `String` nor `Int`"
+ )
+ .into());
+ };
+ // TODO: Remove once recmo/uint#348 is resolved and released:
+ // https://github.com/recmo/uint/issues/348
+ if s.is_empty() {
+ return Err(format!(
+ "Failed to parse `ruint::Uint<{B}, {L}>` from `String`: cannot be empty",
+ )
+ .into());
+ }
+ s.parse().map_err(|e| {
+ format!("Failed to parse `ruint::Uint<{B}, {L}>` from `String`: {e}").into()
+ })
+ }
+
+ // ERGONOMICS: This method is intentionally placed here to allow omitting specifying another
+ // `to_output_with = ScalarValue::from_displayable` macro argument in the user code
+ // once the `with = juniper::integrations::ruint::uint_scalar` is specified already.
+ /// Converts the provided arbitrary [`ruint::Uint`] value into a [`ScalarValue`].
+ ///
+ /// Always serializes as GraphQL `String` in decimal notation.
+ pub fn to_output(int: &ruint::Uint) -> S {
+ S::from_displayable(int)
+ }
+
+ // ERGONOMICS: This method is intentionally placed here to allow omitting specifying another
+ // `parse_token(i32, String)` macro argument in the user code once the
+ // `with = juniper::integrations::ruint::uint_scalar` is specified already.
+ /// Parses a [`ScalarValue`] from the provided [`ScalarToken`] as the [`ruint::Uint`] requires.
+ ///
+ /// # Errors
+ ///
+ /// If the provided [`ScalarToken`] represents neither `String` nor `Int` GraphQL scalar.
+ pub fn parse_token(token: ScalarToken<'_>) -> ParseScalarResult {
+ >::from_str(token)
+ .or_else(|_| >::from_str(token))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::{U8, U16, U32, U64, U128, U256};
+ use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql};
+
+ #[test]
+ fn parses_correct_input_8() {
+ for (input, expected) in [
+ (graphql::input_value!(0), U8::ZERO),
+ (graphql::input_value!(123), U8::from(123)),
+ (graphql::input_value!("0"), U8::ZERO),
+ (graphql::input_value!("42"), U8::from(42)),
+ (graphql::input_value!("0xbe"), U8::from(0xbe)),
+ ] {
+ let input: InputValue = input;
+ let parsed = U8::from_input_value(&input);
+
+ assert!(
+ parsed.is_ok(),
+ "failed to parse `{input:?}`: {:?}",
+ parsed.unwrap_err(),
+ );
+ assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
+ }
+ }
+
+ #[test]
+ fn parses_correct_input_16() {
+ for (input, expected) in [
+ (graphql::input_value!(0), U16::ZERO),
+ (graphql::input_value!(123), U16::from(123)),
+ (graphql::input_value!("0"), U16::ZERO),
+ (graphql::input_value!("42"), U16::from(42)),
+ (graphql::input_value!("0xbeef"), U16::from(0xbeef)),
+ ] {
+ let input: InputValue = input;
+ let parsed = U16::from_input_value(&input);
+
+ assert!(
+ parsed.is_ok(),
+ "failed to parse `{input:?}`: {:?}",
+ parsed.unwrap_err(),
+ );
+ assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
+ }
+ }
+
+ #[test]
+ fn parses_correct_input_32() {
+ for (input, expected) in [
+ (graphql::input_value!(0), U32::ZERO),
+ (graphql::input_value!(123), U32::from(123)),
+ (graphql::input_value!("0"), U32::ZERO),
+ (graphql::input_value!("42"), U32::from(42)),
+ (
+ graphql::input_value!("0xdeadbeef"),
+ U32::from(3735928559u32),
+ ),
+ ] {
+ let input: InputValue = input;
+ let parsed = U32::from_input_value(&input);
+
+ assert!(
+ parsed.is_ok(),
+ "failed to parse `{input:?}`: {:?}",
+ parsed.unwrap_err(),
+ );
+ assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
+ }
+ }
+
+ #[test]
+ fn parses_correct_input_64() {
+ for (input, expected) in [
+ (graphql::input_value!(0), U64::ZERO),
+ (graphql::input_value!(123), U64::from(123)),
+ (graphql::input_value!("0"), U64::ZERO),
+ (graphql::input_value!("42"), U64::from(42)),
+ (
+ graphql::input_value!("0xdeadbeef"),
+ U64::from(3735928559u64),
+ ),
+ ] {
+ let input: InputValue = input;
+ let parsed = U64::from_input_value(&input);
+
+ assert!(
+ parsed.is_ok(),
+ "failed to parse `{input:?}`: {:?}",
+ parsed.unwrap_err(),
+ );
+ assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
+ }
+ }
+
+ #[test]
+ fn parses_correct_input_128() {
+ for (input, expected) in [
+ (graphql::input_value!(0), U128::ZERO),
+ (graphql::input_value!(123), U128::from(123)),
+ (graphql::input_value!("0"), U128::ZERO),
+ (graphql::input_value!("42"), U128::from(42)),
+ (
+ graphql::input_value!("0xdeadbeef"),
+ U128::from(3735928559u64),
+ ),
+ ] {
+ let input: InputValue = input;
+ let parsed = U128::from_input_value(&input);
+
+ assert!(
+ parsed.is_ok(),
+ "failed to parse `{input:?}`: {:?}",
+ parsed.unwrap_err(),
+ );
+ assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
+ }
+ }
+
+ #[test]
+ fn parses_correct_input_256() {
+ for (input, expected) in [
+ (graphql::input_value!(0), U256::ZERO),
+ (graphql::input_value!(123), U256::from(123)),
+ (graphql::input_value!("0"), U256::ZERO),
+ (graphql::input_value!("42"), U256::from(42)),
+ (graphql::input_value!("0o10"), U256::from(8)),
+ (
+ graphql::input_value!("0xdeadbeef"),
+ U256::from(3735928559u64),
+ ),
+ ] {
+ let input: InputValue = input;
+ let parsed = U256::from_input_value(&input);
+
+ assert!(
+ parsed.is_ok(),
+ "failed to parse `{input:?}`: {:?}",
+ parsed.unwrap_err(),
+ );
+ assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
+ }
+ }
+
+ #[test]
+ fn fails_on_invalid_input() {
+ for input in [
+ graphql::input_value!(""),
+ graphql::input_value!("0,0"),
+ graphql::input_value!("12,"),
+ graphql::input_value!("1996-12-19T14:23:43"),
+ graphql::input_value!("i'm not even a number"),
+ graphql::input_value!(null),
+ graphql::input_value!(false),
+ graphql::input_value!(-123),
+ ] {
+ let input: InputValue = input;
+ let parsed = U256::from_input_value(&input);
+
+ assert!(
+ parsed.is_err(),
+ "allows input: {input:?} {}",
+ parsed.unwrap(),
+ );
+ }
+ }
+
+ #[test]
+ fn formats_correctly() {
+ for (raw, expected) in [
+ ("0", "0"),
+ ("87553378877997984345", "87553378877997984345"),
+ ("123", "123"),
+ ("0x42", "66"),
+ ] {
+ let actual: InputValue = raw.parse::().unwrap().to_input_value();
+
+ assert_eq!(actual, graphql::input_value!((expected)), "on value: {raw}");
+ }
+ }
+}