juniper/integrations/
bigdecimal.rs

1//! GraphQL support for [`bigdecimal`] crate types.
2//!
3//! # Supported types
4//!
5//! | Rust type      | GraphQL scalar |
6//! |----------------|----------------|
7//! | [`BigDecimal`] | `BigDecimal`   |
8//!
9//! [`BigDecimal`]: bigdecimal::BigDecimal
10
11use std::str::FromStr as _;
12
13use crate::{graphql_scalar, InputValue, ScalarValue, Value};
14
15/// Big decimal type.
16///
17/// Allows storing any real number to arbitrary precision; which avoids common
18/// floating point errors (such as 0.1 + 0.2 ≠ 0.3) at the cost of complexity.
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 [`bigdecimal`] crate for details.
25///
26/// [`bigdecimal`]: https://docs.rs/bigdecimal
27#[graphql_scalar(
28    with = bigdecimal_scalar,
29    parse_token(i32, f64, String),
30    specified_by_url = "https://docs.rs/bigdecimal",
31)]
32type BigDecimal = bigdecimal::BigDecimal;
33
34mod bigdecimal_scalar {
35    use super::*;
36
37    pub(super) fn to_output<S: ScalarValue>(v: &BigDecimal) -> Value<S> {
38        Value::scalar(v.to_string())
39    }
40
41    pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<BigDecimal, String> {
42        if let Some(i) = v.as_int_value() {
43            Ok(BigDecimal::from(i))
44        } else if let Some(f) = v.as_float_value() {
45            // See akubera/bigdecimal-rs#103 for details:
46            // https://github.com/akubera/bigdecimal-rs/issues/103
47            let mut buf = ryu::Buffer::new();
48            BigDecimal::from_str(buf.format(f))
49                .map_err(|e| format!("Failed to parse `BigDecimal` from `Float`: {e}"))
50        } else {
51            v.as_string_value()
52                .ok_or_else(|| format!("Expected `String`, found: {v}"))
53                .and_then(|s| {
54                    BigDecimal::from_str(s)
55                        .map_err(|e| format!("Failed to parse `BigDecimal` from `String`: {e}"))
56                })
57        }
58    }
59}
60
61#[cfg(test)]
62mod test {
63    use std::str::FromStr as _;
64
65    use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
66
67    use super::BigDecimal;
68
69    #[test]
70    fn parses_correct_input() {
71        for (input, expected) in [
72            (graphql_input_value!("4.20"), "4.20"),
73            (graphql_input_value!("0"), "0"),
74            (
75                graphql_input_value!("999999999999.999999999"),
76                "999999999999.999999999",
77            ),
78            (
79                graphql_input_value!("87553378877997984345"),
80                "87553378877997984345",
81            ),
82            (graphql_input_value!(123), "123"),
83            (graphql_input_value!(0), "0"),
84            (graphql_input_value!(43.44), "43.44"),
85        ] {
86            let input: InputValue = input;
87            let parsed = BigDecimal::from_input_value(&input);
88            let expected = BigDecimal::from_str(expected).unwrap();
89
90            assert!(
91                parsed.is_ok(),
92                "failed to parse `{input:?}`: {:?}",
93                parsed.unwrap_err(),
94            );
95            assert_eq!(parsed.unwrap(), expected, "input: {input:?}");
96        }
97    }
98
99    #[test]
100    fn fails_on_invalid_input() {
101        for input in [
102            graphql_input_value!(""),
103            graphql_input_value!("0,0"),
104            graphql_input_value!("12,"),
105            graphql_input_value!("1996-12-19T14:23:43"),
106            graphql_input_value!("i'm not even a number"),
107            graphql_input_value!(null),
108            graphql_input_value!(false),
109        ] {
110            let input: InputValue = input;
111            let parsed = BigDecimal::from_input_value(&input);
112
113            assert!(parsed.is_err(), "allows input: {input:?}");
114        }
115    }
116
117    #[test]
118    fn formats_correctly() {
119        for raw in [
120            "4.20",
121            "0",
122            "999999999999.999999999",
123            "87553378877997984345",
124            "123",
125            "43.44",
126        ] {
127            let actual: InputValue = BigDecimal::from_str(raw).unwrap().to_input_value();
128
129            assert_eq!(actual, graphql_input_value!((raw)), "on value: {raw}");
130        }
131    }
132}