Skip to content

Commit

Permalink
bson compliance
Browse files Browse the repository at this point in the history
  • Loading branch information
tyranron committed Aug 15, 2024
1 parent bd8fa6c commit 34ae0a5
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 36 deletions.
2 changes: 1 addition & 1 deletion book/src/types/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ mod date_scalar {
|-----------------------------|-------------------|------------------|
| [`bigdecimal::BigDecimal`] | `BigDecimal` | [`bigdecimal`] |
| [`bson::oid::ObjectId`] | `ObjectId` | [`bson`] |
| [`bson::DateTime`] | `UtcDateTime` | [`bson`] |
| [`bson::DateTime`] | [`DateTime`] | [`bson`] |
| [`chrono::NaiveDate`] | [`LocalDate`] | [`chrono`] |
| [`chrono::NaiveTime`] | [`LocalTime`] | [`chrono`] |
| [`chrono::NaiveDateTime`] | [`LocalDateTime`] | [`chrono`] |
Expand Down
2 changes: 2 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Switched from `Date` scalar to `LocalDate` scalar in types:
- `chrono::NaiveDate`.
- `time::Date`.
- Switched from `UtcDateTime` scalar to `DateTime` scalar in types:
- `bson::DateTime`.
- Corrected `TimeZone` scalar in types:
- `chrono_tz::Tz`.
- Renamed `Url` scalar to `URL` in types:
Expand Down
2 changes: 1 addition & 1 deletion juniper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ anyhow = { version = "1.0.47", optional = true }
async-trait = "0.1.39"
auto_enums = "0.8"
bigdecimal = { version = "0.4", optional = true }
bson = { version = "2.4", features = ["chrono-0_4"], optional = true }
bson = { version = "2.4", optional = true }
chrono = { version = "0.4.30", features = ["alloc"], default-features = false, optional = true }
chrono-tz = { version = "0.9", default-features = false, optional = true }
fnv = "1.0.5"
Expand Down
205 changes: 181 additions & 24 deletions juniper/src/integrations/bson.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
//! GraphQL support for [bson](https://github.com/mongodb/bson-rust) types.
//! GraphQL support for [`bson`] crate types.
//!
//! # Supported types
//!
//! | Rust type | Format | GraphQL scalar |
//! |-------------------|-------------------|------------------|
//! | [`oid::ObjectId`] | HEX string | `ObjectId` |
//! | [`DateTime`] | [RFC 3339] string | [`DateTime`][s4] |
//!
//! [`DateTime`]: bson::DateTime
//! [`ObjectId`]: bson::oid::ObjectId
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
use crate::{graphql_scalar, InputValue, ScalarValue, Value};

#[graphql_scalar(with = object_id, parse_token(String))]
/// [BSON ObjectId][0] represented as a HEX string.
///
/// See also [`bson::oid::ObjectId`][2] for details.
///
/// [0]: https://www.mongodb.com/docs/manual/reference/bson-types#objectid
/// [2]: https://docs.rs/bson/*/bson/oid/struct.ObjectId.html
#[graphql_scalar(
with = object_id,
parse_token(String),
specified_by_url = "https://www.mongodb.com/docs/manual/reference/bson-types#objectid",
)]
type ObjectId = bson::oid::ObjectId;

mod object_id {
Expand All @@ -21,32 +43,49 @@ mod object_id {
}
}

#[graphql_scalar(with = utc_date_time, parse_token(String))]
type UtcDateTime = bson::DateTime;

mod utc_date_time {
/// [BSON date][3] in [RFC 3339][0] format.
///
/// [BSON datetimes][3] have millisecond precision and are always in UTC (inputs with other
/// timezones are coerced).
///
/// [`DateTime` scalar][1] compliant.
///
/// See also [`bson::DateTime`][2] for details.
///
/// [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
/// [1]: https://graphql-scalars.dev/docs/scalars/date-time
/// [2]: https://docs.rs/bson/*/bson/struct.DateTime.html
/// [3]: https://www.mongodb.com/docs/manual/reference/bson-types#date
#[graphql_scalar(
with = date_time,
parse_token(String),
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date-time",
)]
type DateTime = bson::DateTime;

