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 ]);