Skip to content

Commit c79730b

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

File tree

3 files changed

+206
-0
lines changed

3 files changed

+206
-0
lines changed

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

0 commit comments

Comments
 (0)