From d06520ea8f54cd1f4d0eb6898ec06efce7b2274c Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 15 Dec 2021 14:37:23 -0800 Subject: [PATCH 1/5] feat: Add support for the time crate --- docs/book/content/types/scalars.md | 23 +- juniper/Cargo.toml | 2 + juniper/src/integrations/mod.rs | 2 + juniper/src/integrations/time.rs | 338 +++++++++++++++++++++++++++++ juniper/src/lib.rs | 2 + 5 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 juniper/src/integrations/time.rs diff --git a/docs/book/content/types/scalars.md b/docs/book/content/types/scalars.md index 23afc7fd3..19c63b955 100644 --- a/docs/book/content/types/scalars.md +++ b/docs/book/content/types/scalars.md @@ -6,11 +6,11 @@ but this often requires coordination with the client library intended to consume the API you're building. 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 -a custom derive. +a custom derive. * For more advanced use cases with custom validation, you can use the `graphql_scalar` proc macro. @@ -36,12 +36,13 @@ crates. They are enabled via features that are on by default. * uuid::Uuid * chrono::DateTime +* time::{Date, OffsetDateTime, PrimitiveDateTime} * url::Url * bson::oid::ObjectId ## 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 serde supports this pattern with `#[serde(transparent)]`. @@ -82,15 +83,15 @@ pub struct UserId(i32); ## 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. Typically, you represent your custom scalars as strings. The example below implements a custom scalar for a custom `Date` 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 +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 purpose. The example below is used just for illustration. @@ -101,9 +102,9 @@ The example below is used just for illustration. ```rust # extern crate juniper; -# mod date { -# pub struct Date; -# impl std::str::FromStr for Date{ +# mod date { +# pub struct Date; +# impl std::str::FromStr for Date{ # type Err = String; fn from_str(_value: &str) -> Result { unimplemented!() } # } # // And we define how to represent date as a string. @@ -118,7 +119,7 @@ use juniper::{Value, ParseScalarResult, ParseScalarValue}; use date::Date; #[juniper::graphql_scalar(description = "Date")] -impl GraphQLScalar for Date +impl GraphQLScalar for Date where S: ScalarValue { diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 0018acddd..b68b4654a 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -22,6 +22,7 @@ travis-ci = { repository = "graphql-rust/juniper" } default = [ "bson", "chrono", + "time", "schema-language", "url", "uuid", @@ -39,6 +40,7 @@ async-trait = "0.1.39" bson = { version = "2.0", features = ["chrono-0_4"], optional = true } chrono = { version = "0.4", default-features = false, optional = true } chrono-tz = { version = "0.6", default-features = false, optional = true } +time = { version = "0.3", features = ["parsing", "formatting"], optional = true } fnv = "1.0.3" futures = { version = "0.3.1", features = ["alloc"], default-features = false } futures-enum = { version = "0.1.12", default-features = false } diff --git a/juniper/src/integrations/mod.rs b/juniper/src/integrations/mod.rs index 76ac28ba6..d966997f2 100644 --- a/juniper/src/integrations/mod.rs +++ b/juniper/src/integrations/mod.rs @@ -8,6 +8,8 @@ pub mod chrono; pub mod chrono_tz; #[doc(hidden)] pub mod serde; +#[cfg(feature = "time")] +pub mod time; #[cfg(feature = "url")] pub mod url; #[cfg(feature = "uuid")] diff --git a/juniper/src/integrations/time.rs b/juniper/src/integrations/time.rs new file mode 100644 index 000000000..8086a10d1 --- /dev/null +++ b/juniper/src/integrations/time.rs @@ -0,0 +1,338 @@ +/*! + +# Supported types + +| Rust Type | JSON Serialization | Notes | +|---------------------|---------------------|--------------------------------------| +| `OffsetDateTime` | RFC3339 string | | +| `Date` | YYYY-MM-DD | | +| `PrimitiveDateTime` | YYYY-MM-DD HH-MM-SS | | +| `Time` | H:M:S | Optional. Use the `scalar-naivetime` | +| | | feature. | + +*/ +use time::{ + format_description::parse, format_description::well_known::Rfc3339, Date, OffsetDateTime, + PrimitiveDateTime, +}; + +#[cfg(feature = "scalar-naivetime")] +use time::Time; + +use crate::{ + parser::{ParseError, ScalarToken, Token}, + value::{ParseScalarResult, ParseScalarValue}, + Value, +}; + +#[doc(hidden)] +pub static RFC3339_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z"; + +#[crate::graphql_scalar(name = "DateTimeFixedOffset", description = "OffsetDateTime")] +impl GraphQLScalar for OffsetDateTime +where + S: ScalarValue, +{ + fn resolve(&self) -> Value { + Value::scalar( + self.format(&Rfc3339) + .expect("Failed to format `DateTimeFixedOffset`"), + ) + } + + fn from_input_value(v: &InputValue) -> Result { + v.as_string_value() + .ok_or_else(|| format!("Expected `String`, found: {}", v)) + .and_then(|s| { + time::OffsetDateTime::parse(s, &Rfc3339) + .map_err(|e| format!("Failed to parse `DateTimeFixedOffset`: {}", e)) + }) + } + + fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> { + if let ScalarToken::String(value) = value { + Ok(S::from(value.to_owned())) + } else { + Err(ParseError::UnexpectedToken(Token::Scalar(value))) + } + } +} + +#[crate::graphql_scalar(description = "Date")] +impl GraphQLScalar for Date +where + S: ScalarValue, +{ + fn resolve(&self) -> Value { + let description = + parse("[year]-[month]-[day]").expect("Failed to parse format description"); + Value::scalar( + self.format(&description) + .expect("Failed to format `Date`") + .to_string(), + ) + } + + fn from_input_value(v: &InputValue) -> Result { + v.as_string_value() + .ok_or_else(|| format!("Expected `String`, found: {}", v)) + .and_then(|s| { + let description = + parse("[year]-[month]-[day]").expect("Failed to parse format description"); + Date::parse(s, &description).map_err(|e| format!("Failed to parse `Date`: {}", e)) + }) + } + + fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> { + if let ScalarToken::String(value) = value { + Ok(S::from(value.to_owned())) + } else { + Err(ParseError::UnexpectedToken(Token::Scalar(value))) + } + } +} + +#[cfg(feature = "scalar-naivetime")] +#[crate::graphql_scalar(description = "Time")] +impl GraphQLScalar for Time +where + S: ScalarValue, +{ + fn resolve(&self) -> Value { + let description = + parse("[hour]:[minute]:[second]").expect("Failed to parse format description"); + Value::scalar( + self.format(&description) + .expect("Failed to format `Time`") + .to_string(), + ) + } + + fn from_input_value(v: &InputValue) -> Result { + v.as_string_value() + .ok_or_else(|| format!("Expected `String`, found: {}", v)) + .and_then(|s| { + let description = + parse("[hour]:[minute]:[second]").expect("Failed to parse format description"); + Time::parse(s, &description).map_err(|e| format!("Failed to parse `Time`: {}", e)) + }) + } + + fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> { + if let ScalarToken::String(value) = value { + Ok(S::from(value.to_owned())) + } else { + Err(ParseError::UnexpectedToken(Token::Scalar(value))) + } + } +} + +#[crate::graphql_scalar(description = "PrimitiveDateTime")] +impl GraphQLScalar for PrimitiveDateTime +where + S: ScalarValue, +{ + fn resolve(&self) -> Value { + let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]") + .expect("Failed to parse format description"); + Value::scalar( + self.format(&description) + .expect("Failed to format `PrimitiveDateTime`"), + ) + } + + fn from_input_value(v: &InputValue) -> Result { + v.as_string_value() + .ok_or_else(|| format!("Expected `String`, found: {}", v)) + .and_then(|s| { + let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]") + .expect("Failed to parse format description"); + PrimitiveDateTime::parse(s, &description) + .map_err(|e| format!("Failed to parse `PrimitiveDateTime`: {}", e)) + }) + } + + fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> { + >::from_str(value) + } +} + +#[cfg(test)] +mod test { + use std::convert::TryFrom; + + use time::{ + format_description::parse, format_description::well_known::Rfc3339, Date, Month, + OffsetDateTime, PrimitiveDateTime, Time, + }; + + use crate::{graphql_input_value, FromInputValue, InputValue}; + + fn offsetdatetime_test(raw: &'static str) { + let input: InputValue = graphql_input_value!((raw)); + + let parsed: OffsetDateTime = FromInputValue::from_input_value(&input).unwrap(); + let expected = OffsetDateTime::parse(raw, &Rfc3339).unwrap(); + + assert_eq!(parsed, expected); + } + + #[test] + fn offsetdatetime_from_input_value() { + offsetdatetime_test("2014-11-28T21:00:09+09:00"); + } + + #[test] + fn offsetdatetime_from_input_value_with_z_timezone() { + offsetdatetime_test("2014-11-28T21:00:09Z"); + } + + #[test] + fn offsetdatetime_from_input_value_with_fractional_seconds() { + offsetdatetime_test("2014-11-28T21:00:09.05+09:00"); + } + + #[test] + fn date_from_input_value() { + let y = 1996; + let m = 12; + let d = 19; + let input: InputValue = graphql_input_value!("1996-12-19"); + + let parsed: Date = FromInputValue::from_input_value(&input).unwrap(); + let expected = Date::from_calendar_date(y, Month::try_from(m).unwrap(), d).unwrap(); + + assert_eq!(parsed, expected); + + assert_eq!(parsed.year(), y); + assert_eq!(u8::from(parsed.month()), m); + assert_eq!(parsed.day(), d); + } + + #[test] + #[cfg(feature = "scalar-naivetime")] + fn time_from_input_value() { + let input: InputValue = graphql_input_value!("21:12:19"); + let [h, m, s] = [21, 12, 19]; + let parsed: Time = FromInputValue::from_input_value(&input).unwrap(); + let expected = Time::from_hms(h, m, s).unwrap(); + assert_eq!(parsed, expected); + assert_eq!(parsed.hour(), h); + assert_eq!(parsed.minute(), m); + assert_eq!(parsed.second(), s); + } + + #[test] + fn primitivedatetime_from_input_value() { + let raw = "2021-12-15 14:12:00"; + let input: InputValue = graphql_input_value!((raw)); + + let parsed: PrimitiveDateTime = FromInputValue::from_input_value(&input).unwrap(); + let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); + let expected = PrimitiveDateTime::parse(&raw, &description).unwrap(); + + assert_eq!(parsed, expected); + } +} + +#[cfg(test)] +mod integration_test { + use time::{ + format_description::parse, format_description::well_known::Rfc3339, Date, Month, + OffsetDateTime, PrimitiveDateTime, + }; + + #[cfg(feature = "scalar-naivetime")] + use time::Time; + + use crate::{ + graphql_object, graphql_value, graphql_vars, + schema::model::RootNode, + types::scalars::{EmptyMutation, EmptySubscription}, + }; + + #[tokio::test] + async fn test_serialization() { + struct Root; + + #[graphql_object] + #[cfg(feature = "scalar-naivetime")] + impl Root { + fn example_date() -> Date { + Date::from_calendar_date(2015, Month::March, 14).unwrap() + } + fn example_primitive_date_time() -> PrimitiveDateTime { + let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); + PrimitiveDateTime::parse("2016-07-08 09:10:11", &description).unwrap() + } + fn example_time() -> Time { + Time::from_hms(16, 7, 8).unwrap() + } + fn example_offset_date_time() -> OffsetDateTime { + OffsetDateTime::parse("1996-12-19T16:39:57-08:00", &Rfc3339).unwrap() + } + } + + #[graphql_object] + #[cfg(not(feature = "scalar-naivetime"))] + impl Root { + fn example_date() -> Date { + Date::from_calendar_date(2015, Month::March, 14).unwrap() + } + fn example_primitive_date_time() -> PrimitiveDateTime { + let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); + PrimitiveDateTime::parse("2016-07-08 09:10:11", &description).unwrap() + } + fn example_offset_date_time() -> OffsetDateTime { + OffsetDateTime::parse("1996-12-19T16:39:57-08:00", &Rfc3339).unwrap() + } + } + + #[cfg(feature = "scalar-naivetime")] + let doc = r#"{ + exampleDate, + examplePrimitiveDateTime, + exampleTime, + exampleOffsetDateTime, + }"#; + + #[cfg(not(feature = "scalar-naivetime"))] + let doc = r#"{ + exampleDate, + examplePrimitiveDateTime, + exampleOffsetDateTime, + }"#; + + let schema = RootNode::new( + Root, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + + let (result, errs) = crate::execute(doc, None, &schema, &graphql_vars! {}, &()) + .await + .expect("Execution failed"); + + assert_eq!(errs, []); + + #[cfg(feature = "scalar-naivetime")] + assert_eq!( + result, + graphql_value!({ + "exampleDate": "2015-03-14", + "examplePrimitiveDateTime": "2016-07-08 09:10:11", + "exampleTime": "16:07:08", + "exampleOffsetDateTime": "1996-12-19T16:39:57-08:00", + }), + ); + #[cfg(not(feature = "scalar-naivetime"))] + assert_eq!( + result, + graphql_value!({ + "exampleDate": "2015-03-14", + "examplePrimitiveDateTime": "2016-07-08 09:10:11", + "exampleOffsetDateTime": "1996-12-19T16:39:57-08:00", + }), + ); + } +} diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index a986d540b..406879726 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -59,6 +59,7 @@ your Schemas automatically. * [uuid][uuid] * [url][url] * [chrono][chrono] +* [time][time] * [bson][bson] ### 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 [url]: https://crates.io/crates/url [chrono]: https://crates.io/crates/chrono +[time]: https://crates.io/crates/time [bson]: https://crates.io/crates/bson */ From f28c37862a052ee58d5d54b1e83942e235b908a3 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 16 Dec 2021 19:56:34 +0100 Subject: [PATCH 2/5] Adjust --- README.md | 2 + docs/book/content/types/scalars.md | 4 +- juniper/Cargo.toml | 2 +- juniper/src/integrations/chrono.rs | 3 - juniper/src/integrations/time.rs | 706 ++++++++++++++++++++++------- 5 files changed, 546 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 1f6db33f9..3bd500489 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ your Schemas automatically. - [url][url] - [chrono][chrono] - [chrono-tz][chrono-tz] +- [time][time] - [bson][bson] ### 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 [chrono]: https://crates.io/crates/chrono [chrono-tz]: https://crates.io/crates/chrono-tz +[time]: https://crates.io/crates/time [bson]: https://crates.io/crates/bson [juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema diff --git a/docs/book/content/types/scalars.md b/docs/book/content/types/scalars.md index 19c63b955..09ba057ac 100644 --- a/docs/book/content/types/scalars.md +++ b/docs/book/content/types/scalars.md @@ -36,7 +36,7 @@ crates. They are enabled via features that are on by default. * uuid::Uuid * chrono::DateTime -* time::{Date, OffsetDateTime, PrimitiveDateTime} +* time::{Date, OffsetDateTime, PrimitiveDateTime, Time} * url::Url * bson::oid::ObjectId @@ -104,7 +104,7 @@ The example below is used just for illustration. # extern crate juniper; # mod 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 { unimplemented!() } # } # // And we define how to represent date as a string. diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index f89d3bc5c..6eb3d7d63 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -40,7 +40,6 @@ async-trait = "0.1.39" bson = { version = "2.0", features = ["chrono-0_4"], optional = true } chrono = { version = "0.4", default-features = false, optional = true } chrono-tz = { version = "0.6", default-features = false, optional = true } -time = { version = "0.3", features = ["parsing", "formatting"], optional = true } fnv = "1.0.3" futures = { version = "0.3.1", features = ["alloc"], default-features = false } futures-enum = { version = "0.1.12", default-features = false } @@ -50,6 +49,7 @@ serde = { version = "1.0.8", features = ["derive"], default-features = false } serde_json = { version = "1.0.2", default-features = false, optional = true } smartstring = "0.2.6" static_assertions = "1.1" +time = { version = "0.3", features = ["macros", "parsing", "formatting"], optional = true } url = { version = "2.0", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/juniper/src/integrations/chrono.rs b/juniper/src/integrations/chrono.rs index 9c0c5f64a..55cfd50ed 100644 --- a/juniper/src/integrations/chrono.rs +++ b/juniper/src/integrations/chrono.rs @@ -24,9 +24,6 @@ use crate::{ Value, }; -#[doc(hidden)] -pub static RFC3339_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z"; - #[crate::graphql_scalar(name = "DateTimeFixedOffset", description = "DateTime")] impl GraphQLScalar for DateTime where diff --git a/juniper/src/integrations/time.rs b/juniper/src/integrations/time.rs index 8086a10d1..3792c42f0 100644 --- a/juniper/src/integrations/time.rs +++ b/juniper/src/integrations/time.rs @@ -1,250 +1,660 @@ -/*! +//! 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 -# Supported types - -| Rust Type | JSON Serialization | Notes | -|---------------------|---------------------|--------------------------------------| -| `OffsetDateTime` | RFC3339 string | | -| `Date` | YYYY-MM-DD | | -| `PrimitiveDateTime` | YYYY-MM-DD HH-MM-SS | | -| `Time` | H:M:S | Optional. Use the `scalar-naivetime` | -| | | feature. | - -*/ use time::{ - format_description::parse, format_description::well_known::Rfc3339, Date, OffsetDateTime, - PrimitiveDateTime, + format_description::{well_known::Rfc3339, FormatItem}, + macros::format_description, }; -#[cfg(feature = "scalar-naivetime")] -use time::Time; - use crate::{ + graphql_scalar, parser::{ParseError, ScalarToken, Token}, - value::{ParseScalarResult, ParseScalarValue}, + value::ParseScalarResult, Value, }; -#[doc(hidden)] -pub static RFC3339_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z"; +pub use time::{ + Date, OffsetDateTime as DateTime, PrimitiveDateTime as LocalDateTime, Time as LocalTime, + UtcOffset, +}; -#[crate::graphql_scalar(name = "DateTimeFixedOffset", description = "OffsetDateTime")] -impl GraphQLScalar for OffsetDateTime -where - S: ScalarValue, -{ +/// 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 GraphQLScalar for Date { fn resolve(&self) -> Value { Value::scalar( - self.format(&Rfc3339) - .expect("Failed to format `DateTimeFixedOffset`"), + self.format(DATE_FORMAT) + .unwrap_or_else(|e| panic!("Failed to format `Date`: {}", e)), + ) + } + + fn from_input_value(v: &InputValue) -> Result { + 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 = "The clock time within a given date (without time zone).\ + \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 GraphQLScalar for LocalTime { + fn resolve(&self) -> Value { + Value::scalar( + self.format(LOCAL_TIME_FORMAT) + .unwrap_or_else(|e| panic!("Failed to format `LocalTime`: {}", e)), ) } - fn from_input_value(v: &InputValue) -> Result { + fn from_input_value(v: &InputValue) -> Result { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - time::OffsetDateTime::parse(s, &Rfc3339) - .map_err(|e| format!("Failed to parse `DateTimeFixedOffset`: {}", e)) + // 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(value) = value { - Ok(S::from(value.to_owned())) + if let ScalarToken::String(s) = value { + Ok(S::from(s.to_owned())) } else { Err(ParseError::UnexpectedToken(Token::Scalar(value))) } } } -#[crate::graphql_scalar(description = "Date")] -impl GraphQLScalar for Date -where - S: ScalarValue, -{ +/// 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 GraphQLScalar for LocalDateTime { fn resolve(&self) -> Value { - let description = - parse("[year]-[month]-[day]").expect("Failed to parse format description"); Value::scalar( - self.format(&description) - .expect("Failed to format `Date`") - .to_string(), + self.format(LOCAL_DATE_TIME_FORMAT) + .unwrap_or_else(|e| panic!("Failed to format `LocalDateTime`: {}", e)), ) } - fn from_input_value(v: &InputValue) -> Result { + fn from_input_value(v: &InputValue) -> Result { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - let description = - parse("[year]-[month]-[day]").expect("Failed to parse format description"); - Date::parse(s, &description).map_err(|e| format!("Failed to parse `Date`: {}", e)) + 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(value) = value { - Ok(S::from(value.to_owned())) + if let ScalarToken::String(s) = value { + Ok(S::from(s.to_owned())) } else { Err(ParseError::UnexpectedToken(Token::Scalar(value))) } } } -#[cfg(feature = "scalar-naivetime")] -#[crate::graphql_scalar(description = "Time")] -impl GraphQLScalar for Time -where - S: ScalarValue, -{ +#[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 GraphQLScalar for DateTime { fn resolve(&self) -> Value { - let description = - parse("[hour]:[minute]:[second]").expect("Failed to parse format description"); Value::scalar( - self.format(&description) - .expect("Failed to format `Time`") - .to_string(), + self.format(&Rfc3339) + .unwrap_or_else(|e| panic!("Failed to format `DateTime`: {}", e)), ) } - fn from_input_value(v: &InputValue) -> Result { + fn from_input_value(v: &InputValue) -> Result { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - let description = - parse("[hour]:[minute]:[second]").expect("Failed to parse format description"); - Time::parse(s, &description).map_err(|e| format!("Failed to parse `Time`: {}", e)) + 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(value) = value { - Ok(S::from(value.to_owned())) + if let ScalarToken::String(s) = value { + Ok(S::from(s.to_owned())) } else { Err(ParseError::UnexpectedToken(Token::Scalar(value))) } } } -#[crate::graphql_scalar(description = "PrimitiveDateTime")] -impl GraphQLScalar for PrimitiveDateTime -where - S: ScalarValue, -{ +/// 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 GraphQLScalar for UtcOffset { fn resolve(&self) -> Value { - let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]") - .expect("Failed to parse format description"); Value::scalar( - self.format(&description) - .expect("Failed to format `PrimitiveDateTime`"), + self.format(UTC_OFFSET_FORMAT) + .unwrap_or_else(|e| panic!("Failed to format `UtcOffset`: {}", e)), ) } - fn from_input_value(v: &InputValue) -> Result { + fn from_input_value(v: &InputValue) -> Result { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]") - .expect("Failed to parse format description"); - PrimitiveDateTime::parse(s, &description) - .map_err(|e| format!("Failed to parse `PrimitiveDateTime`: {}", e)) + Self::parse(s, UTC_OFFSET_FORMAT).map_err(|e| format!("Invalid `UtcOffset`: {}", e)) }) } fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> { - >::from_str(value) + if let ScalarToken::String(s) = value { + Ok(S::from(s.to_owned())) + } else { + Err(ParseError::UnexpectedToken(Token::Scalar(value))) + } } } #[cfg(test)] -mod test { - use std::convert::TryFrom; +mod date_test { + use time::macros::date; - use time::{ - format_description::parse, format_description::well_known::Rfc3339, Date, Month, - OffsetDateTime, PrimitiveDateTime, Time, - }; + use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _}; - use crate::{graphql_input_value, FromInputValue, InputValue}; + use super::Date; - fn offsetdatetime_test(raw: &'static str) { - let input: InputValue = graphql_input_value!((raw)); + #[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); + } + } - let parsed: OffsetDateTime = FromInputValue::from_input_value(&input).unwrap(); - let expected = OffsetDateTime::parse(raw, &Rfc3339).unwrap(); + #[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); + } + } - assert_eq!(parsed, expected); + #[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 offsetdatetime_from_input_value() { - offsetdatetime_test("2014-11-28T21:00:09+09:00"); + 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 offsetdatetime_from_input_value_with_z_timezone() { - offsetdatetime_test("2014-11-28T21:00:09Z"); + 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 offsetdatetime_from_input_value_with_fractional_seconds() { - offsetdatetime_test("2014-11-28T21:00:09.05+09:00"); + 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.000")), + (time!(12:00 pm), graphql_input_value!("12:00:00.000")), + (time!(1:02:03), graphql_input_value!("01:02:03.000")), + ] { + 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 date_from_input_value() { - let y = 1996; - let m = 12; - let d = 19; - let input: InputValue = graphql_input_value!("1996-12-19"); + 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); + } + } - let parsed: Date = FromInputValue::from_input_value(&input).unwrap(); - let expected = Date::from_calendar_date(y, Month::try_from(m).unwrap(), d).unwrap(); + #[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); + } + } - assert_eq!(parsed, expected); + #[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; - assert_eq!(parsed.year(), y); - assert_eq!(u8::from(parsed.month()), m); - assert_eq!(parsed.day(), d); + 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] - #[cfg(feature = "scalar-naivetime")] - fn time_from_input_value() { - let input: InputValue = graphql_input_value!("21:12:19"); - let [h, m, s] = [21, 12, 19]; - let parsed: Time = FromInputValue::from_input_value(&input).unwrap(); - let expected = Time::from_hms(h, m, s).unwrap(); - assert_eq!(parsed, expected); - assert_eq!(parsed.hour(), h); - assert_eq!(parsed.minute(), m); - assert_eq!(parsed.second(), s); + 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 primitivedatetime_from_input_value() { - let raw = "2021-12-15 14:12:00"; - let input: InputValue = graphql_input_value!((raw)); + 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; - let parsed: PrimitiveDateTime = FromInputValue::from_input_value(&input).unwrap(); - let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); - let expected = PrimitiveDateTime::parse(&raw, &description).unwrap(); + use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _}; - assert_eq!(parsed, expected); + 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::{ - format_description::parse, format_description::well_known::Rfc3339, Date, Month, - OffsetDateTime, PrimitiveDateTime, + format_description::{parse, well_known::Rfc3339}, + Date, Month, OffsetDateTime, PrimitiveDateTime, Time, }; - #[cfg(feature = "scalar-naivetime")] - use time::Time; - use crate::{ graphql_object, graphql_value, graphql_vars, schema::model::RootNode, @@ -256,7 +666,6 @@ mod integration_test { struct Root; #[graphql_object] - #[cfg(feature = "scalar-naivetime")] impl Root { fn example_date() -> Date { Date::from_calendar_date(2015, Month::March, 14).unwrap() @@ -273,22 +682,6 @@ mod integration_test { } } - #[graphql_object] - #[cfg(not(feature = "scalar-naivetime"))] - impl Root { - fn example_date() -> Date { - Date::from_calendar_date(2015, Month::March, 14).unwrap() - } - fn example_primitive_date_time() -> PrimitiveDateTime { - let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); - PrimitiveDateTime::parse("2016-07-08 09:10:11", &description).unwrap() - } - fn example_offset_date_time() -> OffsetDateTime { - OffsetDateTime::parse("1996-12-19T16:39:57-08:00", &Rfc3339).unwrap() - } - } - - #[cfg(feature = "scalar-naivetime")] let doc = r#"{ exampleDate, examplePrimitiveDateTime, @@ -296,13 +689,6 @@ mod integration_test { exampleOffsetDateTime, }"#; - #[cfg(not(feature = "scalar-naivetime"))] - let doc = r#"{ - exampleDate, - examplePrimitiveDateTime, - exampleOffsetDateTime, - }"#; - let schema = RootNode::new( Root, EmptyMutation::<()>::new(), @@ -315,7 +701,6 @@ mod integration_test { assert_eq!(errs, []); - #[cfg(feature = "scalar-naivetime")] assert_eq!( result, graphql_value!({ @@ -325,14 +710,5 @@ mod integration_test { "exampleOffsetDateTime": "1996-12-19T16:39:57-08:00", }), ); - #[cfg(not(feature = "scalar-naivetime"))] - assert_eq!( - result, - graphql_value!({ - "exampleDate": "2015-03-14", - "examplePrimitiveDateTime": "2016-07-08 09:10:11", - "exampleOffsetDateTime": "1996-12-19T16:39:57-08:00", - }), - ); } } From 10ffec0fda95417e0dfb304ead030e3a88bff2d5 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 16 Dec 2021 20:00:36 +0100 Subject: [PATCH 3/5] Polish and CHANGELOG --- docs/book/content/types/scalars.md | 2 +- juniper/CHANGELOG.md | 1 + juniper/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/book/content/types/scalars.md b/docs/book/content/types/scalars.md index 09ba057ac..c8382833f 100644 --- a/docs/book/content/types/scalars.md +++ b/docs/book/content/types/scalars.md @@ -36,7 +36,7 @@ crates. They are enabled via features that are on by default. * uuid::Uuid * chrono::DateTime -* time::{Date, OffsetDateTime, PrimitiveDateTime, Time} +* time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset} * url::Url * bson::oid::ObjectId diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index c90fc5bcd..8736f53f2 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -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)) - 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)) +- Support [`time` crate](https://docs.rs/time) types as GraphQL scalars behind `time` feature. ([#1006](https://github.com/graphql-rust/juniper/pull/1006)) ## Fixes diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 6eb3d7d63..5262ad8d9 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -49,7 +49,7 @@ serde = { version = "1.0.8", features = ["derive"], default-features = false } serde_json = { version = "1.0.2", default-features = false, optional = true } smartstring = "0.2.6" static_assertions = "1.1" -time = { version = "0.3", features = ["macros", "parsing", "formatting"], optional = true } +time = { version = "0.3", features = ["formatting", "macros", "parsing"], optional = true } url = { version = "2.0", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] From 70841719ebe7431abb90e0ef627c401247f73d84 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 16 Dec 2021 20:13:21 +0100 Subject: [PATCH 4/5] Polish integration test --- juniper/src/integrations/time.rs | 70 +++++++++++++++++--------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/juniper/src/integrations/time.rs b/juniper/src/integrations/time.rs index 3792c42f0..42eace906 100644 --- a/juniper/src/integrations/time.rs +++ b/juniper/src/integrations/time.rs @@ -650,43 +650,49 @@ mod utc_offset_test { #[cfg(test)] mod integration_test { - use time::{ - format_description::{parse, well_known::Rfc3339}, - Date, Month, OffsetDateTime, PrimitiveDateTime, Time, - }; + use time::macros::{date, datetime, offset, time}; use crate::{ - graphql_object, graphql_value, graphql_vars, + 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 test_serialization() { + async fn serializes() { struct Root; #[graphql_object] impl Root { - fn example_date() -> Date { - Date::from_calendar_date(2015, Month::March, 14).unwrap() + fn date() -> Date { + date!(2015 - 03 - 14) } - fn example_primitive_date_time() -> PrimitiveDateTime { - let description = parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); - PrimitiveDateTime::parse("2016-07-08 09:10:11", &description).unwrap() + + fn local_time() -> LocalTime { + time!(16:07:08) } - fn example_time() -> Time { - Time::from_hms(16, 7, 8).unwrap() + + fn local_date_time() -> LocalDateTime { + datetime!(2016-07-08 09:10:11) } - fn example_offset_date_time() -> OffsetDateTime { - OffsetDateTime::parse("1996-12-19T16:39:57-08:00", &Rfc3339).unwrap() + + fn date_time() -> DateTime { + datetime!(1996-12-19 16:39:57 -8) + } + + fn utc_offset() -> UtcOffset { + offset!(+11:30) } } - let doc = r#"{ - exampleDate, - examplePrimitiveDateTime, - exampleTime, - exampleOffsetDateTime, + const DOC: &str = r#"{ + date + localTime + localDateTime + dateTime, + utcOffset, }"#; let schema = RootNode::new( @@ -695,20 +701,18 @@ mod integration_test { EmptySubscription::<()>::new(), ); - let (result, errs) = crate::execute(doc, None, &schema, &graphql_vars! {}, &()) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - assert_eq!( - result, - graphql_value!({ - "exampleDate": "2015-03-14", - "examplePrimitiveDateTime": "2016-07-08 09:10:11", - "exampleTime": "16:07:08", - "exampleOffsetDateTime": "1996-12-19T16:39:57-08:00", - }), + 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![], + )), ); } } From 241dabbcdf7d29150d3638a2b1ebb1ad256436e2 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 16 Dec 2021 20:23:21 +0100 Subject: [PATCH 5/5] Polish --- juniper/src/integrations/time.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/juniper/src/integrations/time.rs b/juniper/src/integrations/time.rs index 42eace906..abc117686 100644 --- a/juniper/src/integrations/time.rs +++ b/juniper/src/integrations/time.rs @@ -98,8 +98,8 @@ const LOCAL_TIME_FORMAT_NO_MILLIS: &[FormatItem<'_>] = /// [1]: https://graphql-scalars.dev/docs/scalars/local-time const LOCAL_TIME_FORMAT_NO_SECS: &[FormatItem<'_>] = format_description!("[hour]:[minute]"); -#[graphql_scalar( - description = "The clock time within a given date (without time zone).\ +#[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 \ @@ -110,13 +110,16 @@ const LOCAL_TIME_FORMAT_NO_SECS: &[FormatItem<'_>] = format_description!("[hour] 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" -)] + [2]: https://docs.rs/time/*/time/struct.Time.html")] impl GraphQLScalar for LocalTime { fn resolve(&self) -> Value { Value::scalar( - self.format(LOCAL_TIME_FORMAT) - .unwrap_or_else(|e| panic!("Failed to format `LocalTime`: {}", e)), + 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)), ) } @@ -395,9 +398,9 @@ mod local_time_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.000")), - (time!(12:00 pm), graphql_input_value!("12:00:00.000")), - (time!(1:02:03), graphql_input_value!("01:02:03.000")), + (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();