Merge branch 'master' into rework-core-traits
This commit is contained in:
commit
72cd4be715
7 changed files with 270 additions and 0 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
132
juniper/src/integrations/bigdecimal.rs
Normal file
132
juniper/src/integrations/bigdecimal.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
123
juniper/src/integrations/rust_decimal.rs
Normal file
123
juniper/src/integrations/rust_decimal.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue