Skip to content

Commit 70efe2d

Browse files
committed
feat(integrations): add ruint
1 parent 7725a27 commit 70efe2d

File tree

4 files changed

+224
-0
lines changed

4 files changed

+224
-0
lines changed

book/src/types/scalars.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,9 @@ mod date_scalar {
439439
| [`chrono::NaiveDateTime`] | [`LocalDateTime`] | [`chrono`] |
440440
| [`chrono::DateTime`] | [`DateTime`] | [`chrono`] |
441441
| [`chrono_tz::Tz`] | [`TimeZone`] | [`chrono-tz`] |
442+
| [`ruint::aliases::U256`] | [`U256`] | [`ruint`] |
443+
| [`ruint::aliases::U128`] | [`U128`] | [`ruint`] |
444+
| [`ruint::aliases::U64`] | [`U64`] | [`ruint`] |
442445
| [`rust_decimal::Decimal`] | `Decimal` | [`rust_decimal`] |
443446
| [`jiff::civil::Date`] | [`LocalDate`] | [`jiff`] |
444447
| [`jiff::civil::Time`] | [`LocalTime`] | [`jiff`] |
@@ -472,6 +475,10 @@ mod date_scalar {
472475
[`chrono::NaiveTime`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html
473476
[`chrono-tz`]: https://docs.rs/chrono-tz
474477
[`chrono_tz::Tz`]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
478+
[`ruint`]: https://docs.rs/ruint
479+
[`ruint::aliases::U256`]: https://docs.rs/ruint/latest/ruint/aliases/type.U256.html
480+
[`ruint::aliases::U128`]: https://docs.rs/ruint/latest/ruint/aliases/type.U128.html
481+
[`ruint::aliases::U64`]: https://docs.rs/ruint/latest/ruint/aliases/type.U64.html
475482
[`DateTime`]: https://graphql-scalars.dev/docs/scalars/date-time
476483
[`Duration`]: https://graphql-scalars.dev/docs/scalars/duration
477484
[`ID`]: https://spec.graphql.org/October2021#sec-ID

juniper/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ chrono-tz = ["dep:chrono-tz", "dep:regex"]
3535
expose-test-schema = ["dep:anyhow", "dep:serde_json"]
3636
jiff = ["dep:jiff"]
3737
js = ["chrono?/wasmbind", "time?/wasm-bindgen", "uuid?/js"]
38+
ruint = ["dep:ruint"]
3839
rust_decimal = ["dep:rust_decimal"]
3940
schema-language = ["dep:graphql-parser", "dep:void"]
4041
time = ["dep:time"]
@@ -60,6 +61,7 @@ itertools = "0.14"
6061
jiff = { version = "0.2", features = ["std"], default-features = false, optional = true }
6162
juniper_codegen = { version = "0.17.0", path = "../juniper_codegen" }
6263
ref-cast = "1.0"
64+
ruint = { version = "1.17.0", features = ["serde"], optional = true }
6365
rust_decimal = { version = "1.20", default-features = false, optional = true }
6466
ryu = { version = "1.0", optional = true }
6567
serde = { version = "1.0.122", features = ["derive"] }

juniper/src/integrations/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub mod chrono;
1212
pub mod chrono_tz;
1313
#[cfg(feature = "jiff")]
1414
pub mod jiff;
15+
#[cfg(feature = "ruint")]
16+
pub mod ruint;
1517
#[cfg(feature = "rust_decimal")]
1618
pub mod rust_decimal;
1719
#[doc(hidden)]

juniper/src/integrations/ruint.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//! GraphQL support for [`ruint`] crate types.
2+
//!
3+
//! # Supported types
4+
//!
5+
//! | Rust type | GraphQL scalar |
6+
//! |----------------|----------------|
7+
//! | [`U256`] | `U256` |
8+
//! | [`U128`] | `U128` |
9+
//! | [`U64`] | `U64` |
10+
//!
11+
//! [`U256`]: ruint::aliases::U256
12+
//! [`U128`]: ruint::aliases::U128
13+
//! [`U64`]: ruint::aliases::U64
14+
15+
use crate::{ScalarValue, graphql_scalar};
16+
17+
/// Uint type using const generics.
18+
///
19+
/// Always serializes as `String` in decimal notation.
20+
/// May be deserialized from `i32` and `String` with
21+
/// standard Rust syntax for decimal, hexadecimal, binary and octal
22+
/// notation using prefixes 0x, 0b and 0o.
23+
///
24+
/// Confusingly empty strings get parsed as 0
25+
/// https://github.com/recmo/uint/issues/348
26+
#[graphql_scalar]
27+
#[graphql(
28+
with = ruint_scalar,
29+
to_output_with = ScalarValue::from_displayable,
30+
parse_token(i32, String),
31+
specified_by_url = "https://docs.rs/ruint",
32+
)]
33+
pub type U64 = ruint::aliases::U64;
34+
35+
/// Uint type using const generics.
36+
///
37+
/// Always serializes as `String` in decimal notation.
38+
/// May be deserialized from `i32` and `String` with
39+
/// standard Rust syntax for decimal, hexadecimal, binary and octal
40+
/// notation using prefixes 0x, 0b and 0o.
41+
///
42+
/// Confusingly empty strings get parsed as 0
43+
/// https://github.com/recmo/uint/issues/348
44+
#[graphql_scalar]
45+
#[graphql(
46+
with = ruint_scalar,
47+
to_output_with = ScalarValue::from_displayable,
48+
parse_token(i32, String),
49+
specified_by_url = "https://docs.rs/ruint",
50+
)]
51+
pub type U128 = ruint::aliases::U128;
52+
53+
/// Uint type using const generics.
54+
///
55+
/// Always serializes as `String` in decimal notation.
56+
/// May be deserialized from `i32` and `String` with
57+
/// standard Rust syntax for decimal, hexadecimal, binary and octal
58+
/// notation using prefixes 0x, 0b and 0o.
59+
///
60+
/// Confusingly empty strings get parsed as 0
61+
/// https://github.com/recmo/uint/issues/348
62+
#[graphql_scalar]
63+
#[graphql(
64+
with = ruint_scalar,
65+
to_output_with = ScalarValue::from_displayable,
66+
parse_token(i32, String),
67+
specified_by_url = "https://docs.rs/ruint",
68+
)]
69+
pub type U256 = ruint::aliases::U256;
70+
71+
mod ruint_scalar {
72+
use std::str::FromStr;
73+
74+
use crate::{Scalar, ScalarValue};
75+
76+
pub(super) fn from_input<const B: usize, const L: usize>(
77+
v: &Scalar<impl ScalarValue>,
78+
) -> Result<ruint::Uint<B, L>, Box<str>> {
79+
if let Some(int) = v.try_to_int() {
80+
return ruint::Uint::try_from(int)
81+
.map_err(|e| format!("Failt to parse `Uint<{B},{L}>`: {e}").into());
82+
}
83+
84+
let Some(str) = v.try_as_str() else {
85+
return Err(
86+
format!("Failt to parse `Uint<{B},{L}>`: input is not `String` or `Int`").into(),
87+
);
88+
};
89+
90+
ruint::Uint::from_str(str)
91+
.map_err(|e| format!("Failt to parse `Uint<{B},{L}>`: {e}").into())
92+
}
93+
}
94+
95+
#[cfg(test)]
96+
mod test {
97+
use crate::{
98+
FromInputValue as _, InputValue, ToInputValue as _, graphql,
99+
integrations::ruint::{U64, U128, U256},
100+
};
101+
102+
#[test]
103+
fn parses_correct_input_256() {
104+
for (input, expected) in [
105+
(graphql::input_value!(0), ruint::aliases::U256::ZERO),
106+
(graphql::input_value!(123), ruint::aliases::U256::from(123)),
107+
(graphql::input_value!("0"), ruint::aliases::U256::ZERO),
108+
(graphql::input_value!("42"), ruint::aliases::U256::from(42)),
109+
(graphql::input_value!("0o10"), ruint::aliases::U256::from(8)),
110+
(
111+
graphql::input_value!("0xdeadbeef"),
112+
ruint::aliases::U256::from(3735928559u64),
113+
),
114+
] {
115+
let input: InputValue = input;
116+
let parsed = U256::from_input_value(&input);
117+
118+
assert!(
119+
parsed.is_ok(),
120+
"failed to parse `{input:?}`: {:?}",
121+
parsed.unwrap_err(),
122+
);
123+
124+
assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
125+
}
126+
}
127+
128+
#[test]
129+
fn parses_correct_input_128() {
130+
for (input, expected) in [
131+
(graphql::input_value!(0), ruint::aliases::U128::ZERO),
132+
(graphql::input_value!(123), ruint::aliases::U128::from(123)),
133+
(graphql::input_value!("0"), ruint::aliases::U128::ZERO),
134+
(graphql::input_value!("42"), ruint::aliases::U128::from(42)),
135+
(
136+
graphql::input_value!("0xdeadbeef"),
137+
ruint::aliases::U128::from(3735928559u64),
138+
),
139+
] {
140+
let input: InputValue = input;
141+
let parsed = U128::from_input_value(&input);
142+
143+
assert!(
144+
parsed.is_ok(),
145+
"failed to parse `{input:?}`: {:?}",
146+
parsed.unwrap_err(),
147+
);
148+
149+
assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
150+
}
151+
}
152+
153+
#[test]
154+
fn parses_correct_input_64() {
155+
for (input, expected) in [
156+
(graphql::input_value!(0), ruint::aliases::U64::ZERO),
157+
(graphql::input_value!(123), ruint::aliases::U64::from(123)),
158+
(graphql::input_value!("0"), ruint::aliases::U64::ZERO),
159+
(graphql::input_value!("42"), ruint::aliases::U64::from(42)),
160+
(
161+
graphql::input_value!("0xdeadbeef"),
162+
ruint::aliases::U64::from(3735928559u64),
163+
),
164+
] {
165+
let input: InputValue = input;
166+
let parsed = U64::from_input_value(&input);
167+
168+
assert!(
169+
parsed.is_ok(),
170+
"failed to parse `{input:?}`: {:?}",
171+
parsed.unwrap_err(),
172+
);
173+
174+
assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
175+
}
176+
}
177+
178+
#[test]
179+
fn fails_on_invalid_input() {
180+
for input in [
181+
graphql::input_value!("0,0"),
182+
graphql::input_value!("12,"),
183+
graphql::input_value!("1996-12-19T14:23:43"),
184+
graphql::input_value!("i'm not even a number"),
185+
graphql::input_value!(null),
186+
graphql::input_value!(false),
187+
graphql::input_value!(-123),
188+
] {
189+
let input: InputValue = input;
190+
let parsed = U256::from_input_value(&input);
191+
192+
assert!(
193+
parsed.is_err(),
194+
"allows input: {input:?} {}",
195+
parsed.unwrap()
196+
);
197+
}
198+
}
199+
200+
#[test]
201+
fn formats_correctly() {
202+
for (raw, expected) in [
203+
("0", "0"),
204+
("87553378877997984345", "87553378877997984345"),
205+
("123", "123"),
206+
("0x42", "66"),
207+
] {
208+
let actual: InputValue = raw.parse::<U256>().unwrap().to_input_value();
209+
210+
assert_eq!(actual, graphql::input_value!((expected)), "on value: {raw}");
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)