mod date_time {
use super::*;

pub(super) fn to_output<S: ScalarValue>(v: &UtcDateTime) -> Value<S> {
pub(super) fn to_output<S: ScalarValue>(v: &DateTime) -> Value<S> {
Value::scalar(
(*v).try_to_rfc3339_string()
.unwrap_or_else(|e| panic!("failed to format `UtcDateTime` as RFC3339: {e}")),
.unwrap_or_else(|e| panic!("failed to format `DateTime` as RFC 3339: {e}")),
)
}

pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<UtcDateTime, String> {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<DateTime, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {v}"))
.and_then(|s| {
UtcDateTime::parse_rfc3339_str(s)
.map_err(|e| format!("Failed to parse `UtcDateTime`: {e}"))
DateTime::parse_rfc3339_str(s)
.map_err(|e| format!("Failed to parse `DateTime`: {e}"))
})
}
}

#[cfg(test)]
mod test {
use bson::{oid::ObjectId, DateTime as UtcDateTime};
use bson::oid::ObjectId;

use crate::{graphql_input_value, FromInputValue, InputValue};

Expand All @@ -60,21 +99,139 @@ mod test {

assert_eq!(parsed, id);
}
}

#[test]
fn utcdatetime_from_input() {
use chrono::{DateTime, Utc};
#[cfg(test)]
mod date_time_test {
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};

let raw = "2020-03-23T17:38:32.446+00:00";
let input: InputValue = graphql_input_value!((raw));
use super::DateTime;

let parsed: UtcDateTime = FromInputValue::from_input_value(&input).unwrap();
let date_time = UtcDateTime::from_chrono(
DateTime::parse_from_rfc3339(raw)
.unwrap()
.with_timezone(&Utc),
);
#[test]
fn parses_correct_input() {
for (raw, expected) in [
(
"2014-11-28T21:00:09+09:00",
DateTime::builder()
.year(2014)
.month(11)
.day(28)
.hour(12)
.second(9)
.build()
.unwrap(),
),
(
"2014-11-28T21:00:09Z",
DateTime::builder()
.year(2014)
.month(11)
.day(28)
.hour(21)
.second(9)
.build()
.unwrap(),
),
(
"2014-11-28T21:00:09+00:00",
DateTime::builder()
.year(2014)
.month(11)
.day(28)
.hour(21)
.second(9)
.build()
.unwrap(),
),
(
"2014-11-28T21:00:09.05+09:00",
DateTime::builder()
.year(2014)
.month(11)
.day(28)
.hour(12)
.second(9)
.millisecond(50)
.build()
.unwrap(),
),
] {
let input: InputValue = graphql_input_value!((raw));
let parsed = DateTime::from_input_value(&input);

assert!(
parsed.is_ok(),
"failed to parse `{raw}`: {:?}",
parsed.unwrap_err(),
);
assert_eq!(parsed.unwrap(), expected, "input: {raw}");
}
}

assert_eq!(parsed, date_time);
#[test]
fn fails_on_invalid_input() {
for input in [
graphql_input_value!("12"),
graphql_input_value!("12:"),
graphql_input_value!("56:34:22"),
graphql_input_value!("56:34:22.000"),
graphql_input_value!("1996-12-1914:23:43"),
graphql_input_value!("1996-12-19 14:23:43Z"),
graphql_input_value!("1996-12-19T14:23:43"),
graphql_input_value!("1996-12-19T14:23:43ZZ"),
graphql_input_value!("1996-12-19T14:23:43.543"),
graphql_input_value!("1996-12-19T14:23"),
graphql_input_value!("1996-12-19T14:23:1"),
graphql_input_value!("1996-12-19T14:23:"),
graphql_input_value!("1996-12-19T23:78:43Z"),
graphql_input_value!("1996-12-19T23:18:99Z"),
graphql_input_value!("1996-12-19T24:00:00Z"),
graphql_input_value!("1996-12-19T99:02:13Z"),
graphql_input_value!("1996-12-19T99:02:13Z"),
graphql_input_value!("1996-12-19T12:02:13+4444444"),
graphql_input_value!("i'm not even a datetime"),
graphql_input_value!(2.32),
graphql_input_value!(1),
graphql_input_value!(null),
graphql_input_value!(false),
] {
let input: InputValue = input;
let parsed = DateTime::from_input_value(&input);

assert!(parsed.is_err(), "allows input: {input:?}");
}
}

#[test]
fn formats_correctly() {
for (val, expected) in [
(
DateTime::builder()
.year(1996)
.month(12)
.day(19)
.hour(12)
.build()
.unwrap(),
graphql_input_value!("1996-12-19T12:00:00Z"),
),
(
DateTime::builder()
.year(1564)
.month(1)
.day(30)
.hour(5)
.minute(3)
.second(3)
.millisecond(1)
.build()
.unwrap(),
graphql_input_value!("1564-01-30T05:03:03.001Z"),
),
] {
let actual: InputValue = val.to_input_value();

assert_eq!(actual, expected, "on value: {val}");
}
}
}
7 changes: 5 additions & 2 deletions juniper/src/integrations/chrono.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ mod local_date {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {v}"))
.and_then(|s| {
LocalDate::parse_from_str(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}"))
LocalDate::parse_from_str(s, FORMAT)
.map_err(|e| format!("Invalid `LocalDate`: {e}"))
})
}
}
Expand Down Expand Up @@ -749,7 +750,9 @@ mod integration_test {
types::scalars::{EmptyMutation, EmptySubscription},
};

