diff --git a/docs/book/content/types/scalars.md b/docs/book/content/types/scalars.md index ccd35a6e0..a17e07a2c 100644 --- a/docs/book/content/types/scalars.md +++ b/docs/book/content/types/scalars.md @@ -35,7 +35,8 @@ Juniper has built-in support for a few additional types from common third party crates. They are enabled via features that are on by default. * uuid::Uuid -* chrono::DateTime +* chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime} +* chrono_tz::Tz; * time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset} * url::Url * bson::oid::ObjectId diff --git a/examples/actix_subscriptions/Cargo.toml b/examples/actix_subscriptions/Cargo.toml index 3cf886146..cec502e39 100644 --- a/examples/actix_subscriptions/Cargo.toml +++ b/examples/actix_subscriptions/Cargo.toml @@ -6,8 +6,8 @@ authors = ["Mihai Dinculescu "] publish = false [dependencies] -actix-web = "4.0.0-beta.12" -actix-cors = "0.6.0-beta.4" +actix-web = "4.0" +actix-cors = "0.6" futures = "0.3" env_logger = "0.9" serde = "1.0" diff --git a/examples/actix_subscriptions/src/main.rs b/examples/actix_subscriptions/src/main.rs index d36c34d5a..66480242a 100644 --- a/examples/actix_subscriptions/src/main.rs +++ b/examples/actix_subscriptions/src/main.rs @@ -136,7 +136,7 @@ async fn main() -> std::io::Result<()> { .route(web::get().to(graphql)), ) .service(web::resource("/playground").route(web::get().to(playground))) - .default_service(web::route().to(|| { + .default_service(web::to(|| async { HttpResponse::Found() .append_header((header::LOCATION, "/playground")) .finish() diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index dc411719d..700175119 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -29,6 +29,9 @@ - Mirror `#[derive(GraphQLScalar)]` macro. - Support usage on type aliases in case `#[derive(GraphQLScalar)]` isn't applicable because of [orphan rules](https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules). - Rename `ScalarValue::as_boolean` to `ScalarValue::as_bool`. ([#1025](https://github.com/graphql-rust/juniper/pull/1025)) +- Change [`chrono` crate](https://docs.rs/chrono) GraphQL scalars according to the [graphql-scalars.dev](https://graphql-scalars.dev). ([#1010](https://github.com/graphql-rust/juniper/pull/1010)) +- Disable `chrono` feature by default. ([#1010](https://github.com/graphql-rust/juniper/pull/1010)) +- Remove `scalar-naivetime` feature. ([#1010](https://github.com/graphql-rust/juniper/pull/1010)) ## Features diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index c93c9c8ef..db67b6f19 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -21,14 +21,13 @@ travis-ci = { repository = "graphql-rust/juniper" } [features] default = [ "bson", - "chrono", "schema-language", "url", "uuid", ] +chrono-clock = ["chrono", "chrono/clock"] expose-test-schema = ["anyhow", "serde_json"] graphql-parser-integration = ["graphql-parser"] -scalar-naivetime = [] schema-language = ["graphql-parser-integration"] [dependencies] diff --git a/juniper/src/integrations/bson.rs b/juniper/src/integrations/bson.rs index c9af35fb6..44e092056 100644 --- a/juniper/src/integrations/bson.rs +++ b/juniper/src/integrations/bson.rs @@ -1,7 +1,5 @@ //! GraphQL support for [bson](https://github.com/mongodb/bson-rust) types. -use chrono::prelude::*; - use crate::{graphql_scalar, InputValue, ScalarValue, Value}; #[graphql_scalar(with = object_id, parse_token(String))] @@ -30,17 +28,16 @@ mod utc_date_time { use super::*; pub(super) fn to_output(v: &UtcDateTime) -> Value { - Value::scalar((*v).to_chrono().to_rfc3339()) + Value::scalar((*v).to_rfc3339_string()) } pub(super) fn from_input(v: &InputValue) -> Result { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - s.parse::>() + UtcDateTime::parse_rfc3339_str(s) .map_err(|e| format!("Failed to parse `UtcDateTime`: {}", e)) }) - .map(UtcDateTime::from_chrono) } } diff --git a/juniper/src/integrations/chrono.rs b/juniper/src/integrations/chrono.rs index 6c0f8fc2a..a0c6edc42 100644 --- a/juniper/src/integrations/chrono.rs +++ b/juniper/src/integrations/chrono.rs @@ -1,300 +1,806 @@ -/*! - -# Supported types - -| Rust Type | JSON Serialization | Notes | -|-------------------------|------------------------|-------------------------------------------| -| `DateTime` | RFC3339 string | | -| `DateTime` | RFC3339 string | | -| `NaiveDate` | YYYY-MM-DD | | -| `NaiveDateTime` | float (unix timestamp) | JSON numbers (i.e. IEEE doubles) are not | -| | | precise enough for nanoseconds. | -| | | Values will be truncated to microsecond | -| | | resolution. | -| `NaiveTime` | H:M:S | Optional. Use the `scalar-naivetime` | -| | | feature. | - -*/ -#![allow(clippy::needless_lifetimes)] -use crate::{graphql_scalar, InputValue, ScalarValue, Value}; +//! GraphQL support for [`chrono`] crate types. +//! +//! # Supported types +//! +//! | Rust type | Format | GraphQL scalar | +//! |-------------------|-----------------------|-------------------| +//! | [`NaiveDate`] | `yyyy-MM-dd` | [`Date`][s1] | +//! | [`NaiveTime`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] | +//! | [`NaiveDateTime`] | `yyyy-MM-dd HH:mm:ss` | `LocalDateTime` | +//! | [`DateTime`] | [RFC 3339] string | [`DateTime`][s4] | +//! +//! [`DateTime`]: chrono::DateTime +//! [`NaiveDate`]: chrono::naive::NaiveDate +//! [`NaiveDateTime`]: chrono::naive::NaiveDateTime +//! [`NaiveTime`]: chrono::naive::NaiveTime +//! [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 + +use std::fmt; + +use chrono::{FixedOffset, TimeZone}; -#[graphql_scalar(with = date_time_fixed_offset, parse_token(String))] -type DateTimeFixedOffset = chrono::DateTime; +use crate::{graphql_scalar, InputValue, ScalarValue, Value}; -mod date_time_fixed_offset { +/// Date in the proleptic Gregorian calendar (without time zone). +/// +/// Represents a description of the date (as used for birthdays, for example). +/// It cannot represent an instant on the time-line. +/// +/// [`Date` scalar][1] compliant. +/// +/// See also [`chrono::NaiveDate`][2] for details. +/// +/// [1]: https://graphql-scalars.dev/docs/scalars/date +/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html +#[graphql_scalar( + with = date, + parse_token(String), + specified_by_url = "https://graphql-scalars.dev/docs/scalars/date", +)] +pub type Date = chrono::NaiveDate; + +mod date { use super::*; - pub(super) fn to_output(v: &DateTimeFixedOffset) -> Value { - Value::scalar(v.to_rfc3339()) + /// Format of a [`Date` scalar][1]. + /// + /// [1]: https://graphql-scalars.dev/docs/scalars/date + const FORMAT: &str = "%Y-%m-%d"; + + pub(super) fn to_output(v: &Date) -> Value + where + S: ScalarValue, + { + Value::scalar(v.format(FORMAT).to_string()) } - pub(super) fn from_input( - v: &InputValue, - ) -> Result { + pub(super) fn from_input(v: &InputValue) -> Result + where + S: ScalarValue, + { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - DateTimeFixedOffset::parse_from_rfc3339(s) - .map_err(|e| format!("Failed to parse `DateTimeFixedOffset`: {}", e)) + Date::parse_from_str(s, FORMAT).map_err(|e| format!("Invalid `Date`: {}", e)) }) } } -#[graphql_scalar(with = date_time_utc, parse_token(String))] -type DateTimeUtc = chrono::DateTime; +/// Clock time within a given date (without time zone) in `HH:mm[:ss[.SSS]]` +/// format. +/// +/// All minutes are assumed to have exactly 60 seconds; no attempt is made to +/// handle leap seconds (either positive or negative). +/// +/// [`LocalTime` scalar][1] compliant. +/// +/// See also [`chrono::NaiveTime`][2] for details. +/// +/// [1]: https://graphql-scalars.dev/docs/scalars/local-time +/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html +#[graphql_scalar( + with = local_time, + parse_token(String), + specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-time", +)] +pub type LocalTime = chrono::NaiveTime; + +mod local_time { + use chrono::Timelike as _; -mod date_time_utc { use super::*; - pub(super) fn to_output(v: &DateTimeUtc) -> Value { - Value::scalar(v.to_rfc3339()) + /// Full format of a [`LocalTime` scalar][1]. + /// + /// [1]: https://graphql-scalars.dev/docs/scalars/local-time + const FORMAT: &str = "%H:%M:%S%.3f"; + + /// Format of a [`LocalTime` scalar][1] without milliseconds. + /// + /// [1]: https://graphql-scalars.dev/docs/scalars/local-time + const FORMAT_NO_MILLIS: &str = "%H:%M:%S"; + + /// Format of a [`LocalTime` scalar][1] without seconds. + /// + /// [1]: https://graphql-scalars.dev/docs/scalars/local-time + const FORMAT_NO_SECS: &str = "%H:%M"; + + pub(super) fn to_output(v: &LocalTime) -> Value + where + S: ScalarValue, + { + Value::scalar( + if v.nanosecond() == 0 { + v.format(FORMAT_NO_MILLIS) + } else { + v.format(FORMAT) + } + .to_string(), + ) } - pub(super) fn from_input(v: &InputValue) -> Result { + pub(super) fn from_input(v: &InputValue) -> Result + where + S: ScalarValue, + { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - s.parse::() - .map_err(|e| format!("Failed to parse `DateTimeUtc`: {}", 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. + LocalTime::parse_from_str(s, FORMAT_NO_MILLIS) + .or_else(|_| LocalTime::parse_from_str(s, FORMAT_NO_SECS)) + .or_else(|_| LocalTime::parse_from_str(s, FORMAT)) + .map_err(|e| format!("Invalid `LocalTime`: {}", e)) }) } } -// 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(with = naive_date, parse_token(String))] -type NaiveDate = chrono::NaiveDate; +/// Combined date and time (without time zone) in `yyyy-MM-dd HH:mm:ss` format. +/// +/// See also [`chrono::NaiveDateTime`][1] for details. +/// +/// [1]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html +#[graphql_scalar(with = local_date_time, parse_token(String))] +pub type LocalDateTime = chrono::NaiveDateTime; -mod naive_date { +mod local_date_time { use super::*; - pub(super) fn to_output(v: &NaiveDate) -> Value { - Value::scalar(v.format("%Y-%m-%d").to_string()) + /// Format of a `LocalDateTime` scalar. + const FORMAT: &str = "%Y-%m-%d %H:%M:%S"; + + pub(super) fn to_output(v: &LocalDateTime) -> Value + where + S: ScalarValue, + { + Value::scalar(v.format(FORMAT).to_string()) } - pub(super) fn from_input(v: &InputValue) -> Result { + pub(super) fn from_input(v: &InputValue) -> Result + where + S: ScalarValue, + { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - NaiveDate::parse_from_str(s, "%Y-%m-%d") - .map_err(|e| format!("Failed to parse `NaiveDate`: {}", e)) + LocalDateTime::parse_from_str(s, FORMAT) + .map_err(|e| format!("Invalid `LocalDateTime`: {}", e)) }) } } -#[cfg(feature = "scalar-naivetime")] -#[graphql_scalar(with = naive_time, parse_token(String))] -type NaiveTime = chrono::NaiveTime; +/// Combined date and time (with time zone) in [RFC 3339][0] format. +/// +/// Represents a description of an exact instant on the time-line (such as the +/// instant that a user account was created). +/// +/// [`DateTime` scalar][1] compliant. +/// +/// See also [`chrono::DateTime`][2] for details. +/// +/// [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5 +/// [1]: https://graphql-scalars.dev/docs/scalars/date-time +/// [2]: https://docs.rs/chrono/latest/chrono/struct.DateTime.html +#[graphql_scalar( + with = date_time, + parse_token(String), + where( + Tz: TimeZone + FromFixedOffset, + Tz::Offset: fmt::Display, + ) +)] +pub type DateTime = chrono::DateTime; + +mod date_time { + use chrono::{SecondsFormat, Utc}; -#[cfg(feature = "scalar-naivetime")] -mod naive_time { use super::*; - pub(super) fn to_output(v: &NaiveTime) -> Value { - Value::scalar(v.format("%H:%M:%S").to_string()) + pub(super) fn to_output(v: &DateTime) -> Value + where + S: ScalarValue, + Tz: chrono::TimeZone, + Tz::Offset: fmt::Display, + { + Value::scalar( + v.with_timezone(&Utc) + .to_rfc3339_opts(SecondsFormat::AutoSi, true), + ) } - pub(super) fn from_input(v: &InputValue) -> Result { + pub(super) fn from_input(v: &InputValue) -> Result, String> + where + S: ScalarValue, + Tz: TimeZone + FromFixedOffset, + { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - NaiveTime::parse_from_str(s, "%H:%M:%S") - .map_err(|e| format!("Failed to parse `NaiveTime`: {}", e)) + DateTime::::parse_from_rfc3339(s) + .map_err(|e| format!("Invalid `DateTime`: {}", e)) + .map(FromFixedOffset::from_fixed_offset) }) } } -// JSON numbers (i.e. IEEE doubles) are not precise enough for nanosecond -// datetimes. Values will be truncated to microsecond resolution. -#[graphql_scalar(with = naive_date_time, parse_token(f64))] -type NaiveDateTime = chrono::NaiveDateTime; +/// Trait allowing to implement a custom [`TimeZone`], which preserves its +/// [`TimeZone`] information when parsed in a [`DateTime`] GraphQL scalar. +/// +/// # Example +/// +/// Creating a custom [CET] [`TimeZone`] using [`chrono-tz`] crate. This is +/// required because [`chrono-tz`] uses enum to represent all [`TimeZone`]s, so +/// we have no knowledge of the concrete underlying [`TimeZone`] on the type +/// level. +/// +/// ```rust +/// # use chrono::{FixedOffset, TimeZone}; +/// # use juniper::{ +/// # integrations::chrono::{FromFixedOffset, DateTime}, +/// # graphql_object, +/// # }; +/// # +/// #[derive(Clone, Copy)] +/// struct CET; +/// +/// impl TimeZone for CET { +/// type Offset = ::Offset; +/// +/// fn from_offset(_: &Self::Offset) -> Self { +/// CET +/// } +/// +/// fn offset_from_local_date( +/// &self, +/// local: &chrono::NaiveDate, +/// ) -> chrono::LocalResult { +/// chrono_tz::CET.offset_from_local_date(local) +/// } +/// +/// fn offset_from_local_datetime( +/// &self, +/// local: &chrono::NaiveDateTime, +/// ) -> chrono::LocalResult { +/// chrono_tz::CET.offset_from_local_datetime(local) +/// } +/// +/// fn offset_from_utc_date(&self, utc: &chrono::NaiveDate) -> Self::Offset { +/// chrono_tz::CET.offset_from_utc_date(utc) +/// } +/// +/// fn offset_from_utc_datetime(&self, utc: &chrono::NaiveDateTime) -> Self::Offset { +/// chrono_tz::CET.offset_from_utc_datetime(utc) +/// } +/// } +/// +/// impl FromFixedOffset for CET { +/// fn from_fixed_offset(dt: DateTime) -> DateTime { +/// dt.with_timezone(&CET) +/// } +/// } +/// +/// struct Root; +/// +/// #[graphql_object] +/// impl Root { +/// fn pass_date_time(dt: DateTime) -> DateTime { +/// dt +/// } +/// } +/// ``` +/// +/// [`chrono-tz`]: chrono_tz +/// [CET]: https://en.wikipedia.org/wiki/Central_European_Time +pub trait FromFixedOffset: TimeZone { + /// Converts the given [`DateTime`]`<`[`FixedOffset`]`>` into a + /// [`DateTime`]``. + fn from_fixed_offset(dt: DateTime) -> DateTime; +} -mod naive_date_time { - use super::*; +impl FromFixedOffset for FixedOffset { + fn from_fixed_offset(dt: DateTime) -> DateTime { + dt + } +} - pub(super) fn to_output(v: &NaiveDateTime) -> Value { - Value::scalar(v.timestamp() as f64) +impl FromFixedOffset for chrono::Utc { + fn from_fixed_offset(dt: DateTime) -> DateTime { + dt.into() } +} - pub(super) fn from_input(v: &InputValue) -> Result { - v.as_float_value() - .ok_or_else(|| format!("Expected `Float`, found: {}", v)) - .and_then(|f| { - let secs = f as i64; - NaiveDateTime::from_timestamp_opt(secs, 0) - .ok_or_else(|| format!("Out-of-range number of seconds: {}", secs)) - }) +#[cfg(feature = "chrono-clock")] +impl FromFixedOffset for chrono::Local { + fn from_fixed_offset(dt: DateTime) -> DateTime { + dt.into() + } +} + +#[cfg(feature = "chrono-tz")] +impl FromFixedOffset for chrono_tz::Tz { + fn from_fixed_offset(dt: DateTime) -> DateTime { + dt.with_timezone(&chrono_tz::UTC) } } #[cfg(test)] -mod test { - use chrono::prelude::*; +mod date_test { + use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _}; - use crate::{graphql_input_value, FromInputValue, InputValue}; + use super::Date; - fn datetime_fixedoffset_test(raw: &'static str) { - let input: InputValue = graphql_input_value!((raw)); + #[test] + fn parses_correct_input() { + for (raw, expected) in [ + ("1996-12-19", Date::from_ymd(1996, 12, 19)), + ("1564-01-30", Date::from_ymd(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: DateTime = FromInputValue::from_input_value(&input).unwrap(); - let expected = DateTime::parse_from_rfc3339(raw).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::from_ymd(1996, 12, 19), + graphql_input_value!("1996-12-19"), + ), + ( + Date::from_ymd(1564, 01, 30), + graphql_input_value!("1564-01-30"), + ), + ( + Date::from_ymd(2020, 01, 01), + 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 crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _}; + + use super::LocalTime; #[test] - fn datetime_fixedoffset_from_input() { - datetime_fixedoffset_test("2014-11-28T21:00:09+09:00"); + fn parses_correct_input() { + for (raw, expected) in [ + ("14:23:43", LocalTime::from_hms(14, 23, 43)), + ("14:00:00", LocalTime::from_hms(14, 00, 00)), + ("14:00", LocalTime::from_hms(14, 00, 00)), + ("14:32", LocalTime::from_hms(14, 32, 00)), + ("14:00:00.000", LocalTime::from_hms(14, 00, 00)), + ("14:23:43.345", LocalTime::from_hms_milli(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 datetime_fixedoffset_from_input_with_z_timezone() { - datetime_fixedoffset_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."), + 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 datetime_fixedoffset_from_input_with_fractional_seconds() { - datetime_fixedoffset_test("2014-11-28T21:00:09.05+09:00"); + fn formats_correctly() { + for (val, expected) in [ + ( + LocalTime::from_hms_micro(1, 2, 3, 4005), + graphql_input_value!("01:02:03.004"), + ), + ( + LocalTime::from_hms(0, 0, 0), + graphql_input_value!("00:00:00"), + ), + ( + LocalTime::from_hms(12, 0, 0), + graphql_input_value!("12:00:00"), + ), + ( + LocalTime::from_hms(1, 2, 3), + graphql_input_value!("01:02:03"), + ), + ] { + let actual: InputValue = val.to_input_value(); + + assert_eq!(actual, expected, "on value: {}", val); + } } +} - fn datetime_utc_test(raw: &'static str) { - let input: InputValue = graphql_input_value!((raw)); +#[cfg(test)] +mod local_date_time_test { + use chrono::naive::{NaiveDate, NaiveTime}; - let parsed: DateTime = FromInputValue::from_input_value(&input).unwrap(); - let expected = DateTime::parse_from_rfc3339(raw) - .unwrap() - .with_timezone(&Utc); + use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _}; - assert_eq!(parsed, expected); - } + use super::LocalDateTime; #[test] - fn datetime_utc_from_input() { - datetime_utc_test("2014-11-28T21:00:09+09:00") + fn parses_correct_input() { + for (raw, expected) in [ + ( + "1996-12-19 14:23:43", + LocalDateTime::new( + NaiveDate::from_ymd(1996, 12, 19), + NaiveTime::from_hms(14, 23, 43), + ), + ), + ( + "1564-01-30 14:00:00", + LocalDateTime::new( + NaiveDate::from_ymd(1564, 1, 30), + NaiveTime::from_hms(14, 00, 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 datetime_utc_from_input_with_z_timezone() { - datetime_utc_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!("56:34:22.000"), + 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:"), + 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 datetime_utc_from_input_with_fractional_seconds() { - datetime_utc_test("2014-11-28T21:00:09.005+09:00"); + fn formats_correctly() { + for (val, expected) in [ + ( + LocalDateTime::new( + NaiveDate::from_ymd(1996, 12, 19), + NaiveTime::from_hms(0, 0, 0), + ), + graphql_input_value!("1996-12-19 00:00:00"), + ), + ( + LocalDateTime::new( + NaiveDate::from_ymd(1564, 1, 30), + NaiveTime::from_hms(14, 0, 0), + ), + graphql_input_value!("1564-01-30 14:00:00"), + ), + ] { + let actual: InputValue = val.to_input_value(); + + assert_eq!(actual, expected, "on value: {}", val); + } } +} - #[test] - fn naivedate_from_input() { - let input: InputValue = graphql_input_value!("1996-12-19"); - let y = 1996; - let m = 12; - let d = 19; +#[cfg(test)] +mod date_time_test { + use chrono::{ + naive::{NaiveDate, NaiveDateTime, NaiveTime}, + FixedOffset, + }; - let parsed: NaiveDate = FromInputValue::from_input_value(&input).unwrap(); - let expected = NaiveDate::from_ymd(y, m, d); + use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _}; - assert_eq!(parsed, expected); + use super::DateTime; - assert_eq!(parsed.year(), y); - assert_eq!(parsed.month(), m); - assert_eq!(parsed.day(), d); + #[test] + fn parses_correct_input() { + for (raw, expected) in [ + ( + "2014-11-28T21:00:09+09:00", + DateTime::::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd(2014, 11, 28), + NaiveTime::from_hms(12, 0, 9), + ), + FixedOffset::east(9 * 3600), + ), + ), + ( + "2014-11-28T21:00:09Z", + DateTime::::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd(2014, 11, 28), + NaiveTime::from_hms(21, 0, 9), + ), + FixedOffset::east(0), + ), + ), + ( + "2014-11-28T21:00:09+00:00", + DateTime::::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd(2014, 11, 28), + NaiveTime::from_hms(21, 0, 9), + ), + FixedOffset::east(0), + ), + ), + ( + "2014-11-28T21:00:09.05+09:00", + DateTime::::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd(2014, 11, 28), + NaiveTime::from_hms_milli(12, 0, 9, 50), + ), + FixedOffset::east(0), + ), + ), + ] { + 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 naivetime_from_input() { - let input: InputValue = graphql_input_value!("21:12:19"); - let [h, m, s] = [21, 12, 19]; - let parsed: NaiveTime = FromInputValue::from_input_value(&input).unwrap(); - let expected = NaiveTime::from_hms(h, m, s); - 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 naivedatetime_from_input() { - let raw = 1_000_000_000_f64; - let input: InputValue = graphql_input_value!((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); + fn formats_correctly() { + for (val, expected) in [ + ( + DateTime::::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd(1996, 12, 19), + NaiveTime::from_hms(0, 0, 0), + ), + FixedOffset::east(0), + ), + graphql_input_value!("1996-12-19T00:00:00Z"), + ), + ( + DateTime::::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd(1564, 1, 30), + NaiveTime::from_hms_milli(5, 0, 0, 123), + ), + FixedOffset::east(9 * 3600), + ), + graphql_input_value!("1564-01-30T05:00:00.123Z"), + ), + ] { + let actual: InputValue = val.to_input_value(); + + assert_eq!(actual, expected, "on value: {}", val); + } } } #[cfg(test)] mod integration_test { - use chrono::{prelude::*, Utc}; - 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, FixedOffset, FromFixedOffset, LocalDateTime, LocalTime, TimeZone}; + #[tokio::test] - async fn test_serialization() { - struct Root; + async fn serializes() { + #[derive(Clone, Copy)] + struct CET; - #[graphql_object] - #[cfg(feature = "scalar-naivetime")] - impl Root { - fn example_naive_date() -> NaiveDate { - NaiveDate::from_ymd(2015, 3, 14) + impl TimeZone for CET { + type Offset = ::Offset; + + fn from_offset(_: &Self::Offset) -> Self { + CET } - fn example_naive_date_time() -> NaiveDateTime { - NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11) + + fn offset_from_local_date( + &self, + local: &chrono::NaiveDate, + ) -> chrono::LocalResult { + chrono_tz::CET.offset_from_local_date(local) + } + + fn offset_from_local_datetime( + &self, + local: &chrono::NaiveDateTime, + ) -> chrono::LocalResult { + chrono_tz::CET.offset_from_local_datetime(local) } - fn example_naive_time() -> NaiveTime { - NaiveTime::from_hms(16, 7, 8) + + fn offset_from_utc_date(&self, utc: &chrono::NaiveDate) -> Self::Offset { + chrono_tz::CET.offset_from_utc_date(utc) } - fn example_date_time_fixed_offset() -> DateTime { - DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap() + + fn offset_from_utc_datetime(&self, utc: &chrono::NaiveDateTime) -> Self::Offset { + chrono_tz::CET.offset_from_utc_datetime(utc) } - fn example_date_time_utc() -> DateTime { - Utc.timestamp(61, 0) + } + + impl FromFixedOffset for CET { + fn from_fixed_offset(dt: DateTime) -> DateTime { + dt.with_timezone(&CET) } } + struct Root; + #[graphql_object] - #[cfg(not(feature = "scalar-naivetime"))] impl Root { - fn example_naive_date() -> NaiveDate { - NaiveDate::from_ymd(2015, 3, 14) + fn date() -> Date { + Date::from_ymd(2015, 3, 14) } - fn example_naive_date_time() -> NaiveDateTime { - NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11) + + fn local_time() -> LocalTime { + LocalTime::from_hms(16, 7, 8) } - fn example_date_time_fixed_offset() -> DateTime { - DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap() + + fn local_date_time() -> LocalDateTime { + LocalDateTime::new(Date::from_ymd(2016, 7, 8), LocalTime::from_hms(9, 10, 11)) } - fn example_date_time_utc() -> DateTime { - Utc.timestamp(61, 0) + + fn date_time() -> DateTime { + DateTime::from_utc( + LocalDateTime::new( + Date::from_ymd(1996, 12, 20), + LocalTime::from_hms(0, 39, 57), + ), + chrono::Utc, + ) } - } - #[cfg(feature = "scalar-naivetime")] - let doc = r#"{ - exampleNaiveDate, - exampleNaiveDateTime, - exampleNaiveTime, - exampleDateTimeFixedOffset, - exampleDateTimeUtc, - }"#; + fn pass_date_time(dt: DateTime) -> DateTime { + dt + } + + fn transform_date_time(dt: DateTime) -> DateTime { + dt.with_timezone(&chrono::Utc) + } + } - #[cfg(not(feature = "scalar-naivetime"))] - let doc = r#"{ - exampleNaiveDate, - exampleNaiveDateTime, - exampleDateTimeFixedOffset, - exampleDateTimeUtc, + const DOC: &str = r#"{ + date + localTime + localDateTime + dateTime, + passDateTime(dt: "2014-11-28T21:00:09+09:00") + transformDateTime(dt: "2014-11-28T21:00:09+09:00") }"#; let schema = RootNode::new( @@ -303,32 +809,19 @@ mod integration_test { 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!({ - "exampleNaiveDate": "2015-03-14", - "exampleNaiveDateTime": 1_467_969_011.0, - "exampleNaiveTime": "16:07:08", - "exampleDateTimeFixedOffset": "1996-12-19T16:39:57-08:00", - "exampleDateTimeUtc": "1970-01-01T00:01:01+00:00", - }), - ); - #[cfg(not(feature = "scalar-naivetime"))] assert_eq!( - result, - graphql_value!({ - "exampleNaiveDate": "2015-03-14", - "exampleNaiveDateTime": 1_467_969_011.0, - "exampleDateTimeFixedOffset": "1996-12-19T16:39:57-08:00", - "exampleDateTimeUtc": "1970-01-01T00:01:01+00: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-20T00:39:57Z", + "passDateTime": "2014-11-28T12:00:09Z", + "transformDateTime": "2014-11-28T12:00:09Z", + }), + vec![], + )), ); } } diff --git a/juniper/src/integrations/chrono_tz.rs b/juniper/src/integrations/chrono_tz.rs index cf7621392..b6fa57321 100644 --- a/juniper/src/integrations/chrono_tz.rs +++ b/juniper/src/integrations/chrono_tz.rs @@ -1,38 +1,57 @@ -//! [`Tz`] (timezone) scalar implementation, represented by its [IANA database][1] name. +//! GraphQL support for [`chrono-tz`] crate types. //! +//! # Supported types +//! +//! | Rust type | Format | GraphQL scalar | +//! |-----------|--------------------|----------------| +//! | [`Tz`] | [IANA database][1] | `TimeZone` | +//! +//! [`chrono-tz`]: chrono_tz //! [`Tz`]: chrono_tz::Tz //! [1]: http://www.iana.org/time-zones use crate::{graphql_scalar, InputValue, ScalarValue, Value}; +/// Timezone based on [`IANA` database][1]. +/// +/// See ["List of tz database time zones"][2] `TZ database name` column for +/// available names. +/// +/// See also [`chrono_tz::Tz`][3] for detals. +/// +/// [1]: https://www.iana.org/time-zones +/// [2]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +/// [3]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html #[graphql_scalar(with = tz, parse_token(String))] -type Tz = chrono_tz::Tz; +pub type TimeZone = chrono_tz::Tz; mod tz { use super::*; - pub(super) fn to_output(v: &Tz) -> Value { + pub(super) fn to_output(v: &TimeZone) -> Value { Value::scalar(v.name().to_owned()) } - pub(super) fn from_input(v: &InputValue) -> Result { + pub(super) fn from_input(v: &InputValue) -> Result { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - s.parse::() - .map_err(|e| format!("Failed to parse `Tz`: {}", e)) + s.parse::() + .map_err(|e| format!("Failed to parse `TimeZone`: {}", e)) }) } } #[cfg(test)] mod test { - mod from_input { - use chrono_tz::Tz; + use super::TimeZone; + + mod from_input_value { + use super::TimeZone; use crate::{graphql_input_value, FromInputValue, InputValue, IntoFieldError}; - fn tz_input_test(raw: &'static str, expected: Result) { + fn tz_input_test(raw: &'static str, expected: Result) { let input: InputValue = graphql_input_value!((raw)); let parsed = FromInputValue::from_input_value(&input); @@ -59,7 +78,7 @@ mod test { fn forward_slash() { tz_input_test( "Abc/Xyz", - Err("Failed to parse `Tz`: received invalid timezone"), + Err("Failed to parse `TimeZone`: received invalid timezone"), ); } @@ -67,7 +86,7 @@ mod test { fn number() { tz_input_test( "8086", - Err("Failed to parse `Tz`: received invalid timezone"), + Err("Failed to parse `TimeZone`: received invalid timezone"), ); } @@ -75,7 +94,7 @@ mod test { fn no_forward_slash() { tz_input_test( "AbcXyz", - Err("Failed to parse `Tz`: received invalid timezone"), + Err("Failed to parse `TimeZone`: received invalid timezone"), ); } } diff --git a/juniper/src/integrations/time.rs b/juniper/src/integrations/time.rs index 2b1e874a4..e05bf535c 100644 --- a/juniper/src/integrations/time.rs +++ b/juniper/src/integrations/time.rs @@ -28,16 +28,11 @@ use time::{ use crate::{graphql_scalar, InputValue, ScalarValue, Value}; -/// Format of a [`Date` scalar][1]. -/// -/// [1]: https://graphql-scalars.dev/docs/scalars/date -const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); - /// Date in the proleptic Gregorian calendar (without time zone). /// /// Represents a description of the date (as used for birthdays, for example). /// It cannot represent an instant on the time-line. -/// +/// /// [`Date` scalar][1] compliant. /// /// See also [`time::Date`][2] for details. @@ -54,9 +49,14 @@ pub type Date = time::Date; mod date { use super::*; + /// Format of a [`Date` scalar][1]. + /// + /// [1]: https://graphql-scalars.dev/docs/scalars/date + const FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); + pub(super) fn to_output(v: &Date) -> Value { Value::scalar( - v.format(DATE_FORMAT) + v.format(FORMAT) .unwrap_or_else(|e| panic!("Failed to format `Date`: {}", e)), ) } @@ -64,27 +64,10 @@ mod date { pub(super) fn from_input(v: &InputValue) -> Result { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) - .and_then(|s| Date::parse(s, DATE_FORMAT).map_err(|e| format!("Invalid `Date`: {}", e))) + .and_then(|s| Date::parse(s, FORMAT).map_err(|e| format!("Invalid `Date`: {}", e))) } } -/// 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]"); - /// Clock time within a given date (without time zone) in `HH:mm[:ss[.SSS]]` /// format. /// @@ -103,12 +86,28 @@ pub type LocalTime = time::Time; mod local_time { use super::*; + /// Full format of a [`LocalTime` scalar][1]. + /// + /// [1]: https://graphql-scalars.dev/docs/scalars/local-time + const 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 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 FORMAT_NO_SECS: &[FormatItem<'_>] = format_description!("[hour]:[minute]"); + pub(super) fn to_output(v: &LocalTime) -> Value { Value::scalar( if v.millisecond() == 0 { - v.format(LOCAL_TIME_FORMAT_NO_MILLIS) + v.format(FORMAT_NO_MILLIS) } else { - v.format(LOCAL_TIME_FORMAT) + v.format(FORMAT) } .unwrap_or_else(|e| panic!("Failed to format `LocalTime`: {}", e)), ) @@ -121,18 +120,14 @@ mod local_time { // 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. - LocalTime::parse(s, LOCAL_TIME_FORMAT_NO_MILLIS) - .or_else(|_| LocalTime::parse(s, LOCAL_TIME_FORMAT_NO_SECS)) - .or_else(|_| LocalTime::parse(s, LOCAL_TIME_FORMAT)) + LocalTime::parse(s, FORMAT_NO_MILLIS) + .or_else(|_| LocalTime::parse(s, FORMAT_NO_SECS)) + .or_else(|_| LocalTime::parse(s, FORMAT)) .map_err(|e| format!("Invalid `LocalTime`: {}", e)) }) } } -/// Format of a [`LocalDateTime`] scalar. -const LOCAL_DATE_TIME_FORMAT: &[FormatItem<'_>] = - format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); - /// Combined date and time (without time zone) in `yyyy-MM-dd HH:mm:ss` format. /// /// See also [`time::PrimitiveDateTime`][2] for details. @@ -144,9 +139,13 @@ pub type LocalDateTime = time::PrimitiveDateTime; mod local_date_time { use super::*; + /// Format of a [`LocalDateTime`] scalar. + const FORMAT: &[FormatItem<'_>] = + format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); + pub(super) fn to_output(v: &LocalDateTime) -> Value { Value::scalar( - v.format(LOCAL_DATE_TIME_FORMAT) + v.format(FORMAT) .unwrap_or_else(|e| panic!("Failed to format `LocalDateTime`: {}", e)), ) } @@ -155,7 +154,7 @@ mod local_date_time { v.as_string_value() .ok_or_else(|| format!("Expected `String`, found: {}", v)) .and_then(|s| { - LocalDateTime::parse(s, LOCAL_DATE_TIME_FORMAT) + LocalDateTime::parse(s, FORMAT) .map_err(|e| format!("Invalid `LocalDateTime`: {}", e)) }) } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index ff430e325..d26f8dcb7 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -59,6 +59,7 @@ your Schemas automatically. * [uuid][uuid] * [url][url] * [chrono][chrono] +* [chrono-tz][chrono-tz] * [time][time] * [bson][bson] @@ -88,6 +89,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 +[chrono-tz]: https://crates.io/crates/chrono-tz [time]: https://crates.io/crates/time [bson]: https://crates.io/crates/bson diff --git a/juniper_actix/Cargo.toml b/juniper_actix/Cargo.toml index 862fbbb2b..6e2ef2dca 100644 --- a/juniper_actix/Cargo.toml +++ b/juniper_actix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "juniper_actix" -version = "0.4.0" +version = "0.5.0" edition = "2018" authors = ["Jordao Rosario "] description = "Juniper GraphQL integration with Actix" @@ -13,10 +13,10 @@ subscriptions = ["juniper_graphql_ws", "tokio"] [dependencies] actix = "0.12" -actix-http = "3.0.0-beta.15" +actix-http = "3.0" http = "0.2.4" -actix-web = "4.0.0-beta.14" -actix-web-actors = "4.0.0-beta.8" +actix-web = "4.0" +actix-web-actors = "=4.0.0" juniper = { version = "0.16.0-dev", path = "../juniper", default-features = false } juniper_graphql_ws = { version = "0.3.0", path = "../juniper_graphql_ws", optional = true } @@ -30,11 +30,11 @@ tokio = { version = "1.0", features = ["sync"], optional = true } [dev-dependencies] actix-rt = "2" -actix-cors = "0.6.0-beta.6" -actix-identity = "0.4.0-beta.5" +actix-cors = "0.6" +actix-identity = "0.4" tokio = "1.0" async-stream = "0.3" -actix-test = "0.1.0-beta.8" +actix-test = "=0.1.0-beta.13" juniper = { version = "0.16.0-dev", path = "../juniper", features = ["expose-test-schema"] }