From 70efe2d2a6a887eee672899da4e0b10a5d972cf1 Mon Sep 17 00:00:00 2001 From: siosw Date: Tue, 14 Oct 2025 22:54:58 +0200 Subject: [PATCH 01/11] feat(integrations): add ruint --- book/src/types/scalars.md | 7 + juniper/Cargo.toml | 2 + juniper/src/integrations/mod.rs | 2 + juniper/src/integrations/ruint.rs | 213 ++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 juniper/src/integrations/ruint.rs diff --git a/book/src/types/scalars.md b/book/src/types/scalars.md index 95690a955..349ba245e 100644 --- a/book/src/types/scalars.md +++ b/book/src/types/scalars.md @@ -439,6 +439,9 @@ mod date_scalar { | [`chrono::NaiveDateTime`] | [`LocalDateTime`] | [`chrono`] | | [`chrono::DateTime`] | [`DateTime`] | [`chrono`] | | [`chrono_tz::Tz`] | [`TimeZone`] | [`chrono-tz`] | +| [`ruint::aliases::U256`] | [`U256`] | [`ruint`] | +| [`ruint::aliases::U128`] | [`U128`] | [`ruint`] | +| [`ruint::aliases::U64`] | [`U64`] | [`ruint`] | | [`rust_decimal::Decimal`] | `Decimal` | [`rust_decimal`] | | [`jiff::civil::Date`] | [`LocalDate`] | [`jiff`] | | [`jiff::civil::Time`] | [`LocalTime`] | [`jiff`] | @@ -472,6 +475,10 @@ mod date_scalar { [`chrono::NaiveTime`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html [`chrono-tz`]: https://docs.rs/chrono-tz [`chrono_tz::Tz`]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html +[`ruint`]: https://docs.rs/ruint +[`ruint::aliases::U256`]: https://docs.rs/ruint/latest/ruint/aliases/type.U256.html +[`ruint::aliases::U128`]: https://docs.rs/ruint/latest/ruint/aliases/type.U128.html +[`ruint::aliases::U64`]: https://docs.rs/ruint/latest/ruint/aliases/type.U64.html [`DateTime`]: https://graphql-scalars.dev/docs/scalars/date-time [`Duration`]: https://graphql-scalars.dev/docs/scalars/duration [`ID`]: https://spec.graphql.org/October2021#sec-ID diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index cda6c2564..b43b48106 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.17.0", features = ["serde"], 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/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..21ac8ebdf --- /dev/null +++ b/juniper/src/integrations/ruint.rs @@ -0,0 +1,213 @@ +//! GraphQL support for [`ruint`] crate types. +//! +//! # Supported types +//! +//! | Rust type | GraphQL scalar | +//! |----------------|----------------| +//! | [`U256`] | `U256` | +//! | [`U128`] | `U128` | +//! | [`U64`] | `U64` | +//! +//! [`U256`]: ruint::aliases::U256 +//! [`U128`]: ruint::aliases::U128 +//! [`U64`]: ruint::aliases::U64 + +use crate::{ScalarValue, graphql_scalar}; + +/// Uint type using const generics. +/// +/// Always serializes as `String` in decimal notation. +/// May be deserialized from `i32` and `String` with +/// standard Rust syntax for decimal, hexadecimal, binary and octal +/// notation using prefixes 0x, 0b and 0o. +/// +/// Confusingly empty strings get parsed as 0 +/// https://github.com/recmo/uint/issues/348 +#[graphql_scalar] +#[graphql( + with = ruint_scalar, + to_output_with = ScalarValue::from_displayable, + parse_token(i32, String), + specified_by_url = "https://docs.rs/ruint", +)] +pub type U64 = ruint::aliases::U64; + +/// Uint type using const generics. +/// +/// Always serializes as `String` in decimal notation. +/// May be deserialized from `i32` and `String` with +/// standard Rust syntax for decimal, hexadecimal, binary and octal +/// notation using prefixes 0x, 0b and 0o. +/// +/// Confusingly empty strings get parsed as 0 +/// https://github.com/recmo/uint/issues/348 +#[graphql_scalar] +#[graphql( + with = ruint_scalar, + to_output_with = ScalarValue::from_displayable, + parse_token(i32, String), + specified_by_url = "https://docs.rs/ruint", +)] +pub type U128 = ruint::aliases::U128; + +/// Uint type using const generics. +/// +/// Always serializes as `String` in decimal notation. +/// May be deserialized from `i32` and `String` with +/// standard Rust syntax for decimal, hexadecimal, binary and octal +/// notation using prefixes 0x, 0b and 0o. +/// +/// Confusingly empty strings get parsed as 0 +/// https://github.com/recmo/uint/issues/348 +#[graphql_scalar] +#[graphql( + with = ruint_scalar, + to_output_with = ScalarValue::from_displayable, + parse_token(i32, String), + specified_by_url = "https://docs.rs/ruint", +)] +pub type U256 = ruint::aliases::U256; + +mod ruint_scalar { + use std::str::FromStr; + + use crate::{Scalar, ScalarValue}; + + pub(super) fn from_input( + v: &Scalar, + ) -> Result, Box> { + if let Some(int) = v.try_to_int() { + return ruint::Uint::try_from(int) + .map_err(|e| format!("Failt to parse `Uint<{B},{L}>`: {e}").into()); + } + + let Some(str) = v.try_as_str() else { + return Err( + format!("Failt to parse `Uint<{B},{L}>`: input is not `String` or `Int`").into(), + ); + }; + + ruint::Uint::from_str(str) + .map_err(|e| format!("Failt to parse `Uint<{B},{L}>`: {e}").into()) + } +} + +#[cfg(test)] +mod test { + use crate::{ + FromInputValue as _, InputValue, ToInputValue as _, graphql, + integrations::ruint::{U64, U128, U256}, + }; + + #[test] + fn parses_correct_input_256() { + for (input, expected) in [ + (graphql::input_value!(0), ruint::aliases::U256::ZERO), + (graphql::input_value!(123), ruint::aliases::U256::from(123)), + (graphql::input_value!("0"), ruint::aliases::U256::ZERO), + (graphql::input_value!("42"), ruint::aliases::U256::from(42)), + (graphql::input_value!("0o10"), ruint::aliases::U256::from(8)), + ( + graphql::input_value!("0xdeadbeef"), + ruint::aliases::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 parses_correct_input_128() { + for (input, expected) in [ + (graphql::input_value!(0), ruint::aliases::U128::ZERO), + (graphql::input_value!(123), ruint::aliases::U128::from(123)), + (graphql::input_value!("0"), ruint::aliases::U128::ZERO), + (graphql::input_value!("42"), ruint::aliases::U128::from(42)), + ( + graphql::input_value!("0xdeadbeef"), + ruint::aliases::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_64() { + for (input, expected) in [ + (graphql::input_value!(0), ruint::aliases::U64::ZERO), + (graphql::input_value!(123), ruint::aliases::U64::from(123)), + (graphql::input_value!("0"), ruint::aliases::U64::ZERO), + (graphql::input_value!("42"), ruint::aliases::U64::from(42)), + ( + graphql::input_value!("0xdeadbeef"), + ruint::aliases::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 fails_on_invalid_input() { + for input in [ + 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}"); + } + } +} From 3de41a145b6a3494223e424c5f916864472c6d00 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 19:37:20 +0300 Subject: [PATCH 02/11] Fix missing CI job for `ruint` Cargo feature --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) 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 } From 855ac6305f46bae4d4c288f7ca583a15e27ddb65 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 19:39:54 +0300 Subject: [PATCH 03/11] Mention in README --- juniper/README.md | 2 ++ 1 file changed, 2 insertions(+) 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 From 0ef9847c473e5b0755f300d602f0cf67580cf20d Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 19:40:12 +0300 Subject: [PATCH 04/11] Refactor integration --- juniper/Cargo.toml | 2 +- juniper/src/integrations/ruint.rs | 126 ++++++++++++++++++------------ 2 files changed, 77 insertions(+), 51 deletions(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index b43b48106..b5a7353c8 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -61,7 +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.17.0", features = ["serde"], optional = true } +ruint = { version = "1.17", 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/src/integrations/ruint.rs b/juniper/src/integrations/ruint.rs index 21ac8ebdf..ce413428a 100644 --- a/juniper/src/integrations/ruint.rs +++ b/juniper/src/integrations/ruint.rs @@ -12,7 +12,7 @@ //! [`U128`]: ruint::aliases::U128 //! [`U64`]: ruint::aliases::U64 -use crate::{ScalarValue, graphql_scalar}; +use crate::graphql_scalar; /// Uint type using const generics. /// @@ -25,9 +25,7 @@ use crate::{ScalarValue, graphql_scalar}; /// https://github.com/recmo/uint/issues/348 #[graphql_scalar] #[graphql( - with = ruint_scalar, - to_output_with = ScalarValue::from_displayable, - parse_token(i32, String), + with = uint_scalar, specified_by_url = "https://docs.rs/ruint", )] pub type U64 = ruint::aliases::U64; @@ -43,9 +41,7 @@ pub type U64 = ruint::aliases::U64; /// https://github.com/recmo/uint/issues/348 #[graphql_scalar] #[graphql( - with = ruint_scalar, - to_output_with = ScalarValue::from_displayable, - parse_token(i32, String), + with = uint_scalar, specified_by_url = "https://docs.rs/ruint", )] pub type U128 = ruint::aliases::U128; @@ -61,55 +57,88 @@ pub type U128 = ruint::aliases::U128; /// https://github.com/recmo/uint/issues/348 #[graphql_scalar] #[graphql( - with = ruint_scalar, - to_output_with = ScalarValue::from_displayable, - parse_token(i32, String), + with = uint_scalar, specified_by_url = "https://docs.rs/ruint", )] pub type U256 = ruint::aliases::U256; -mod ruint_scalar { - use std::str::FromStr; - - use crate::{Scalar, ScalarValue}; - - pub(super) fn from_input( - v: &Scalar, +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) = v.try_to_int() { - return ruint::Uint::try_from(int) - .map_err(|e| format!("Failt to parse `Uint<{B},{L}>`: {e}").into()); + 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(str) = v.try_as_str() else { - return Err( - format!("Failt to parse `Uint<{B},{L}>`: input is not `String` or `Int`").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()); }; + s.parse().map_err(|e| { + format!("Failed to parse `ruint::Uint<{B}, {L}>` from `String`: {e}").into() + }) + } - ruint::Uint::from_str(str) - .map_err(|e| format!("Failt to parse `Uint<{B},{L}>`: {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 crate::{ - FromInputValue as _, InputValue, ToInputValue as _, graphql, - integrations::ruint::{U64, U128, U256}, - }; + use super::{U64, U128, U256}; + use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql}; #[test] fn parses_correct_input_256() { for (input, expected) in [ - (graphql::input_value!(0), ruint::aliases::U256::ZERO), - (graphql::input_value!(123), ruint::aliases::U256::from(123)), - (graphql::input_value!("0"), ruint::aliases::U256::ZERO), - (graphql::input_value!("42"), ruint::aliases::U256::from(42)), - (graphql::input_value!("0o10"), ruint::aliases::U256::from(8)), + (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"), - ruint::aliases::U256::from(3735928559u64), + U256::from(3735928559u64), ), ] { let input: InputValue = input; @@ -120,7 +149,6 @@ mod test { "failed to parse `{input:?}`: {:?}", parsed.unwrap_err(), ); - assert_eq!(parsed.unwrap(), expected, "input: {input:?}"); } } @@ -128,13 +156,13 @@ mod test { #[test] fn parses_correct_input_128() { for (input, expected) in [ - (graphql::input_value!(0), ruint::aliases::U128::ZERO), - (graphql::input_value!(123), ruint::aliases::U128::from(123)), - (graphql::input_value!("0"), ruint::aliases::U128::ZERO), - (graphql::input_value!("42"), ruint::aliases::U128::from(42)), + (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"), - ruint::aliases::U128::from(3735928559u64), + U128::from(3735928559u64), ), ] { let input: InputValue = input; @@ -145,7 +173,6 @@ mod test { "failed to parse `{input:?}`: {:?}", parsed.unwrap_err(), ); - assert_eq!(parsed.unwrap(), expected, "input: {input:?}"); } } @@ -153,13 +180,13 @@ mod test { #[test] fn parses_correct_input_64() { for (input, expected) in [ - (graphql::input_value!(0), ruint::aliases::U64::ZERO), - (graphql::input_value!(123), ruint::aliases::U64::from(123)), - (graphql::input_value!("0"), ruint::aliases::U64::ZERO), - (graphql::input_value!("42"), ruint::aliases::U64::from(42)), + (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"), - ruint::aliases::U64::from(3735928559u64), + U64::from(3735928559u64), ), ] { let input: InputValue = input; @@ -170,7 +197,6 @@ mod test { "failed to parse `{input:?}`: {:?}", parsed.unwrap_err(), ); - assert_eq!(parsed.unwrap(), expected, "input: {input:?}"); } } @@ -192,7 +218,7 @@ mod test { assert!( parsed.is_err(), "allows input: {input:?} {}", - parsed.unwrap() + parsed.unwrap(), ); } } From 485e8c45ff8e6492f1e49b7917400a80ec5c3e83 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 23:18:14 +0300 Subject: [PATCH 05/11] Support defining custom sizes --- juniper/src/integrations/ruint.rs | 102 ++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/juniper/src/integrations/ruint.rs b/juniper/src/integrations/ruint.rs index ce413428a..c3acbc99f 100644 --- a/juniper/src/integrations/ruint.rs +++ b/juniper/src/integrations/ruint.rs @@ -8,58 +8,85 @@ //! | [`U128`] | `U128` | //! | [`U64`] | `U64` | //! +//! # Custom 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; -/// Uint type using const generics. +/// 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`. /// -/// Always serializes as `String` in decimal notation. -/// May be deserialized from `i32` and `String` with -/// standard Rust syntax for decimal, hexadecimal, binary and octal -/// notation using prefixes 0x, 0b and 0o. +/// See also [`ruint`] crate for details. /// -/// Confusingly empty strings get parsed as 0 -/// https://github.com/recmo/uint/issues/348 +/// [`ruint`]: https://docs.rs/ruint #[graphql_scalar] -#[graphql( - with = uint_scalar, - specified_by_url = "https://docs.rs/ruint", -)] +#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")] pub type U64 = ruint::aliases::U64; -/// Uint type using const generics. +/// 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`. /// -/// Always serializes as `String` in decimal notation. -/// May be deserialized from `i32` and `String` with -/// standard Rust syntax for decimal, hexadecimal, binary and octal -/// notation using prefixes 0x, 0b and 0o. +/// See also [`ruint`] crate for details. /// -/// Confusingly empty strings get parsed as 0 -/// https://github.com/recmo/uint/issues/348 +/// [`ruint`]: https://docs.rs/ruint #[graphql_scalar] -#[graphql( - with = uint_scalar, - specified_by_url = "https://docs.rs/ruint", -)] +#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")] pub type U128 = ruint::aliases::U128; -/// Uint type using const generics. +/// Unsigned integer type representing the ring of numbers modulo 2256. /// -/// Always serializes as `String` in decimal notation. -/// May be deserialized from `i32` and `String` with -/// standard Rust syntax for decimal, hexadecimal, binary and octal -/// notation using prefixes 0x, 0b and 0o. +/// 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`. /// -/// Confusingly empty strings get parsed as 0 -/// https://github.com/recmo/uint/issues/348 +/// See also [`ruint`] crate for details. +/// +/// [`ruint`]: https://docs.rs/ruint #[graphql_scalar] -#[graphql( - with = uint_scalar, - specified_by_url = "https://docs.rs/ruint", -)] +#[graphql(with = uint_scalar, specified_by_url = "https://docs.rs/ruint")] pub type U256 = ruint::aliases::U256; pub mod uint_scalar { @@ -94,6 +121,14 @@ pub mod uint_scalar { ) .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() }) @@ -204,6 +239,7 @@ mod test { #[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"), From 32bfc8aff7409eb9320f53b72dcc21930daa1e59 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 23:26:41 +0300 Subject: [PATCH 06/11] Support also `U8`, `U16` and `U32` --- book/src/types/scalars.md | 20 ++-- juniper/src/integrations/ruint.rs | 146 ++++++++++++++++++++++++++---- 2 files changed, 140 insertions(+), 26 deletions(-) diff --git a/book/src/types/scalars.md b/book/src/types/scalars.md index 349ba245e..4d81b0961 100644 --- a/book/src/types/scalars.md +++ b/book/src/types/scalars.md @@ -439,9 +439,12 @@ mod date_scalar { | [`chrono::NaiveDateTime`] | [`LocalDateTime`] | [`chrono`] | | [`chrono::DateTime`] | [`DateTime`] | [`chrono`] | | [`chrono_tz::Tz`] | [`TimeZone`] | [`chrono-tz`] | -| [`ruint::aliases::U256`] | [`U256`] | [`ruint`] | -| [`ruint::aliases::U128`] | [`U128`] | [`ruint`] | -| [`ruint::aliases::U64`] | [`U64`] | [`ruint`] | +| [`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`] | @@ -475,10 +478,6 @@ mod date_scalar { [`chrono::NaiveTime`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html [`chrono-tz`]: https://docs.rs/chrono-tz [`chrono_tz::Tz`]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html -[`ruint`]: https://docs.rs/ruint -[`ruint::aliases::U256`]: https://docs.rs/ruint/latest/ruint/aliases/type.U256.html -[`ruint::aliases::U128`]: https://docs.rs/ruint/latest/ruint/aliases/type.U128.html -[`ruint::aliases::U64`]: https://docs.rs/ruint/latest/ruint/aliases/type.U64.html [`DateTime`]: https://graphql-scalars.dev/docs/scalars/date-time [`Duration`]: https://graphql-scalars.dev/docs/scalars/duration [`ID`]: https://spec.graphql.org/October2021#sec-ID @@ -496,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/src/integrations/ruint.rs b/juniper/src/integrations/ruint.rs index c3acbc99f..3ded63824 100644 --- a/juniper/src/integrations/ruint.rs +++ b/juniper/src/integrations/ruint.rs @@ -4,11 +4,14 @@ //! //! | Rust type | GraphQL scalar | //! |----------------|----------------| -//! | [`U256`] | `U256` | -//! | [`U128`] | `U128` | +//! | [`U8`] | `U8` | +//! | [`U16`] | `U16` | +//! | [`U32`] | `U32` | //! | [`U64`] | `U64` | +//! | [`U128`] | `U128` | +//! | [`U256`] | `U256` | //! -//! # Custom type +//! # 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. @@ -50,6 +53,45 @@ 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 @@ -160,24 +202,89 @@ pub mod uint_scalar { #[cfg(test)] mod test { - use super::{U64, U128, U256}; + use super::{U8, U16, U32, U64, U128, U256}; use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql}; #[test] - fn parses_correct_input_256() { + fn parses_correct_input_8() { 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!(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"), - U256::from(3735928559u64), + U32::from(3735928559u32), ), ] { let input: InputValue = input; - let parsed = U256::from_input_value(&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(), @@ -213,19 +320,20 @@ mod test { } #[test] - fn parses_correct_input_64() { + fn parses_correct_input_256() { 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!(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"), - U64::from(3735928559u64), + U256::from(3735928559u64), ), ] { let input: InputValue = input; - let parsed = U64::from_input_value(&input); + let parsed = U256::from_input_value(&input); assert!( parsed.is_ok(), From 59512fa7abd80c53b9f044142e3e0da3befb2d88 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 23:27:51 +0300 Subject: [PATCH 07/11] Decrease minimal version --- juniper/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index b5a7353c8..2b530931d 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -61,7 +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.17", optional = true } +ruint = { version = "1.0", 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"] } From c047d7466183e354b426364cfd6f56a8a6310a8c Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 23:39:15 +0300 Subject: [PATCH 08/11] Mention in CHANGELOG --- juniper/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From e90a0bba49b1ddf1f4e84da979eda7731c2bb000 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 23:40:11 +0300 Subject: [PATCH 09/11] Bump minimal version to 1.1 --- juniper/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 2b530931d..3dfd6c5b8 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -61,7 +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.0", optional = true } +ruint = { version = "1.1", 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"] } From bce17fdf77937cd916ef05b254a39800b86da306 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 17 Oct 2025 23:49:55 +0300 Subject: [PATCH 10/11] Bump minimal version to 1.2 --- juniper/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 3dfd6c5b8..2de8c6e99 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -61,7 +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.1", optional = true } +ruint = { version = "1.2", 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"] } From 10ed1e18204f24d36323d9f42ca2ad51c104d197 Mon Sep 17 00:00:00 2001 From: tyranron Date: Sat, 18 Oct 2025 00:00:06 +0300 Subject: [PATCH 11/11] Bump minimal version to 1.10 --- juniper/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 2de8c6e99..4f8a30e5f 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -61,7 +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.2", optional = true } +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"] }