juniper/integrations/
rust_decimal.rs

1//! GraphQL support for [`rust_decimal`] crate types.
2//!
3//! # Supported types
4//!
5//! | Rust type   | GraphQL scalar |
6//! |-------------|----------------|
7//! | [`Decimal`] | `Decimal`      |
8//!
9//! [`Decimal`]: rust_decimal::Decimal
10
11use crate::{ScalarValue, graphql_scalar};
12
13/// 128 bit representation of a fixed-precision decimal number.
14///
15/// The finite set of values of `Decimal` scalar are of the form
16/// m / 10<sup>e</sup>, where m is an integer such that
17/// -2<sup>96</sup> < m < 2<sup>96</sup>, and e is an integer between 0 and 28
18/// inclusive.
19///
20/// Always serializes as `String`. But may be deserialized from `Int` and
21/// `Float` values too. It's not recommended to deserialize from a `Float`
22/// directly, as the floating point representation may be unexpected.
23///
24/// See also [`rust_decimal`] crate for details.
25///
26/// [`rust_decimal`]: https://docs.rs/rust_decimal
27#[graphql_scalar]
28#[graphql(
29    with = rust_decimal_scalar,
30    to_output_with = ScalarValue::from_displayable,
31    parse_token(i32, f64, String),
32    specified_by_url = "https://docs.rs/rust_decimal",
33)]
34type Decimal = rust_decimal::Decimal;
35
36mod rust_decimal_scalar {
37    use super::Decimal;
38    use crate::{Scalar, ScalarValue};
39
40    pub(super) fn from_input(v: &Scalar<impl ScalarValue>) -> Result<Decimal, Box<str>> {
41        if let Some(i) = v.try_to_int() {
42            Ok(Decimal::from(i))
43        } else if let Some(f) = v.try_to_float() {
44            Decimal::try_from(f)
45                .map_err(|e| format!("Failed to parse `Decimal` from `Float`: {e}").into())
46        } else {
47            v.try_to::<&str>()
48                .map_err(|e| e.to_string().into())
49                .and_then(|s| {
50                    s.parse::<Decimal>()
51                        .map_err(|e| format!("Failed to parse `Decimal` from `String`: {e}").into())
52                })
53        }
54    }
55}
56
57#[cfg(test)]
58mod test {
59    use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql_input_value};
60
61    use super::Decimal;
62
63    #[test]
64    fn parses_correct_input() {
65        for (input, expected) in [
66            (graphql_input_value!("4.20"), "4.20"),
67            (graphql_input_value!("0"), "0"),
68            (graphql_input_value!("999.999999999"), "999.999999999"),
69            (graphql_input_value!("875533788"), "875533788"),
70            (graphql_input_value!(123), "123"),
71            (graphql_input_value!(0), "0"),
72            (graphql_input_value!(43.44), "43.44"),
73        ] {
74            let input: InputValue = input;
75            let parsed = Decimal::from_input_value(&input);
76            let expected = expected.parse::<Decimal>().unwrap();
77
78            assert!(
79                parsed.is_ok(),
80                "failed to parse `{input:?}`: {:?}",
81                parsed.unwrap_err(),
82            );
83            assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
84        }
85    }
86
87    #[test]
88    fn fails_on_invalid_input() {
89        for input in [
90            graphql_input_value!(""),
91            graphql_input_value!("0,0"),
92            graphql_input_value!("12,"),
93            graphql_input_value!("1996-12-19T14:23:43"),
94            graphql_input_value!("99999999999999999999999999999999999999"),
95            graphql_input_value!("99999999999999999999999999999999999999.99"),
96            graphql_input_value!("i'm not even a number"),
97            graphql_input_value!(null),
98            graphql_input_value!(false),
99        ] {
100            let input: InputValue = input;
101            let parsed = Decimal::from_input_value(&input);
102
103            assert!(parsed.is_err(), "allows input: {input:?}");
104        }
105    }
106
107    #[test]
108    fn formats_correctly() {
109        for raw in ["4.20", "0", "999.999999999", "875533788", "123", "43.44"] {
110            let actual: InputValue = raw.parse::<Decimal>().unwrap().to_input_value();
111
112            assert_eq!(actual, graphql_input_value!((raw)), "on value: {raw}");
113        }
114    }
115}