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

@ -6,11 +6,11 @@ but this often requires coordination with the client library intended to consume
the API you're building. the API you're building.
Since any value going over the wire is eventually transformed into JSON, you're Since any value going over the wire is eventually transformed into JSON, you're
also limited in the data types you can use. also limited in the data types you can use.
There are two ways to define custom scalars. There are two ways to define custom scalars.
* For simple scalars that just wrap a primitive type, you can use the newtype pattern with * For simple scalars that just wrap a primitive type, you can use the newtype pattern with
a custom derive. a custom derive.
* For more advanced use cases with custom validation, you can use * For more advanced use cases with custom validation, you can use
the `graphql_scalar` proc macro. the `graphql_scalar` proc macro.
@ -36,12 +36,13 @@ 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
## newtype pattern ## newtype pattern
Often, you might need a custom scalar that just wraps an existing type. Often, you might need a custom scalar that just wraps an existing type.
This can be done with the newtype pattern and a custom derive, similar to how This can be done with the newtype pattern and a custom derive, similar to how
serde supports this pattern with `#[serde(transparent)]`. serde supports this pattern with `#[serde(transparent)]`.
@ -82,15 +83,15 @@ pub struct UserId(i32);
## Custom scalars ## Custom scalars
For more complex situations where you also need custom parsing or validation, For more complex situations where you also need custom parsing or validation,
you can use the `graphql_scalar` proc macro. you can use the `graphql_scalar` proc macro.
Typically, you represent your custom scalars as strings. Typically, you represent your custom scalars as strings.
The example below implements a custom scalar for a custom `Date` type. The example below implements a custom scalar for a custom `Date` type.
Note: juniper already has built-in support for the `chrono::DateTime` type Note: juniper already has built-in support for the `chrono::DateTime` type
via `chrono` feature, which is enabled by default and should be used for this via `chrono` feature, which is enabled by default and should be used for this
purpose. purpose.
The example below is used just for illustration. The example below is used just for illustration.
@ -101,9 +102,9 @@ The example below is used just for illustration.
```rust ```rust
# 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.
@ -118,7 +119,7 @@ use juniper::{Value, ParseScalarResult, ParseScalarValue};
use date::Date; use date::Date;
#[juniper::graphql_scalar(description = "Date")] #[juniper::graphql_scalar(description = "Date")]
impl<S> GraphQLScalar for Date impl<S> GraphQLScalar for Date
where where
S: ScalarValue S: ScalarValue
{ {

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
*/ */