From bdaee42156843d45e01271ad45732e4e58a5b62c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wojciech=20Nied=C5=BAwied=C5=BA?= <bloodywojtas@gmail.com>
Date: Thu, 5 May 2022 20:00:58 +0200
Subject: [PATCH] Provide integration for `bigdecimal` and `rust_decimal`
 crates (#1060, #1059)

Co-authored-by: Kai Ren <tyranron@gmail.com>
---
 .github/workflows/ci.yml                 |   2 +
 juniper/CHANGELOG.md                     |   3 +
 juniper/Cargo.toml                       |   2 +
 juniper/README.md                        |   4 +
 juniper/src/integrations/bigdecimal.rs   | 132 +++++++++++++++++++++++
 juniper/src/integrations/mod.rs          |   4 +
 juniper/src/integrations/rust_decimal.rs | 123 +++++++++++++++++++++
 7 files changed, 270 insertions(+)
 create mode 100644 juniper/src/integrations/bigdecimal.rs
 create mode 100644 juniper/src/integrations/rust_decimal.rs

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d6768559..313a4f4f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -97,12 +97,14 @@ jobs:
       matrix:
         include:
           - { feature: <none>, crate: juniper }
+          - { feature: bigdecimal, crate: juniper }
           - { feature: bson, crate: juniper }
           - { feature: chrono, crate: juniper }
           - { feature: chrono-clock, crate: juniper }
           - { feature: chrono-tz, crate: juniper }
           - { feature: expose-test-schema, crate: juniper }
           - { feature: graphql-parser, crate: juniper }
+          - { feature: rust_decimal, crate: juniper }
           - { feature: schema-language, crate: juniper }
           - { feature: serde_json, crate: juniper }
           - { feature: time, crate: juniper }
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 92991bae..1bff5bca 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -56,6 +56,8 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 - `graphql_input_value!` and `graphql_vars!` macros. ([#996])
 - [`time` crate] integration behind `time` [Cargo feature]. ([#1006])
 - `#[derive(GraphQLInterface)]` macro allowing using structs as GraphQL interfaces. ([#1026])
+- [`bigdecimal` crate] integration behind `bigdecimal` [Cargo feature]. ([#1060])
+- [`rust_decimal` crate] integration behind `rust_decimal` [Cargo feature]. ([#1060])
 
 ### Changed
 
@@ -95,6 +97,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 [#1051]: /../../issues/1051
 [#1054]: /../../pull/1054
 [#1057]: /../../pull/1057
+[#1060]: /../../pull/1060
 [ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
 
 
diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml
index f5f22c90..ea089c5f 100644
--- a/juniper/Cargo.toml
+++ b/juniper/Cargo.toml
@@ -37,6 +37,7 @@ schema-language = ["graphql-parser"]
 [dependencies]
 anyhow = { version = "1.0.32", default-features = false, optional = true }
 async-trait = "0.1.39"
+bigdecimal = { version = "0.3", optional = true }
 bson = { version = "2.0", features = ["chrono-0_4"], optional = true }
 chrono = { version = "0.4", features = ["alloc"], default-features = false, optional = true }
 chrono-tz = { version = "0.6", default-features = false, optional = true }
@@ -46,6 +47,7 @@ futures-enum = { version = "0.1.12", default-features = false }
 graphql-parser = { version = "0.4", optional = true }
 indexmap = { version = "1.0", features = ["serde-1"] }
 juniper_codegen = { version = "0.16.0-dev", path = "../juniper_codegen" }
+rust_decimal = { version = "1.0", default-features = false, optional = true }
 serde = { version = "1.0.8", features = ["derive"], default-features = false }
 serde_json = { version = "1.0.2", default-features = false, optional = true }
 smartstring = "1.0"
diff --git a/juniper/README.md b/juniper/README.md
index 03b1ed8c..feb17329 100644
--- a/juniper/README.md
+++ b/juniper/README.md
@@ -44,9 +44,11 @@ As an exception to other [GraphQL] libraries for other languages, [Juniper] buil
 ### Data types
 
 [Juniper] has automatic integration with some very common [Rust] crates to make building schemas a breeze. The types from these crates will be usable in your schemas automatically:
+- [`bigdecimal`] (feature gated)
 - [`bson`]
 - [`chrono`] (feature gated)
 - [`chrono-tz`] (feature gated)
+- [`rust_decimal`] (feature gated)
 - [`time`] (feature gated)
 - [`url`]
 - [`uuid`]
@@ -78,6 +80,7 @@ This project is licensed under [BSD 2-Clause License](https://github.com/graphql
 
 
 [`actix-web`]: https://docs.rs/actix-web
+[`bigdecimal`]: https://docs.rs/bigdecimal
 [`bson`]: https://docs.rs/bson
 [`chrono`]: https://docs.rs/chrono
 [`chrono-tz`]: https://docs.rs/chrono-tz
@@ -89,6 +92,7 @@ This project is licensed under [BSD 2-Clause License](https://github.com/graphql
 [`hyper`]: https://docs.rs/hyper
 [`iron`]: https://docs.rs/iron
 [`rocket`]: https://docs.rs/rocket
+[`rust_decimal`]: https://docs.rs/rust_decimal
 [`time`]: https://docs.rs/time
 [`url`]: https://docs.rs/url
 [`uuid`]: https://docs.rs/uuid
diff --git a/juniper/src/integrations/bigdecimal.rs b/juniper/src/integrations/bigdecimal.rs
new file mode 100644
index 00000000..a1635fcc
--- /dev/null
+++ b/juniper/src/integrations/bigdecimal.rs
@@ -0,0 +1,132 @@
+//! GraphQL support for [`bigdecimal`] crate types.
+//!
+//! # Supported types
+//!
+//! | Rust type      | GraphQL scalar |
+//! |----------------|----------------|
+//! | [`BigDecimal`] | `BigDecimal`   |
+//!
+//! [`BigDecimal`]: bigdecimal::BigDecimal
+
+use std::str::FromStr as _;
+
+use crate::{graphql_scalar, InputValue, ScalarValue, Value};
+
+/// Big decimal type.
+///
+/// Allows storing any real number to arbitrary precision; which avoids common
+/// floating point errors (such as 0.1 + 0.2 ≠ 0.3) at the cost of complexity.
+///
+/// Always serializes as `String`. But may be deserialized from `Int` and
+/// `Float` values too. It's not recommended to deserialize from a `Float`
+/// directly, as the floating point representation may be unexpected.
+///
+/// See also [`bigdecimal`] crate for details.
+///
+/// [`bigdecimal`]: https://docs.rs/bigdecimal
+#[graphql_scalar(
+    with = bigdecimal_scalar,
+    parse_token(i32, f64, String),
+    specified_by_url = "https://docs.rs/bigdecimal",
+)]
+type BigDecimal = bigdecimal::BigDecimal;
+
+mod bigdecimal_scalar {
+    use std::convert::TryFrom as _;
+
+    use super::*;
+
+    pub(super) fn to_output<S: ScalarValue>(v: &BigDecimal) -> Value<S> {
+        Value::scalar(v.to_string())
+    }
+
+    pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<BigDecimal, String> {
+        if let Some(i) = v.as_int_value() {
+            Ok(BigDecimal::from(i))
+        } else if let Some(f) = v.as_float_value() {
+            BigDecimal::try_from(f)
+                .map_err(|e| format!("Failed to parse `BigDecimal` from `Float`: {}", e))
+        } else {
+            v.as_string_value()
+                .ok_or_else(|| format!("Expected `String`, found: {}", v))
+                .and_then(|s| {
+                    BigDecimal::from_str(s)
+                        .map_err(|e| format!("Failed to parse `BigDecimal` from `String`: {}", e))
+                })
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::str::FromStr as _;
+
+    use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
+
+    use super::BigDecimal;
+
+    #[test]
+    fn parses_correct_input() {
+        for (input, expected) in [
+            (graphql_input_value!("4.20"), "4.20"),
+            (graphql_input_value!("0"), "0"),
+            (
+                graphql_input_value!("999999999999.999999999"),
+                "999999999999.999999999",
+            ),
+            (
+                graphql_input_value!("87553378877997984345"),
+                "87553378877997984345",
+            ),
+            (graphql_input_value!(123), "123"),
+            (graphql_input_value!(0), "0"),
+            (graphql_input_value!(43.44), "43.44"),
+        ] {
+            let input: InputValue = input;
+            let parsed = BigDecimal::from_input_value(&input);
+            let expected = BigDecimal::from_str(expected).unwrap();
+
+            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!(""),
+            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),
+        ] {
+            let input: InputValue = input;
+            let parsed = BigDecimal::from_input_value(&input);
+
+            assert!(parsed.is_err(), "allows input: {:?}", input);
+        }
+    }
+
+    #[test]
+    fn formats_correctly() {
+        for raw in [
+            "4.20",
+            "0",
+            "999999999999.999999999",
+            "87553378877997984345",
+            "123",
+            "43.44",
+        ] {
+            let actual: InputValue = BigDecimal::from_str(raw).unwrap().to_input_value();
+
+            assert_eq!(actual, graphql_input_value!((raw)), "on value: {}", raw);
+        }
+    }
+}
diff --git a/juniper/src/integrations/mod.rs b/juniper/src/integrations/mod.rs
index d966997f..05f988cb 100644
--- a/juniper/src/integrations/mod.rs
+++ b/juniper/src/integrations/mod.rs
@@ -1,11 +1,15 @@
 //! Provides GraphQLType implementations for some external types
 
+#[cfg(feature = "bigdecimal")]
+pub mod bigdecimal;
 #[cfg(feature = "bson")]
 pub mod bson;
 #[cfg(feature = "chrono")]
 pub mod chrono;
 #[cfg(feature = "chrono-tz")]
 pub mod chrono_tz;
+#[cfg(feature = "rust_decimal")]
+pub mod rust_decimal;
 #[doc(hidden)]
 pub mod serde;
 #[cfg(feature = "time")]
diff --git a/juniper/src/integrations/rust_decimal.rs b/juniper/src/integrations/rust_decimal.rs
new file mode 100644
index 00000000..2e74db32
--- /dev/null
+++ b/juniper/src/integrations/rust_decimal.rs
@@ -0,0 +1,123 @@
+//! GraphQL support for [`rust_decimal`] crate types.
+//!
+//! # Supported types
+//!
+//! | Rust type   | GraphQL scalar |
+//! |-------------|----------------|
+//! | [`Decimal`] | `Decimal`      |
+//!
+//! [`Decimal`]: rust_decimal::Decimal
+
+use std::str::FromStr as _;
+
+use crate::{graphql_scalar, InputValue, ScalarValue, Value};
+
+/// 128 bit representation of a fixed-precision decimal number.
+///
+/// The finite set of values of `Decimal` scalar are of the form
+/// m / 10<sup>e</sup>, where m is an integer such that
+/// -2<sup>96</sup> < m < 2<sup>96</sup>, and e is an integer between 0 and 28
+/// inclusive.
+///
+/// Always serializes as `String`. But may be deserialized from `Int` and
+/// `Float` values too. It's not recommended to deserialize from a `Float`
+/// directly, as the floating point representation may be unexpected.
+///
+/// See also [`rust_decimal`] crate for details.
+///
+/// [`rust_decimal`]: https://docs.rs/rust_decimal
+#[graphql_scalar(
+    with = rust_decimal_scalar,
+    parse_token(i32, f64, String),
+    specified_by_url = "https://docs.rs/rust_decimal",
+)]
+type Decimal = rust_decimal::Decimal;
+
+mod rust_decimal_scalar {
+    use std::convert::TryFrom as _;
+
+    use super::*;
+
+    pub(super) fn to_output<S: ScalarValue>(v: &Decimal) -> Value<S> {
+        Value::scalar(v.to_string())
+    }
+
+    pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Decimal, String> {
+        if let Some(i) = v.as_int_value() {
+            Ok(Decimal::from(i))
+        } else if let Some(f) = v.as_float_value() {
+            Decimal::try_from(f)
+                .map_err(|e| format!("Failed to parse `Decimal` from `Float`: {}", e))
+        } else {
+            v.as_string_value()
+                .ok_or_else(|| format!("Expected `String`, found: {}", v))
+                .and_then(|s| {
+                    Decimal::from_str(s)
+                        .map_err(|e| format!("Failed to parse `Decimal` from `String`: {}", e))
+                })
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::str::FromStr as _;
+
+    use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
+
+    use super::Decimal;
+
+    #[test]
+    fn parses_correct_input() {
+        for (input, expected) in [
+            (graphql_input_value!("4.20"), "4.20"),
+            (graphql_input_value!("0"), "0"),
+            (graphql_input_value!("999.999999999"), "999.999999999"),
+            (graphql_input_value!("875533788"), "875533788"),
+            (graphql_input_value!(123), "123"),
+            (graphql_input_value!(0), "0"),
+            (graphql_input_value!(43.44), "43.44"),
+        ] {
+            let input: InputValue = input;
+            let parsed = Decimal::from_input_value(&input);
+            let expected = Decimal::from_str(expected).unwrap();
+
+            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!(""),
+            graphql_input_value!("0,0"),
+            graphql_input_value!("12,"),
+            graphql_input_value!("1996-12-19T14:23:43"),
+            graphql_input_value!("99999999999999999999999999999999999999"),
+            graphql_input_value!("99999999999999999999999999999999999999.99"),
+            graphql_input_value!("i'm not even a number"),
+            graphql_input_value!(null),
+            graphql_input_value!(false),
+        ] {
+            let input: InputValue = input;
+            let parsed = Decimal::from_input_value(&input);
+
+            assert!(parsed.is_err(), "allows input: {:?}", input);
+        }
+    }
+
+    #[test]
+    fn formats_correctly() {
+        for raw in ["4.20", "0", "999.999999999", "875533788", "123", "43.44"] {
+            let actual: InputValue = Decimal::from_str(raw).unwrap().to_input_value();
+
+            assert_eq!(actual, graphql_input_value!((raw)), "on value: {}", raw);
+        }
+    }
+}