Support time crate types as GraphQL scalars (#1006)

Co-authored-by: Kai Ren <tyranron@gmail.com>
This commit is contained in:
Scott Wilson 2021-12-16 12:43:15 -08:00 committed by GitHub
parent 265d4c5bb2
commit 3e4d4ea76e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 742 additions and 14 deletions

View file

@ -73,6 +73,7 @@ your Schemas automatically.
- [url][url] - [url][url]
- [chrono][chrono] - [chrono][chrono]
- [chrono-tz][chrono-tz] - [chrono-tz][chrono-tz]
- [time][time]
- [bson][bson] - [bson][bson]
### Web Frameworks ### Web Frameworks
@ -118,5 +119,6 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
[url]: https://crates.io/crates/url [url]: https://crates.io/crates/url
[chrono]: https://crates.io/crates/chrono [chrono]: https://crates.io/crates/chrono
[chrono-tz]: https://crates.io/crates/chrono-tz [chrono-tz]: https://crates.io/crates/chrono-tz
[time]: https://crates.io/crates/time
[bson]: https://crates.io/crates/bson [bson]: https://crates.io/crates/bson
[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema [juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema

View file

@ -36,6 +36,7 @@ crates. They are enabled via features that are on by default.
* uuid::Uuid * uuid::Uuid
* chrono::DateTime * chrono::DateTime
* time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}
* url::Url * url::Url
* bson::oid::ObjectId * bson::oid::ObjectId
@ -103,7 +104,7 @@ The example below is used just for illustration.
# extern crate juniper; # extern crate juniper;
# mod date { # mod date {
# pub struct Date; # pub struct Date;
# impl std::str::FromStr for Date{ # impl std::str::FromStr for Date {
# type Err = String; fn from_str(_value: &str) -> Result<Self, Self::Err> { unimplemented!() } # type Err = String; fn from_str(_value: &str) -> Result<Self, Self::Err> { unimplemented!() }
# } # }
# // And we define how to represent date as a string. # // And we define how to represent date as a string.

View file

@ -19,6 +19,7 @@
- Use `null` in addition to `None` to create `Value::Null` in `graphql_value!` macro to mirror `serde_json::json!`. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Use `null` in addition to `None` to create `Value::Null` in `graphql_value!` macro to mirror `serde_json::json!`. ([#996](https://github.com/graphql-rust/juniper/pull/996))
- Add `From` impls to `InputValue` mirroring the ones for `Value` and provide better support for `Option` handling. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Add `From` impls to `InputValue` mirroring the ones for `Value` and provide better support for `Option` handling. ([#996](https://github.com/graphql-rust/juniper/pull/996))
- Implement `graphql_input_value!` and `graphql_vars!` macros. ([#996](https://github.com/graphql-rust/juniper/pull/996)) - Implement `graphql_input_value!` and `graphql_vars!` macros. ([#996](https://github.com/graphql-rust/juniper/pull/996))
- Support [`time` crate](https://docs.rs/time) types as GraphQL scalars behind `time` feature. ([#1006](https://github.com/graphql-rust/juniper/pull/1006))
## Fixes ## Fixes

View file

@ -22,6 +22,7 @@ travis-ci = { repository = "graphql-rust/juniper" }
default = [ default = [
"bson", "bson",
"chrono", "chrono",
"time",
"schema-language", "schema-language",
"url", "url",
"uuid", "uuid",
@ -48,6 +49,7 @@ serde = { version = "1.0.8", features = ["derive"], default-features = false }
serde_json = { version = "1.0.2", default-features = false, optional = true } serde_json = { version = "1.0.2", default-features = false, optional = true }
smartstring = "0.2.6" smartstring = "0.2.6"
static_assertions = "1.1" static_assertions = "1.1"
time = { version = "0.3", features = ["formatting", "macros", "parsing"], optional = true }
url = { version = "2.0", optional = true } url = { version = "2.0", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View file

@ -24,9 +24,6 @@ use crate::{
Value, Value,
}; };
#[doc(hidden)]
pub static RFC3339_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z";
#[crate::graphql_scalar(name = "DateTimeFixedOffset", description = "DateTime")] #[crate::graphql_scalar(name = "DateTimeFixedOffset", description = "DateTime")]
impl<S> GraphQLScalar for DateTime<FixedOffset> impl<S> GraphQLScalar for DateTime<FixedOffset>
where where

View file

@ -8,6 +8,8 @@ pub mod chrono;
pub mod chrono_tz; pub mod chrono_tz;
#[doc(hidden)] #[doc(hidden)]
pub mod serde; pub mod serde;
#[cfg(feature = "time")]
pub mod time;
#[cfg(feature = "url")] #[cfg(feature = "url")]
pub mod url; pub mod url;
#[cfg(feature = "uuid")] #[cfg(feature = "uuid")]

View file

@ -0,0 +1,721 @@
//! GraphQL support for [`time`] crate types.
//!
//! # Supported types
//!
//! | Rust type | Format | GraphQL scalar |
//! |-----------------------|-----------------------|---------------------|
//! | [`Date`] | `yyyy-MM-dd` | [`Date`][s1] |
//! | [`Time`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] |
//! | [`PrimitiveDateTime`] | `yyyy-MM-dd HH:mm:ss` | `LocalDateTime` |
//! | [`OffsetDateTime`] | [RFC 3339] string | [`DateTime`][s4] |
//! | [`UtcOffset`] | `±hh:mm` | [`UtcOffset`][s5] |
//!
//! [`Date`]: time::Date
//! [`OffsetDateTime`]: time::OffsetDateTime
//! [`PrimitiveDateTime`]: time::PrimitiveDateTime
//! [`Time`]: time::Time
//! [`UtcOffset`]: time::UtcOffset
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
//! [s1]: https://graphql-scalars.dev/docs/scalars/date
//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
//! [s5]: https://graphql-scalars.dev/docs/scalars/utc-offset
use time::{
format_description::{well_known::Rfc3339, FormatItem},
macros::format_description,
};
use crate::{
graphql_scalar,
parser::{ParseError, ScalarToken, Token},
value::ParseScalarResult,
Value,
};
pub use time::{
Date, OffsetDateTime as DateTime, PrimitiveDateTime as LocalDateTime, Time as LocalTime,
UtcOffset,
};
/// Format of a [`Date` scalar][1].
///
/// [1]: https://graphql-scalars.dev/docs/scalars/date
const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]");
#[graphql_scalar(
description = "Date in the proleptic Gregorian calendar (without time \
zone).\
\n\n\
Represents a description of the date (as used for birthdays,
for example). It cannot represent an instant on the \
time-line.\
\n\n\
[`Date` scalar][1] compliant.\
\n\n\
See also [`time::Date`][2] for details.\
\n\n\
[1]: https://graphql-scalars.dev/docs/scalars/date\n\
[2]: https://docs.rs/time/*/time/struct.Date.html"
)]
impl<S: ScalarValue> GraphQLScalar for Date {
fn resolve(&self) -> Value {
Value::scalar(
self.format(DATE_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `Date`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| Self::parse(s, DATE_FORMAT).map_err(|e| format!("Invalid `Date`: {}", e)))
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
/// Full format of a [`LocalTime` scalar][1].
///
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
const LOCAL_TIME_FORMAT: &[FormatItem<'_>] =
format_description!("[hour]:[minute]:[second].[subsecond digits:3]");
/// Format of a [`LocalTime` scalar][1] without milliseconds.
///
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
const LOCAL_TIME_FORMAT_NO_MILLIS: &[FormatItem<'_>] =
format_description!("[hour]:[minute]:[second]");
/// Format of a [`LocalTime` scalar][1] without seconds.
///
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
const LOCAL_TIME_FORMAT_NO_SECS: &[FormatItem<'_>] = format_description!("[hour]:[minute]");
#[graphql_scalar(description = "Clock time within a given date (without time zone) in \
`HH:mm[:ss[.SSS]]` format.\
\n\n\
All minutes are assumed to have exactly 60 seconds; no \
attempt is made to handle leap seconds (either positive or \
negative).\
\n\n\
[`LocalTime` scalar][1] compliant.\
\n\n\
See also [`time::Time`][2] for details.\
\n\n\
[1]: https://graphql-scalars.dev/docs/scalars/local-time\n\
[2]: https://docs.rs/time/*/time/struct.Time.html")]
impl<S: ScalarValue> GraphQLScalar for LocalTime {
fn resolve(&self) -> Value {
Value::scalar(
if self.millisecond() == 0 {
self.format(LOCAL_TIME_FORMAT_NO_MILLIS)
} else {
self.format(LOCAL_TIME_FORMAT)
}
.unwrap_or_else(|e| panic!("Failed to format `LocalTime`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
// First, try to parse the most used format.
// At the end, try to parse the full format for the parsing
// error to be most informative.
Self::parse(s, LOCAL_TIME_FORMAT_NO_MILLIS)
.or_else(|_| Self::parse(s, LOCAL_TIME_FORMAT_NO_SECS))
.or_else(|_| Self::parse(s, LOCAL_TIME_FORMAT))
.map_err(|e| format!("Invalid `LocalTime`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
/// Format of a [`LocalDateTime`] scalar.
const LOCAL_DATE_TIME_FORMAT: &[FormatItem<'_>] =
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
#[graphql_scalar(
description = "Combined date and time (without time zone) in `yyyy-MM-dd \
HH:mm:ss` format.\
\n\n\
See also [`time::PrimitiveDateTime`][2] for details.\
\n\n\
[2]: https://docs.rs/time/*/time/struct.PrimitiveDateTime.html"
)]
impl<S: ScalarValue> GraphQLScalar for LocalDateTime {
fn resolve(&self) -> Value {
Value::scalar(
self.format(LOCAL_DATE_TIME_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `LocalDateTime`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
Self::parse(s, LOCAL_DATE_TIME_FORMAT)
.map_err(|e| format!("Invalid `LocalDateTime`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
#[graphql_scalar(
description = "Combined date and time (with time zone) in [RFC 3339][0] \
format.\
\n\n\
Represents a description of an exact instant on the \
time-line (such as the instant that a user account was \
created).\
\n\n\
[`DateTime` scalar][1] compliant.\
\n\n\
See also [`time::OffsetDateTime`][2] for details.\
\n\n\
[0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6\n\
[1]: https://graphql-scalars.dev/docs/scalars/date-time\n\
[2]: https://docs.rs/time/*/time/struct.OffsetDateTime.html"
)]
impl<S: ScalarValue> GraphQLScalar for DateTime {
fn resolve(&self) -> Value {
Value::scalar(
self.format(&Rfc3339)
.unwrap_or_else(|e| panic!("Failed to format `DateTime`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
Self::parse(s, &Rfc3339).map_err(|e| format!("Invalid `DateTime`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
/// Format of a [`UtcOffset` scalar][1].
///
/// [1]: https://graphql-scalars.dev/docs/scalars/utc-offset
const UTC_OFFSET_FORMAT: &[FormatItem<'_>] =
format_description!("[offset_hour sign:mandatory]:[offset_minute]");
#[graphql_scalar(
description = "Offset from UTC in `±hh:mm` format. See [list of database \
time zones][0].\
\n\n\
[`UtcOffset` scalar][1] compliant.\
\n\n\
See also [`time::UtcOffset`][2] for details.\
\n\n\
[0]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n\
[1]: https://graphql-scalars.dev/docs/scalars/utc-offset\n\
[2]: https://docs.rs/time/*/time/struct.UtcOffset.html"
)]
impl<S: ScalarValue> GraphQLScalar for UtcOffset {
fn resolve(&self) -> Value {
Value::scalar(
self.format(UTC_OFFSET_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `UtcOffset`: {}", e)),
)
}
fn from_input_value(v: &InputValue) -> Result<Self, String> {
v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {}", v))
.and_then(|s| {
Self::parse(s, UTC_OFFSET_FORMAT).map_err(|e| format!("Invalid `UtcOffset`: {}", e))
})
}
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
if let ScalarToken::String(s) = value {
Ok(S::from(s.to_owned()))
} else {
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
}
}
}
#[cfg(test)]
mod date_test {
use time::macros::date;
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
use super::Date;
#[test]
fn parses_correct_input() {
for (raw, expected) in [
("1996-12-19", date!(1996 - 12 - 19)),
("1564-01-30", date!(1564 - 01 - 30)),
] {
let input: InputValue = graphql_input_value!((raw));
let parsed = Date::from_input_value(&input);
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
raw,
parsed.unwrap_err(),
);
assert_eq!(parsed.unwrap(), expected, "input: {}", raw);
}
}
#[test]
fn fails_on_invalid_input() {
for input in [
graphql_input_value!("1996-13-19"),
graphql_input_value!("1564-01-61"),
graphql_input_value!("2021-11-31"),
graphql_input_value!("11-31"),
graphql_input_value!("2021-11"),
graphql_input_value!("2021"),
graphql_input_value!("31"),
graphql_input_value!("i'm not even a date"),
graphql_input_value!(2.32),
graphql_input_value!(1),
graphql_input_value!(null),
graphql_input_value!(false),
] {
let input: InputValue = input;
let parsed = Date::from_input_value(&input);
assert!(parsed.is_err(), "allows input: {:?}", input);
}
}
#[test]
fn formats_correctly() {
for (val, expected) in [
(date!(1996 - 12 - 19), graphql_input_value!("1996-12-19")),
(date!(1564 - 01 - 30), graphql_input_value!("1564-01-30")),
(date!(2020 - W 01 - 3), graphql_input_value!("2020-01-01")),
(date!(2020 - 001), graphql_input_value!("2020-01-01")),
] {
let actual: InputValue = val.to_input_value();
assert_eq!(actual, expected, "on value: {}", val);
}
}
}
#[cfg(test)]
mod local_time_test {
use time::macros::time;
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
use super::LocalTime;
#[test]
fn parses_correct_input() {
for (raw, expected) in [
("14:23:43", time!(14:23:43)),
("14:00:00", time!(14:00)),
("14:00", time!(14:00)),
("14:32", time!(14:32:00)),
("14:00:00.000", time!(14:00)),
("14:23:43.345", time!(14:23:43.345)),
] {
let input: InputValue = graphql_input_value!((raw));
let parsed = LocalTime::from_input_value(&input);
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
raw,
parsed.unwrap_err(),
);
assert_eq!(parsed.unwrap(), expected, "input: {}", raw);
}
}
#[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!("23:78:43"),
graphql_input_value!("23:78:"),
graphql_input_value!("23:18:99"),
graphql_input_value!("23:18:22.4351"),
graphql_input_value!("23:18:22."),
graphql_input_value!("23:18:22.3"),
graphql_input_value!("23:18:22.03"),
graphql_input_value!("22.03"),
graphql_input_value!("24:00"),
graphql_input_value!("24:00:00"),
graphql_input_value!("24:00:00.000"),
graphql_input_value!("i'm not even a time"),
graphql_input_value!(2.32),
graphql_input_value!(1),
graphql_input_value!(null),
graphql_input_value!(false),
] {
let input: InputValue = input;
let parsed = LocalTime::from_input_value(&input);
assert!(parsed.is_err(), "allows input: {:?}", input);
}
}
#[test]
fn formats_correctly() {
for (val, expected) in [
(time!(1:02:03.004_005), graphql_input_value!("01:02:03.004")),
(time!(0:00), graphql_input_value!("00:00:00")),
(time!(12:00 pm), graphql_input_value!("12:00:00")),
(time!(1:02:03), graphql_input_value!("01:02:03")),
] {
let actual: InputValue = val.to_input_value();
assert_eq!(actual, expected, "on value: {}", val);
}
}
}
#[cfg(test)]
mod local_date_time_test {
use time::macros::datetime;
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
use super::LocalDateTime;
#[test]
fn parses_correct_input() {
for (raw, expected) in [
("1996-12-19 14:23:43", datetime!(1996-12-19 14:23:43)),
("1564-01-30 14:00:00", datetime!(1564-01-30 14:00)),
] {
let input: InputValue = graphql_input_value!((raw));
let parsed = LocalDateTime::from_input_value(&input);
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
raw,
parsed.unwrap_err(),
);
assert_eq!(parsed.unwrap(), expected, "input: {}", raw);
}
}
#[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-19T14:23:43"),
graphql_input_value!("1996-12-19 14:23:43Z"),
graphql_input_value!("1996-12-19 14:23:43.543"),
graphql_input_value!("1996-12-19 14:23"),
graphql_input_value!("1996-12-19 14:23:1"),
graphql_input_value!("1996-12-19 14:23:"),
graphql_input_value!("1996-12-19 23:78:43"),
graphql_input_value!("1996-12-19 23:18:99"),
graphql_input_value!("1996-12-19 24:00:00"),
graphql_input_value!("1996-12-19 99:02:13"),
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 = LocalDateTime::from_input_value(&input);
assert!(parsed.is_err(), "allows input: {:?}", input);
}
}
#[test]
fn formats_correctly() {
for (val, expected) in [
(
datetime!(1996-12-19 12:00 am),
graphql_input_value!("1996-12-19 00:00:00"),
),
(
datetime!(1564-01-30 14:00),
graphql_input_value!("1564-01-30 14:00:00"),
),
] {
let actual: InputValue = val.to_input_value();
assert_eq!(actual, expected, "on value: {}", val);
}
}
}
#[cfg(test)]
mod date_time_test {
use time::macros::datetime;
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
use super::DateTime;
#[test]
fn parses_correct_input() {
for (raw, expected) in [
(
"2014-11-28T21:00:09+09:00",
datetime!(2014-11-28 21:00:09 +9),
),
("2014-11-28T21:00:09Z", datetime!(2014-11-28 21:00:09 +0)),
(
"2014-11-28T21:00:09+00:00",
datetime!(2014-11-28 21:00:09 +0),
),
(
"2014-11-28T21:00:09.05+09:00",
datetime!(2014-11-28 21:00:09.05 +9),
),
] {
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);
}
}
#[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!(1996-12-19 12:00 am UTC),
graphql_input_value!("1996-12-19T00:00:00Z"),
),
(
datetime!(1564-01-30 14:00 +9),
graphql_input_value!("1564-01-30T14:00:00+09:00"),
),
] {
let actual: InputValue = val.to_input_value();
assert_eq!(actual, expected, "on value: {}", val);
}
}
}
#[cfg(test)]
mod utc_offset_test {
use time::macros::offset;
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
use super::UtcOffset;
#[test]
fn parses_correct_input() {
for (raw, expected) in [
("+00:00", offset!(+0)),
("-00:00", offset!(-0)),
("+10:00", offset!(+10)),
("-07:30", offset!(-7:30)),
("+14:00", offset!(+14)),
("-12:00", offset!(-12)),
] {
let input: InputValue = graphql_input_value!((raw));
let parsed = UtcOffset::from_input_value(&input);
assert!(
parsed.is_ok(),
"failed to parse `{}`: {}",
raw,
parsed.unwrap_err(),
);
assert_eq!(parsed.unwrap(), expected, "input: {}", raw);
}
}
#[test]
fn fails_on_invalid_input() {
for input in [
graphql_input_value!("12"),
graphql_input_value!("12:"),
graphql_input_value!("12:00"),
graphql_input_value!("+12:"),
graphql_input_value!("+12:0"),
graphql_input_value!("+12:00:34"),
graphql_input_value!("+12"),
graphql_input_value!("-12"),
graphql_input_value!("-12:"),
graphql_input_value!("-12:0"),
graphql_input_value!("-12:00:32"),
graphql_input_value!("-999:00"),
graphql_input_value!("+999:00"),
graphql_input_value!("i'm not even an offset"),
graphql_input_value!(2.32),
graphql_input_value!(1),
graphql_input_value!(null),
graphql_input_value!(false),
] {
let input: InputValue = input;
let parsed = UtcOffset::from_input_value(&input);
assert!(parsed.is_err(), "allows input: {:?}", input);
}
}
#[test]
fn formats_correctly() {
for (val, expected) in [
(offset!(+1), graphql_input_value!("+01:00")),
(offset!(+0), graphql_input_value!("+00:00")),
(offset!(-2:30), graphql_input_value!("-02:30")),
] {
let actual: InputValue = val.to_input_value();
assert_eq!(actual, expected, "on value: {}", val);
}
}
}
#[cfg(test)]
mod integration_test {
use time::macros::{date, datetime, offset, time};
use crate::{
execute, graphql_object, graphql_value, graphql_vars,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
};
use super::{Date, DateTime, LocalDateTime, LocalTime, UtcOffset};
#[tokio::test]
async fn serializes() {
struct Root;
#[graphql_object]
impl Root {
fn date() -> Date {
date!(2015 - 03 - 14)
}
fn local_time() -> LocalTime {
time!(16:07:08)
}
fn local_date_time() -> LocalDateTime {
datetime!(2016-07-08 09:10:11)
}
fn date_time() -> DateTime {
datetime!(1996-12-19 16:39:57 -8)
}
fn utc_offset() -> UtcOffset {
offset!(+11:30)
}
}
const DOC: &str = r#"{
date
localTime
localDateTime
dateTime,
utcOffset,
}"#;
let schema = RootNode::new(
Root,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({
"date": "2015-03-14",
"localTime": "16:07:08",
"localDateTime": "2016-07-08 09:10:11",
"dateTime": "1996-12-19T16:39:57-08:00",
"utcOffset": "+11:30",
}),
vec![],
)),
);
}
}

View file

@ -59,6 +59,7 @@ your Schemas automatically.
* [uuid][uuid] * [uuid][uuid]
* [url][url] * [url][url]
* [chrono][chrono] * [chrono][chrono]
* [time][time]
* [bson][bson] * [bson][bson]
### Web Frameworks ### Web Frameworks
@ -87,6 +88,7 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
[uuid]: https://crates.io/crates/uuid [uuid]: https://crates.io/crates/uuid
[url]: https://crates.io/crates/url [url]: https://crates.io/crates/url
[chrono]: https://crates.io/crates/chrono [chrono]: https://crates.io/crates/chrono
[time]: https://crates.io/crates/time
[bson]: https://crates.io/crates/bson [bson]: https://crates.io/crates/bson
*/ */