use super::{LocalDate, DateTime, FixedOffset, FromFixedOffset, LocalDateTime, LocalTime, TimeZone};
use super::{
DateTime, FixedOffset, FromFixedOffset, LocalDate, LocalDateTime, LocalTime, TimeZone,
};

#[tokio::test]
async fn serializes() {
Expand Down
12 changes: 7 additions & 5 deletions juniper/src/integrations/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ mod local_date {
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<LocalDate, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {v}"))
.and_then(|s| LocalDate::parse(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}")))
.and_then(|s| {
LocalDate::parse(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}"))
})
}
}

Expand Down Expand Up @@ -641,15 +643,15 @@ mod integration_test {
types::scalars::{EmptyMutation, EmptySubscription},
};

use super::{Date, DateTime, LocalDateTime, LocalTime, UtcOffset};
use super::{DateTime, LocalDate, LocalDateTime, LocalTime, UtcOffset};

#[tokio::test]
async fn serializes() {
struct Root;

#[graphql_object]
impl Root {
fn date() -> Date {
fn local_date() -> LocalDate {
date!(2015 - 03 - 14)
}

Expand All @@ -671,7 +673,7 @@ mod integration_test {
}

const DOC: &str = r#"{
date
localDate
localTime
localDateTime
dateTime,
Expand All @@ -688,7 +690,7 @@ mod integration_test {
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({
"date": "2015-03-14",
"localDate": "2015-03-14",
"localTime": "16:07:08",
"localDateTime": "2016-07-08T09:10:11",
"dateTime": "1996-12-20T00:39:57Z",
Expand Down
9 changes: 9 additions & 0 deletions juniper/src/integrations/url.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
//! GraphQL support for [`url`] crate types.
//!
//! # Supported types
//!
//! | Rust type | GraphQL scalar |
//! |-----------|----------------|
//! | [`Url`] | [`URL`][s1] |
//!
//! [`Url`]: url::Url
//! [s1]: https://graphql-scalars.dev/docs/scalars/url
use crate::{graphql_scalar, InputValue, ScalarValue, Value};

Expand Down
13 changes: 10 additions & 3 deletions juniper/src/integrations/uuid.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
//! GraphQL support for [uuid](https://doc.rust-lang.org/uuid/uuid/struct.Uuid.html) types.
#![allow(clippy::needless_lifetimes)]
//! GraphQL support for [`uuid`] crate types.
//!
//! # Supported types
//!
//! | Rust type | GraphQL scalar |
//! |-----------|----------------|
//! | [`Uuid`] | [`UUID`][s1] |
//!
//! [`Uuid`]: uuid::Uuid
//! [s1]: https://graphql-scalars.dev/docs/scalars/uuid
use crate::{graphql_scalar, InputValue, ScalarValue, Value};

Expand Down

0 comments on commit 34ae0a5

Please sign in to comment.