From 2a2534525d6d6684e1e48d592b8a013199e80b5e Mon Sep 17 00:00:00 2001 From: Christian Legnitto <christian@legnitto.com> Date: Tue, 26 Sep 2017 22:53:16 +0300 Subject: [PATCH] chrono feature: Implement GraphQLType for chrono types Fixes https://github.com/graphql-rust/juniper/issues/87 --- juniper/Cargo.toml | 3 +- juniper/src/ast.rs | 8 ++ juniper/src/integrations/chrono.rs | 140 +++++++++++++++++++++++++++++ juniper/src/integrations/mod.rs | 11 ++- juniper/src/lib.rs | 12 ++- juniper/src/value.rs | 10 ++- 6 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 juniper/src/integrations/chrono.rs diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 66a796a7..37c94304 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -22,9 +22,10 @@ path = "benches/bench.rs" [features] nightly = [] expose-test-schema = [] -default = ["url", "uuid"] +default = ["chrono", "url", "uuid"] [dependencies] +chrono = { version = "^0.4.0", optional = true } ordermap = { version = "^0.2.11", features = ["serde-1"] } serde = { version = "^1.0.8" } serde_derive = {version="^1.0.8" } diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs index f1f13c5f..38b9ece9 100644 --- a/juniper/src/ast.rs +++ b/juniper/src/ast.rs @@ -338,6 +338,14 @@ impl InputValue { } } + /// View the underlying float value, if present. + pub fn as_float_value(&self) -> Option<f64> { + match *self { + InputValue::Float(f) => Some(f), + _ => None, + } + } + /// View the underlying string value, if present. pub fn as_string_value(&self) -> Option<&str> { match *self { diff --git a/juniper/src/integrations/chrono.rs b/juniper/src/integrations/chrono.rs new file mode 100644 index 00000000..53825fdd --- /dev/null +++ b/juniper/src/integrations/chrono.rs @@ -0,0 +1,140 @@ +/*! + +# Supported types + +| Rust Type | JSON Serialization | Notes | +|-------------------------|------------------------|-------------------------------------------| +| `DateTime<FixedOffset>` | RFC3339 string | | +| `DateTime<Utc>` | RFC3339 string | | +| `NaiveDate` | RFC3339 string | | +| `NaiveDateTime` | float (unix timestamp) | JSON numbers (i.e. IEEE doubles) are not | +| | | precise enough for nanoseconds. | +| | | Values will be truncated to microsecond | +| | | resolution. | + +*/ +use chrono::prelude::*; + +use ::Value; + +#[doc(hidden)] +pub static RFC3339_FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S%z"; + +graphql_scalar!(DateTime<FixedOffset> { + description: "DateTime" + + resolve(&self) -> Value { + Value::string(self.to_rfc3339()) + } + + from_input_value(v: &InputValue) -> Option<DateTime<FixedOffset>> { + v.as_string_value() + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + } +}); + +graphql_scalar!(DateTime<Utc> { + description: "DateTime" + + resolve(&self) -> Value { + Value::string(self.to_rfc3339()) + } + + from_input_value(v: &InputValue) -> Option<DateTime<Utc>> { + v.as_string_value() + .and_then(|s| (s.parse::<DateTime<Utc>>().ok())) + } +}); + +// Don't use `Date` as the docs say: +// "[Date] should be considered ambiguous at best, due to the " +// inherent lack of precision required for the time zone resolution. +// For serialization and deserialization uses, it is best to use +// `NaiveDate` instead." +graphql_scalar!(NaiveDate { + description: "NaiveDate" + + resolve(&self) -> Value { + Value::string(self.format(RFC3339_FORMAT).to_string()) + } + + from_input_value(v: &InputValue) -> Option<NaiveDate> { + v.as_string_value() + .and_then(|s| NaiveDate::parse_from_str(s, RFC3339_FORMAT).ok()) + } +}); + +/// JSON numbers (i.e. IEEE doubles) are not precise enough for nanosecond +/// datetimes. Values will be truncated to microsecond resolution. +graphql_scalar!(NaiveDateTime { + description: "NaiveDateTime" + + resolve(&self) -> Value { + Value::float(self.timestamp() as f64) + } + + from_input_value(v: &InputValue) -> Option<NaiveDateTime> { + v.as_float_value() + .and_then(|f| NaiveDateTime::from_timestamp_opt(f as i64, 0)) + } +}); + +#[cfg(test)] +mod test { + use chrono::prelude::*; + use super::RFC3339_FORMAT; + + #[test] + fn datetime_fixedoffset_from_input_value() { + let raw = "2014-11-28T21:00:09+09:00"; + let input = ::InputValue::String(raw.to_string()); + + let parsed: DateTime<FixedOffset> = ::FromInputValue::from_input_value(&input).unwrap(); + let expected = DateTime::parse_from_rfc3339(raw).unwrap(); + + assert_eq!(parsed, expected); + } + + #[test] + fn datetime_utc_from_input_value() { + let raw = "2014-11-28T21:00:09+09:00"; + let input = ::InputValue::String(raw.to_string()); + + let parsed: DateTime<Utc> = ::FromInputValue::from_input_value(&input).unwrap(); + let expected = DateTime::parse_from_rfc3339(raw).unwrap().with_timezone(&Utc); + + assert_eq!(parsed, expected); + } + + #[test] + fn naivedate_from_input_value() { + let raw = "1996-12-19T16:39:57-08:00"; + let input = ::InputValue::String(raw.to_string()); + + let parsed: NaiveDate = ::FromInputValue::from_input_value(&input).unwrap(); + let expected = NaiveDate::parse_from_str(raw, &RFC3339_FORMAT).unwrap(); + let expected_via_datetime = DateTime::parse_from_rfc3339(raw).unwrap().date().naive_utc(); + let expected_via_ymd = NaiveDate::from_ymd(1996, 12, 19); + + assert_eq!(parsed, expected); + assert_eq!(parsed, expected_via_datetime); + assert_eq!(parsed, expected_via_ymd); + + assert_eq!(parsed.year(), 1996); + assert_eq!(parsed.month(), 12); + assert_eq!(parsed.day(), 19); + } + + #[test] + fn naivedatetime_from_input_value() { + let raw = 1_000_000_000_f64; + let input = ::InputValue::Float(raw); + + let parsed: NaiveDateTime = ::FromInputValue::from_input_value(&input).unwrap(); + let expected = NaiveDateTime::from_timestamp_opt(raw as i64, 0).unwrap(); + + assert_eq!(parsed, expected); + assert_eq!(raw, expected.timestamp() as f64); + + } +} diff --git a/juniper/src/integrations/mod.rs b/juniper/src/integrations/mod.rs index 3a2e9b87..2c9116f2 100644 --- a/juniper/src/integrations/mod.rs +++ b/juniper/src/integrations/mod.rs @@ -1,7 +1,14 @@ +#[doc(hidden)] pub mod serde; +#[cfg(feature = "chrono")] +/// GraphQL support for [chrono](https://github.com/chronotope/chrono) types. +pub mod chrono; + #[cfg(feature = "url")] -mod url; +/// GraphQL support for [url](https://github.com/servo/rust-url) types. +pub mod url; #[cfg(feature = "uuid")] -mod uuid; +/// GraphQL support for [uuid](https://doc.rust-lang.org/uuid/uuid/struct.Uuid.html) types. +pub mod uuid; diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 9a98e345..39271351 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -95,6 +95,10 @@ Adding per type, field, and argument documentation is possible directly from this macro. For more in-depth information on how to expose fields and types, see the [`graphql_object!`][3] macro. +### Built-in object type integrations + +Juniper has [built-in integrations][object_integrations] for converting existing object types to +GraphQL objects for popular crates. ## Integrating with web servers @@ -108,6 +112,7 @@ To support this, Juniper offers additional crates that integrate with popular we [3]: macro.graphql_object!.html [Iron]: http://ironframework.io [Rocket]: https://rocket.rs +[object_integrations]: integrations/index.html */ #![cfg_attr(feature = "nightly", feature(test))] @@ -124,6 +129,9 @@ extern crate serde_json; extern crate ordermap; +#[cfg(any(test, feature = "chrono"))] +extern crate chrono; + #[cfg(any(test, feature = "url"))] extern crate url; @@ -141,7 +149,9 @@ mod types; mod schema; mod validation; mod executor; -mod integrations; +// This needs to be public until docs have support for private modules: +// https://github.com/rust-lang/cargo/issues/1520 +pub mod integrations; pub mod graphiql; pub mod http; #[macro_use] diff --git a/juniper/src/value.rs b/juniper/src/value.rs index 6424403c..a186cd25 100644 --- a/juniper/src/value.rs +++ b/juniper/src/value.rs @@ -76,6 +76,14 @@ impl Value { } } + /// View the underlying float value, if present. + pub fn as_float_value(&self) -> Option<&f64> { + match *self { + Value::Float(ref f) => Some(f), + _ => None, + } + } + /// View the underlying object value, if present. pub fn as_object_value(&self) -> Option<&OrderMap<String, Value>> { match *self { @@ -179,7 +187,7 @@ impl<T> From<Option<T>> for Value where Value: From<T> { /// passed in. /// ```rust /// #[macro_use] extern crate juniper; -/// +/// /// graphql_value!(1234); /// graphql_value!("test"); /// graphql_value!([ 1234, "test", true ]);