Add Jiff date/time types Zoned
/TimeZone
(#1278)
* Enable support for Jiff's Zoned type * Enable support for Jiff's TimeZone type * Remove unnecessary feature flag `jiff-tz` * Avoid panic and allow offset-only time zones to be returned * Add ZonedDateTime and TimeZone to integration test * Split TimeZone into TimeZone and UTCOffset for Jiff integration * Fix capitalization of `UtcOffset` * Provide TimeZoneOrUtcOffset integration for jiff::tz::TimeZone * Adjust documentation for newtypes and `TimeZoneOrUtcOffset` * Use `jiff::tz::Offset` directly in `UtcOffset` newtype * Remove unnecessary newtype for `UtcOffset` scalar * Fix parsing of fixed offsets from `TimeZoneOrUtcOffset` scalar * Add unrelated missing link to books page on scalars * State necessity for newtype in scalars table in book * Remove redundant `alloc` feature, is included in `std` * Implement `Display` for TimeZone newtype * Rename error enum variant to clarify meaning * Use concise wording in description of TimeZone Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com> * Revert extra notes column in scalar table * Add link to documentation of newtype `TimeZone` * Remove unused duplicate link reference * Sort links and group links from the same crate together --------- Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
This commit is contained in:
parent
eb2079b2bd
commit
fb1531f90c
4 changed files with 848 additions and 42 deletions
|
@ -386,7 +386,7 @@ mod date_scalar {
|
||||||
[Juniper] provides out-of-the-box [GraphQL scalar][0] implementations for some very common [Rust] crates. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled [Cargo feature].
|
[Juniper] provides out-of-the-box [GraphQL scalar][0] implementations for some very common [Rust] crates. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled [Cargo feature].
|
||||||
|
|
||||||
| [Rust] type | [GraphQL] scalar | [Cargo feature] |
|
| [Rust] type | [GraphQL] scalar | [Cargo feature] |
|
||||||
|-----------------------------|-------------------|------------------|
|
|-----------------------------|-----------------------|------------------|
|
||||||
| [`bigdecimal::BigDecimal`] | `BigDecimal` | [`bigdecimal`] |
|
| [`bigdecimal::BigDecimal`] | `BigDecimal` | [`bigdecimal`] |
|
||||||
| [`bson::oid::ObjectId`] | [`ObjectID`] | [`bson`] |
|
| [`bson::oid::ObjectId`] | [`ObjectID`] | [`bson`] |
|
||||||
| [`bson::DateTime`] | [`DateTime`] | [`bson`] |
|
| [`bson::DateTime`] | [`DateTime`] | [`bson`] |
|
||||||
|
@ -400,6 +400,10 @@ mod date_scalar {
|
||||||
| [`jiff::civil::Time`] | [`LocalTime`] | [`jiff`] |
|
| [`jiff::civil::Time`] | [`LocalTime`] | [`jiff`] |
|
||||||
| [`jiff::civil::DateTime`] | [`LocalDateTime`] | [`jiff`] |
|
| [`jiff::civil::DateTime`] | [`LocalDateTime`] | [`jiff`] |
|
||||||
| [`jiff::Timestamp`] | [`DateTime`] | [`jiff`] |
|
| [`jiff::Timestamp`] | [`DateTime`] | [`jiff`] |
|
||||||
|
| [`jiff::Zoned`] | `ZonedDateTime` | [`jiff`] |
|
||||||
|
| [`jiff::tz::TimeZone`] | `TimeZoneOrUtcOffset` | [`jiff`] |
|
||||||
|
| [`jiff::tz::TimeZone`] | [`TimeZone`] [^n1] | [`jiff`] |
|
||||||
|
| [`jiff::tz::Offset`] | [`UtcOffset`] | [`jiff`] |
|
||||||
| [`jiff::Span`] | [`Duration`] | [`jiff`] |
|
| [`jiff::Span`] | [`Duration`] | [`jiff`] |
|
||||||
| [`time::Date`] | [`LocalDate`] | [`time`] |
|
| [`time::Date`] | [`LocalDate`] | [`time`] |
|
||||||
| [`time::Time`] | [`LocalTime`] | [`time`] |
|
| [`time::Time`] | [`LocalTime`] | [`time`] |
|
||||||
|
@ -409,6 +413,8 @@ mod date_scalar {
|
||||||
| [`url::Url`] | [`URL`] | [`url`] |
|
| [`url::Url`] | [`URL`] | [`url`] |
|
||||||
| [`uuid::Uuid`] | [`UUID`] | [`uuid`] |
|
| [`uuid::Uuid`] | [`UUID`] | [`uuid`] |
|
||||||
|
|
||||||
|
[^n1]: Conversion supported via newtype [`integrations::jiff::TimeZone`][10].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -425,7 +431,6 @@ mod date_scalar {
|
||||||
[`chrono-tz`]: https://docs.rs/chrono-tz
|
[`chrono-tz`]: https://docs.rs/chrono-tz
|
||||||
[`chrono_tz::Tz`]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
|
[`chrono_tz::Tz`]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
|
||||||
[`DateTime`]: https://graphql-scalars.dev/docs/scalars/date-time
|
[`DateTime`]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||||
[`Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html
|
|
||||||
[`Duration`]: https://graphql-scalars.dev/docs/scalars/duration
|
[`Duration`]: https://graphql-scalars.dev/docs/scalars/duration
|
||||||
[`ID`]: https://spec.graphql.org/October2021#sec-ID
|
[`ID`]: https://spec.graphql.org/October2021#sec-ID
|
||||||
[`jiff`]: https://docs.rs/jiff
|
[`jiff`]: https://docs.rs/jiff
|
||||||
|
@ -434,11 +439,15 @@ mod date_scalar {
|
||||||
[`jiff::civil::Time`]: https://docs.rs/jiff/latest/jiff/civil/struct.Time.html
|
[`jiff::civil::Time`]: https://docs.rs/jiff/latest/jiff/civil/struct.Time.html
|
||||||
[`jiff::Span`]: https://docs.rs/jiff/latest/jiff/struct.Span.html
|
[`jiff::Span`]: https://docs.rs/jiff/latest/jiff/struct.Span.html
|
||||||
[`jiff::Timestamp`]: https://docs.rs/jiff/latest/jiff/struct.Timestamp.html
|
[`jiff::Timestamp`]: https://docs.rs/jiff/latest/jiff/struct.Timestamp.html
|
||||||
|
[`jiff::tz::Offset`]: https://docs.rs/jiff/latest/jiff/tz/struct.Offset.html
|
||||||
|
[`jiff::tz::TimeZone`]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
|
||||||
|
[`jiff::Zoned`]: https://docs.rs/jiff/latest/jiff/struct.Zoned.html
|
||||||
[`LocalDate`]: https://graphql-scalars.dev/docs/scalars/local-date
|
[`LocalDate`]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
[`LocalDateTime`]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
[`LocalDateTime`]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
[`LocalTime`]: https://graphql-scalars.dev/docs/scalars/local-time
|
[`LocalTime`]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
[`ObjectID`]: https://the-guild.dev/graphql/scalars/docs/scalars/object-id
|
[`ObjectID`]: https://the-guild.dev/graphql/scalars/docs/scalars/object-id
|
||||||
[`rust_decimal`]: https://docs.rs/rust_decimal
|
[`rust_decimal`]: https://docs.rs/rust_decimal
|
||||||
|
[`rust_decimal::Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html
|
||||||
[`ScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html
|
[`ScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html
|
||||||
[`serde`]: https://docs.rs/serde
|
[`serde`]: https://docs.rs/serde
|
||||||
[`time`]: https://docs.rs/time
|
[`time`]: https://docs.rs/time
|
||||||
|
@ -472,3 +481,4 @@ mod date_scalar {
|
||||||
[7]: https://spec.graphql.org/October2021#sec-Value-Resolution
|
[7]: https://spec.graphql.org/October2021#sec-Value-Resolution
|
||||||
[8]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLScalar.html
|
[8]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLScalar.html
|
||||||
[9]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_scalar.html
|
[9]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_scalar.html
|
||||||
|
[10]: https://docs.rs/juniper/0.16.1/juniper/integrations/jiff/struct.TimeZone.html
|
||||||
|
|
|
@ -34,11 +34,14 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- [`jiff` crate] integration behind `jiff` [Cargo feature]: ([#1271], [#1270])
|
- [`jiff` crate] integration behind `jiff` [Cargo feature]: ([#1271], [#1278], [#1270])
|
||||||
- `jiff::civil::Date` as `LocalDate` scalar.
|
- `jiff::civil::Date` as `LocalDate` scalar.
|
||||||
- `jiff::civil::Time` as `LocalTime` scalar.
|
- `jiff::civil::Time` as `LocalTime` scalar.
|
||||||
- `jiff::civil::DateTime` as `LocalDateTime` scalar. ([#1275])
|
- `jiff::civil::DateTime` as `LocalDateTime` scalar. ([#1275])
|
||||||
- `jiff::Timestamp` as `DateTime` scalar.
|
- `jiff::Timestamp` as `DateTime` scalar.
|
||||||
|
- `jiff::Zoned` as `ZonedDateTime` scalar.
|
||||||
|
- `jiff::tz::TimeZone` as `TimeZoneOrUtcOffset` scalar.
|
||||||
|
- `jiff::tz::Offset` as `UtcOffset` scalar.
|
||||||
- `jiff::Span` as `Duration` scalar.
|
- `jiff::Span` as `Duration` scalar.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -51,6 +54,8 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
[#1272]: /../../pull/1272
|
[#1272]: /../../pull/1272
|
||||||
[#1275]: /../../pull/1275
|
[#1275]: /../../pull/1275
|
||||||
[#1277]: /../../pull/1277
|
[#1277]: /../../pull/1277
|
||||||
|
[#1278]: /../../pull/1278
|
||||||
|
[#1279]: /../../pull/1279
|
||||||
[#1281]: /../../pull/1281
|
[#1281]: /../../pull/1281
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ fnv = "1.0.5"
|
||||||
futures = { version = "0.3.22", features = ["alloc"], default-features = false }
|
futures = { version = "0.3.22", features = ["alloc"], default-features = false }
|
||||||
graphql-parser = { version = "0.4", optional = true }
|
graphql-parser = { version = "0.4", optional = true }
|
||||||
indexmap = { version = "2.0", features = ["serde"] }
|
indexmap = { version = "2.0", features = ["serde"] }
|
||||||
jiff = { version = "0.1.5", features = ["alloc"], default-features = false, optional = true }
|
jiff = { version = "0.1.5", features = ["std"], default-features = false, optional = true }
|
||||||
juniper_codegen = { version = "0.16.0", path = "../juniper_codegen" }
|
juniper_codegen = { version = "0.16.0", path = "../juniper_codegen" }
|
||||||
rust_decimal = { version = "1.20", default-features = false, optional = true }
|
rust_decimal = { version = "1.20", default-features = false, optional = true }
|
||||||
ryu = { version = "1.0", optional = true }
|
ryu = { version = "1.0", optional = true }
|
||||||
|
@ -78,6 +78,7 @@ void = { version = "1.0.2", optional = true }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bencher = "0.1.2"
|
bencher = "0.1.2"
|
||||||
chrono = { version = "0.4.30", features = ["alloc"], default-features = false }
|
chrono = { version = "0.4.30", features = ["alloc"], default-features = false }
|
||||||
|
jiff = { version = "0.1.5", features = ["tzdb-bundle-always"], default-features = false }
|
||||||
pretty_assertions = "1.0.0"
|
pretty_assertions = "1.0.0"
|
||||||
serde_json = "1.0.18"
|
serde_json = "1.0.18"
|
||||||
serial_test = "3.0"
|
serial_test = "3.0"
|
||||||
|
|
|
@ -3,25 +3,38 @@
|
||||||
//! # Supported types
|
//! # Supported types
|
||||||
//!
|
//!
|
||||||
//! | Rust type | Format | GraphQL scalar |
|
//! | Rust type | Format | GraphQL scalar |
|
||||||
//! |---------------------|-----------------------|-----------------------|
|
//! |---------------------------------------|-----------------------------|-----------------------|
|
||||||
//! | [`civil::Date`] | `yyyy-MM-dd` | [`LocalDate`][s1] |
|
//! | [`civil::Date`] | `yyyy-MM-dd` | [`LocalDate`][s1] |
|
||||||
//! | [`civil::Time`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] |
|
//! | [`civil::Time`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] |
|
||||||
//! | [`civil::DateTime`] | `yyyy-MM-ddTHH:mm:ss` | [`LocalDateTime`][s3] |
|
//! | [`civil::DateTime`] | `yyyy-MM-ddTHH:mm:ss` | [`LocalDateTime`][s3] |
|
||||||
//! | [`Timestamp`] | [RFC 3339] string | [`DateTime`][s4] |
|
//! | [`Timestamp`] | [RFC 3339] string | [`DateTime`][s4] |
|
||||||
//! | [`Span`] | [ISO 8601] duration | [`Duration`][s5] |
|
//! | [`Zoned`][^1] | [RFC 9557] string | `ZonedDateTime` |
|
||||||
|
//! | [`tz::TimeZone`][^1] | [IANA database][1]/`±hh:mm` | `TimeZoneOrUtcOffset` |
|
||||||
|
//! | [`tz::TimeZone`] via [`TimeZone`][^1] | [IANA database][1] | [`TimeZone`][s5] |
|
||||||
|
//! | [`tz::Offset`] | `±hh:mm` | [`UtcOffset`][s6] |
|
||||||
|
//! | [`Span`] | [ISO 8601] duration | [`Duration`][s7] |
|
||||||
//!
|
//!
|
||||||
//! # Unsupported types
|
//! [^1]: For these, crate [`jiff`] must be installed with a feature flag that provides access to
|
||||||
|
//! the Time Zone Database (e.g. by using the crate's default feature flags). See [`jiff` time zone
|
||||||
|
//! features][tz] for details.
|
||||||
//!
|
//!
|
||||||
//! [`Zoned`] is not supported because the GraphQL scalar [`DateTime`][s4] only supports time zone
|
//! # Time zone types
|
||||||
//! offsets but no IANA time zone names (as in `2024-08-10T23:14:00-04:00[America/New_York]`, cf.
|
//!
|
||||||
//! [RFC 9557]). Serializing such values would incur a loss of information with unexpected and
|
//! `tz::TimeZone` values can be IANA time zone identifiers or fixed offsets, corresponding to
|
||||||
//! subtle consequences (a fixed offset would only _seem_ to work in most cases).
|
//! GraphQL scalars [`TimeZone`][s5] and [`UtcOffset`][s6]. While `UtcOffset` can be serialized from
|
||||||
|
//! [`tz::Offset`] directly, newtype [`TimeZone`] handles serialization to `TimeZone`, with
|
||||||
|
//! [`TryFrom`] and [`Into`] implementations from and to `tz::TimeZone`.
|
||||||
|
//!
|
||||||
|
//! In addition, `tz::TimeZone` serializes to `TimeZoneOrUtcOffset` which is a GraphQL scalar that
|
||||||
|
//! contains either an IANA identifier or a fixed offset for clients that can consume both values.
|
||||||
//!
|
//!
|
||||||
//! [`civil::Date`]: jiff::civil::Date
|
//! [`civil::Date`]: jiff::civil::Date
|
||||||
//! [`civil::DateTime`]: jiff::civil::DateTime
|
//! [`civil::DateTime`]: jiff::civil::DateTime
|
||||||
//! [`civil::Time`]: jiff::civil::Time
|
//! [`civil::Time`]: jiff::civil::Time
|
||||||
//! [`Span`]: jiff::Span
|
//! [`Span`]: jiff::Span
|
||||||
//! [`Timestamp`]: jiff::Timestamp
|
//! [`Timestamp`]: jiff::Timestamp
|
||||||
|
//! [`tz::Offset`]: jiff::tz::Offset
|
||||||
|
//! [`tz::TimeZone`]: jiff::tz::TimeZone
|
||||||
//! [`Zoned`]: jiff::Zoned
|
//! [`Zoned`]: jiff::Zoned
|
||||||
//! [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601#Durations
|
//! [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601#Durations
|
||||||
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||||
|
@ -30,7 +43,13 @@
|
||||||
//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
|
//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
//! [s3]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
//! [s3]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
|
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||||
//! [s5]: https://graphql-scalars.dev/docs/scalars/duration
|
//! [s5]: https://graphql-scalars.dev/docs/scalars/time-zone
|
||||||
|
//! [s6]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
||||||
|
//! [s7]: https://graphql-scalars.dev/docs/scalars/duration
|
||||||
|
//! [tz]: https://docs.rs/jiff/latest/jiff/index.html#time-zone-features
|
||||||
|
//! [1]: http://www.iana.org/time-zones
|
||||||
|
|
||||||
|
use std::{error::Error, fmt, str};
|
||||||
|
|
||||||
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
|
|
||||||
|
@ -242,6 +261,53 @@ mod date_time {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Time zone aware instant in time.
|
||||||
|
///
|
||||||
|
/// Can be thought of as combination of the following types, all rolled into one:
|
||||||
|
///
|
||||||
|
/// - [`Timestamp`][3] for indicating precise instant in time.
|
||||||
|
/// - [`DateTime`][4] for indicating "civil" calendar date and clock time.
|
||||||
|
/// - [`TimeZone`][5] for indicating how to apply time zone transitions while performing arithmetic.
|
||||||
|
///
|
||||||
|
/// [RFC 9557][1] compliant.
|
||||||
|
///
|
||||||
|
/// See also [`jiff::Zoned`][2] for details.
|
||||||
|
///
|
||||||
|
/// [1]: https://datatracker.ietf.org/doc/html/rfc9557#section-4.1
|
||||||
|
/// [2]: https://docs.rs/jiff/latest/jiff/struct.Zoned.html
|
||||||
|
/// [3]: https://docs.rs/jiff/latest/jiff/struct.Timestamp.html
|
||||||
|
/// [4]: https://docs.rs/jiff/latest/jiff/civil/struct.DateTime.html
|
||||||
|
/// [5]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
|
||||||
|
#[graphql_scalar(
|
||||||
|
with = zoned_date_time,
|
||||||
|
parse_token(String),
|
||||||
|
)]
|
||||||
|
pub type ZonedDateTime = jiff::Zoned;
|
||||||
|
|
||||||
|
mod zoned_date_time {
|
||||||
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(super) fn to_output<S>(v: &ZonedDateTime) -> Value<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
Value::scalar(v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<ZonedDateTime, String>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
v.as_string_value()
|
||||||
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
|
.and_then(|s| {
|
||||||
|
ZonedDateTime::from_str(s).map_err(|e| format!("Invalid `ZonedDateTime`: {e}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Span of time represented via a mixture of calendar and clock units.
|
/// Span of time represented via a mixture of calendar and clock units.
|
||||||
///
|
///
|
||||||
/// Represents a duration of time in units of years, months, weeks, days, hours, minutes, seconds,
|
/// Represents a duration of time in units of years, months, weeks, days, hours, minutes, seconds,
|
||||||
|
@ -282,6 +348,231 @@ mod duration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Representation of time zone or UTC offset.
|
||||||
|
///
|
||||||
|
/// [IANA database][1] or `±hh:mm`.
|
||||||
|
///
|
||||||
|
/// See also [`jiff::tz::TimeZone`][2] for details.
|
||||||
|
///
|
||||||
|
/// [1]: http://www.iana.org/time-zones
|
||||||
|
/// [2]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
|
||||||
|
#[graphql_scalar(
|
||||||
|
with = time_zone_or_utc_offset,
|
||||||
|
parse_token(String),
|
||||||
|
)]
|
||||||
|
pub type TimeZoneOrUtcOffset = jiff::tz::TimeZone;
|
||||||
|
|
||||||
|
mod time_zone_or_utc_offset {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Format of a `TimeZoneOrUtcOffset` scalar.
|
||||||
|
const FORMAT: &str = "%:V";
|
||||||
|
|
||||||
|
pub(super) fn to_output<S>(v: &TimeZoneOrUtcOffset) -> Value<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
Value::scalar(v.iana_name().map_or_else(
|
||||||
|
|| {
|
||||||
|
// If no IANA time zone identifier is available, fall back to displaying the time
|
||||||
|
// offset directly (using format `[+-]HH:MM[:SS]` from RFC 9557, e.g. `+05:30`).
|
||||||
|
//
|
||||||
|
// <https://github.com/graphql-rust/juniper/pull/1278#discussion_r1719161686>
|
||||||
|
jiff::Zoned::now()
|
||||||
|
.with_time_zone(v.clone())
|
||||||
|
.strftime(FORMAT)
|
||||||
|
.to_string()
|
||||||
|
},
|
||||||
|
ToOwned::to_owned,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<TimeZoneOrUtcOffset, String>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
v.as_string_value()
|
||||||
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
|
.and_then(|s| {
|
||||||
|
TimeZoneOrUtcOffset::get(s)
|
||||||
|
.map_err(TimeZoneError::InvalidTimeZone)
|
||||||
|
.or_else(|_| utc_offset::utc_offset_from_str(s).map(TimeZoneOrUtcOffset::fixed))
|
||||||
|
.map_err(|e| format!("Invalid `TimeZoneOrUtcOffset`: {e}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error while handling [`TimeZone`] value.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum TimeZoneError {
|
||||||
|
/// Identifier could not be parsed by [`tz::TimeZone::get`](jiff::tz::TimeZone::get).
|
||||||
|
InvalidTimeZone(jiff::Error),
|
||||||
|
/// GraphQL scalar [`TimeZone`] requires `tz::TimeZone` with IANA name.
|
||||||
|
MissingIanaName(jiff::tz::TimeZone),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for TimeZoneError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::InvalidTimeZone(err) => write!(f, "TimeZoneError::InvalidTimeZone({err:?})"),
|
||||||
|
Self::MissingIanaName(_value) => write!(f, "TimeZoneError::MissingIanaName(..)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TimeZoneError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::InvalidTimeZone(err) => err.fmt(f),
|
||||||
|
Self::MissingIanaName(_value) => write!(f, "missing IANA name"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for TimeZoneError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::InvalidTimeZone(err) => Some(err),
|
||||||
|
Self::MissingIanaName(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Representation of time zone.
|
||||||
|
///
|
||||||
|
/// A set of rules for determining the civil time, via an offset from UTC, in a particular
|
||||||
|
/// geographic region. In many cases, the offset in a particular time zone can vary over the course
|
||||||
|
/// of a year through transitions into and out of daylight saving time.
|
||||||
|
///
|
||||||
|
/// [`TimeZone` scalar][1] compliant.
|
||||||
|
///
|
||||||
|
/// See also [`jiff::tz::TimeZone`][2] for details.
|
||||||
|
///
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/time-zone
|
||||||
|
/// [2]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
|
||||||
|
#[graphql_scalar(
|
||||||
|
with = time_zone,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/time-zone",
|
||||||
|
)]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct TimeZone(jiff::tz::TimeZone);
|
||||||
|
|
||||||
|
impl TryFrom<jiff::tz::TimeZone> for TimeZone {
|
||||||
|
type Error = TimeZoneError;
|
||||||
|
|
||||||
|
fn try_from(value: jiff::tz::TimeZone) -> Result<Self, Self::Error> {
|
||||||
|
if value.iana_name().is_none() {
|
||||||
|
return Err(TimeZoneError::MissingIanaName(value));
|
||||||
|
}
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl str::FromStr for TimeZone {
|
||||||
|
type Err = TimeZoneError;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
let value = jiff::tz::TimeZone::get(value).map_err(TimeZoneError::InvalidTimeZone)?;
|
||||||
|
value.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TimeZone {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0
|
||||||
|
.iana_name()
|
||||||
|
// PANIC: We made sure that IANA name is available when constructing `Self`.
|
||||||
|
.unwrap_or_else(|| panic!("Failed to display `TimeZone`: no IANA name"))
|
||||||
|
.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimeZone> for jiff::tz::TimeZone {
|
||||||
|
fn from(value: TimeZone) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod time_zone {
|
||||||
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(super) fn to_output<S>(v: &TimeZone) -> Value<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
Value::scalar(v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<TimeZone, String>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
v.as_string_value()
|
||||||
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
|
.and_then(|s| TimeZone::from_str(s).map_err(|e| format!("Invalid `TimeZone`: {e}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents fixed time zone offset.
|
||||||
|
///
|
||||||
|
/// [`UtcOffset` scalar][1] compliant.
|
||||||
|
///
|
||||||
|
/// See also [`jiff::tz::Offset`][2] for details.
|
||||||
|
///
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
||||||
|
/// [2]: https://docs.rs/jiff/latest/jiff/tz/struct.Offset.html
|
||||||
|
#[graphql_scalar(
|
||||||
|
with = utc_offset,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/utc-offset",
|
||||||
|
)]
|
||||||
|
pub type UtcOffset = jiff::tz::Offset;
|
||||||
|
|
||||||
|
mod utc_offset {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Format of a [`UtcOffset` scalar][1].
|
||||||
|
///
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
||||||
|
const FORMAT: &str = "%:z";
|
||||||
|
|
||||||
|
pub(super) fn utc_offset_from_str(value: &str) -> Result<jiff::tz::Offset, jiff::Error> {
|
||||||
|
let tm = jiff::fmt::strtime::BrokenDownTime::parse(FORMAT, value)?;
|
||||||
|
let offset = tm
|
||||||
|
.offset()
|
||||||
|
.expect("successful %:z parsing guarantees offset");
|
||||||
|
Ok(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utc_offset_to_string(value: jiff::tz::Offset) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
let tm = jiff::fmt::strtime::BrokenDownTime::from(
|
||||||
|
&jiff::Zoned::now().with_time_zone(jiff::tz::TimeZone::fixed(value)),
|
||||||
|
);
|
||||||
|
tm.format(FORMAT, &mut buf).unwrap();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn to_output<S>(v: &UtcOffset) -> Value<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
Value::scalar(utc_offset_to_string(*v))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<UtcOffset, String>
|
||||||
|
where
|
||||||
|
S: ScalarValue,
|
||||||
|
{
|
||||||
|
v.as_string_value()
|
||||||
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
|
.and_then(|s| utc_offset_from_str(s).map_err(|e| format!("Invalid `UtcOffset`: {e}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod local_date_test {
|
mod local_date_test {
|
||||||
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
@ -640,6 +931,192 @@ mod date_time_test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod zoned_date_time_test {
|
||||||
|
use jiff::{civil, tz, tz::TimeZone};
|
||||||
|
|
||||||
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
|
||||||
|
use super::ZonedDateTime;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_correct_input() {
|
||||||
|
for (raw, expected) in [
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09+09:00[Asia/Tokyo]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("Asia/Tokyo").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09[America/New_York]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("America/New_York").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28 21:00:09[America/New_York]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("America/New_York").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09Z[gmt+0]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("GMT+0").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09+03:00[etc/gmt-3]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("Etc/GMT-3").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09+00:00[UTC]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("UTC").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09+02:00[+02:00]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::fixed(tz::offset(2)))
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09-11:00[-11:00]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::fixed(tz::offset(-11)))
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09.05+09:00[Asia/Tokyo]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 50_000_000)
|
||||||
|
.to_zoned(TimeZone::get("Asia/Tokyo").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28 21:00:09.05+09:00[Asia/Tokyo]",
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 50_000_000)
|
||||||
|
.to_zoned(TimeZone::get("Asia/Tokyo").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
|
let parsed = ZonedDateTime::from_input_value(&input);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
parsed.is_ok(),
|
||||||
|
"failed to parse `{raw}`: {:?}",
|
||||||
|
parsed.unwrap_err(),
|
||||||
|
);
|
||||||
|
assert_eq!(parsed.unwrap(), expected, "input: {raw}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fails_on_invalid_input() {
|
||||||
|
for input in [
|
||||||
|
graphql_input_value!("12"),
|
||||||
|
graphql_input_value!("12:"),
|
||||||
|
graphql_input_value!("56:34:22"),
|
||||||
|
graphql_input_value!("56:34:22.000"),
|
||||||
|
graphql_input_value!("1996-12-1914:23:43"),
|
||||||
|
graphql_input_value!("1996-12-19Q14: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!("2014-11-28T21:00:09Z"),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09+09:00"),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09+09:00[InvTZ]"),
|
||||||
|
graphql_input_value!(2.32),
|
||||||
|
graphql_input_value!(1),
|
||||||
|
graphql_input_value!(null),
|
||||||
|
graphql_input_value!(false),
|
||||||
|
] {
|
||||||
|
let input: InputValue = input;
|
||||||
|
let parsed = ZonedDateTime::from_input_value(&input);
|
||||||
|
|
||||||
|
assert!(parsed.is_err(), "allows input: {input:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_correctly() {
|
||||||
|
for (val, expected) in [
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(1996, 12, 19, 0, 0, 0, 0)
|
||||||
|
.to_zoned(TimeZone::get("America/New_York").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("1996-12-19T00:00:00-05:00[America/New_York]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(1964, 7, 30, 5, 0, 0, 123_000_000)
|
||||||
|
.to_zoned(TimeZone::get("America/New_York").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("1964-07-30T05:00:00.123-04:00[America/New_York]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("GMT+0").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09+00:00[GMT+0]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("Etc/GMT+3").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09-03:00[Etc/GMT+3]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::get("UTC").unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09+00:00[UTC]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::UTC)
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09+00:00[UTC]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::fixed(tz::offset(0)))
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09+00:00[UTC]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::fixed(tz::offset(2)))
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09+02:00[+02:00]"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 21, 0, 9, 0)
|
||||||
|
.to_zoned(TimeZone::fixed(tz::offset(-11)))
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("2014-11-28T21:00:09-11:00[-11:00]"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let actual: InputValue = val.to_input_value();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected, "on value: {val}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod duration_test {
|
mod duration_test {
|
||||||
use jiff::ToSpan as _;
|
use jiff::ToSpan as _;
|
||||||
|
@ -735,9 +1212,300 @@ mod duration_test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod time_zone_or_utc_offset_test {
|
||||||
|
use jiff::tz;
|
||||||
|
|
||||||
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
|
||||||
|
use super::TimeZoneOrUtcOffset;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_correct_input() {
|
||||||
|
for (raw, expected) in [
|
||||||
|
(
|
||||||
|
"Europe/London",
|
||||||
|
TimeZoneOrUtcOffset::get("Europe/London").unwrap(),
|
||||||
|
),
|
||||||
|
("Etc/GMT-3", TimeZoneOrUtcOffset::get("Etc/GMT-3").unwrap()),
|
||||||
|
(
|
||||||
|
"etc/gmt+11",
|
||||||
|
TimeZoneOrUtcOffset::get("Etc/GMT+11").unwrap(),
|
||||||
|
),
|
||||||
|
("factory", TimeZoneOrUtcOffset::get("Factory").unwrap()),
|
||||||
|
("zULU", TimeZoneOrUtcOffset::get("Zulu").unwrap()),
|
||||||
|
("UTC", TimeZoneOrUtcOffset::get("UTC").unwrap()),
|
||||||
|
(
|
||||||
|
"+00:00",
|
||||||
|
TimeZoneOrUtcOffset::try_from(tz::TimeZone::fixed(tz::offset(0))).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"+03:00",
|
||||||
|
TimeZoneOrUtcOffset::try_from(tz::TimeZone::fixed(tz::offset(3))).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"-09:00",
|
||||||
|
TimeZoneOrUtcOffset::try_from(tz::TimeZone::fixed(tz::offset(-9))).unwrap(),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
|
let parsed = TimeZoneOrUtcOffset::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!("Abc/Xyz"),
|
||||||
|
graphql_input_value!("8086"),
|
||||||
|
graphql_input_value!("AbcXyz"),
|
||||||
|
graphql_input_value!("Z"),
|
||||||
|
graphql_input_value!("i'm not even a time zone"),
|
||||||
|
graphql_input_value!(2.32),
|
||||||
|
graphql_input_value!(1),
|
||||||
|
graphql_input_value!(null),
|
||||||
|
graphql_input_value!(false),
|
||||||
|
] {
|
||||||
|
let input: InputValue = input;
|
||||||
|
let parsed = TimeZoneOrUtcOffset::from_input_value(&input);
|
||||||
|
|
||||||
|
assert!(parsed.is_err(), "allows input: {input:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_correctly() {
|
||||||
|
for (val, expected) in [
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::get("Europe/London").unwrap(),
|
||||||
|
graphql_input_value!("Europe/London"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::get("Etc/GMT-3").unwrap(),
|
||||||
|
graphql_input_value!("Etc/GMT-3"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::get("etc/gmt+11").unwrap(),
|
||||||
|
graphql_input_value!("Etc/GMT+11"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::get("Factory").unwrap(),
|
||||||
|
graphql_input_value!("Factory"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::get("zulu").unwrap(),
|
||||||
|
graphql_input_value!("Zulu"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::fixed(tz::offset(0)),
|
||||||
|
graphql_input_value!("UTC"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::get("UTC").unwrap(),
|
||||||
|
graphql_input_value!("UTC"),
|
||||||
|
),
|
||||||
|
(TimeZoneOrUtcOffset::UTC, graphql_input_value!("UTC")),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::fixed(tz::offset(2)),
|
||||||
|
graphql_input_value!("+02:00"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZoneOrUtcOffset::fixed(tz::offset(-11)),
|
||||||
|
graphql_input_value!("-11:00"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let actual: InputValue = val.to_input_value();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected, "on value: {val:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod time_zone_test {
|
||||||
|
use jiff::tz;
|
||||||
|
|
||||||
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
|
||||||
|
use super::TimeZone;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_correct_input() {
|
||||||
|
for (raw, expected) in [
|
||||||
|
(
|
||||||
|
"Europe/London",
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Europe/London").unwrap()).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Etc/GMT-3",
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Etc/GMT-3").unwrap()).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"etc/gmt+11",
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Etc/GMT+11").unwrap()).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"factory",
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Factory").unwrap()).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"zULU",
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Zulu").unwrap()).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"UTC",
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("UTC").unwrap()).unwrap(),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
|
let parsed = TimeZone::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!("Abc/Xyz"),
|
||||||
|
graphql_input_value!("8086"),
|
||||||
|
graphql_input_value!("AbcXyz"),
|
||||||
|
graphql_input_value!("-02:00"),
|
||||||
|
graphql_input_value!("+11:00"),
|
||||||
|
graphql_input_value!("Z"),
|
||||||
|
graphql_input_value!("i'm not even a time zone"),
|
||||||
|
graphql_input_value!(2.32),
|
||||||
|
graphql_input_value!(1),
|
||||||
|
graphql_input_value!(null),
|
||||||
|
graphql_input_value!(false),
|
||||||
|
] {
|
||||||
|
let input: InputValue = input;
|
||||||
|
let parsed = TimeZone::from_input_value(&input);
|
||||||
|
|
||||||
|
assert!(parsed.is_err(), "allows input: {input:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_correctly() {
|
||||||
|
for (val, expected) in [
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Europe/London").unwrap()).unwrap(),
|
||||||
|
graphql_input_value!("Europe/London"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Etc/GMT-3").unwrap()).unwrap(),
|
||||||
|
graphql_input_value!("Etc/GMT-3"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("etc/gmt+11").unwrap()).unwrap(),
|
||||||
|
graphql_input_value!("Etc/GMT+11"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("Factory").unwrap()).unwrap(),
|
||||||
|
graphql_input_value!("Factory"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("zulu").unwrap()).unwrap(),
|
||||||
|
graphql_input_value!("Zulu"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::fixed(tz::offset(0))).unwrap(),
|
||||||
|
graphql_input_value!("UTC"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::get("UTC").unwrap()).unwrap(),
|
||||||
|
graphql_input_value!("UTC"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeZone::try_from(tz::TimeZone::UTC).unwrap(),
|
||||||
|
graphql_input_value!("UTC"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let actual: InputValue = val.to_input_value();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected, "on value: {val:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod utc_offset_test {
|
||||||
|
use jiff::tz;
|
||||||
|
|
||||||
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
|
||||||
|
use super::UtcOffset;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_correct_input() {
|
||||||
|
for (raw, expected) in [
|
||||||
|
("+00:00", tz::offset(0)),
|
||||||
|
("+03:00", tz::offset(3)),
|
||||||
|
("-09:00", tz::offset(-9)),
|
||||||
|
] {
|
||||||
|
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!("Europe/London"),
|
||||||
|
graphql_input_value!("Abc/Xyz"),
|
||||||
|
graphql_input_value!("8086"),
|
||||||
|
graphql_input_value!("AbcXyz"),
|
||||||
|
graphql_input_value!("Z"),
|
||||||
|
graphql_input_value!("i'm not even a time zone"),
|
||||||
|
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 [
|
||||||
|
(tz::offset(0), graphql_input_value!("+00:00")),
|
||||||
|
(tz::offset(2), graphql_input_value!("+02:00")),
|
||||||
|
(tz::offset(-11), graphql_input_value!("-11:00")),
|
||||||
|
] {
|
||||||
|
let actual: InputValue = val.to_input_value();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected, "on value: {val:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod integration_test {
|
mod integration_test {
|
||||||
use jiff::{civil, tz::TimeZone, ToSpan as _};
|
use jiff::{civil, tz, ToSpan as _};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
execute, graphql_object, graphql_value, graphql_vars,
|
execute, graphql_object, graphql_value, graphql_vars,
|
||||||
|
@ -745,7 +1513,9 @@ mod integration_test {
|
||||||
types::scalars::{EmptyMutation, EmptySubscription},
|
types::scalars::{EmptyMutation, EmptySubscription},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{DateTime, Duration, LocalDate, LocalDateTime, LocalTime};
|
use super::{
|
||||||
|
DateTime, Duration, LocalDate, LocalDateTime, LocalTime, TimeZone, UtcOffset, ZonedDateTime,
|
||||||
|
};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serializes() {
|
async fn serializes() {
|
||||||
|
@ -767,11 +1537,25 @@ mod integration_test {
|
||||||
|
|
||||||
fn date_time() -> DateTime {
|
fn date_time() -> DateTime {
|
||||||
civil::DateTime::constant(2014, 11, 28, 12, 0, 9, 50_000_000)
|
civil::DateTime::constant(2014, 11, 28, 12, 0, 9, 50_000_000)
|
||||||
.to_zoned(TimeZone::UTC)
|
.to_zoned(tz::TimeZone::UTC)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.timestamp()
|
.timestamp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn zoned_date_time() -> ZonedDateTime {
|
||||||
|
civil::DateTime::constant(2014, 11, 28, 12, 0, 9, 50_000_000)
|
||||||
|
.to_zoned(tz::TimeZone::get("America/New_York").unwrap())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time_zone() -> TimeZone {
|
||||||
|
tz::TimeZone::get("Asia/Tokyo").unwrap().try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utc_offset() -> UtcOffset {
|
||||||
|
tz::offset(10)
|
||||||
|
}
|
||||||
|
|
||||||
fn duration() -> Duration {
|
fn duration() -> Duration {
|
||||||
1.year()
|
1.year()
|
||||||
.months(1)
|
.months(1)
|
||||||
|
@ -788,6 +1572,9 @@ mod integration_test {
|
||||||
localTime
|
localTime
|
||||||
localDateTime
|
localDateTime
|
||||||
dateTime,
|
dateTime,
|
||||||
|
zonedDateTime,
|
||||||
|
timeZone,
|
||||||
|
utcOffset,
|
||||||
duration,
|
duration,
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
|
@ -805,6 +1592,9 @@ mod integration_test {
|
||||||
"localTime": "16:07:08",
|
"localTime": "16:07:08",
|
||||||
"localDateTime": "2016-07-08T09:10:11",
|
"localDateTime": "2016-07-08T09:10:11",
|
||||||
"dateTime": "2014-11-28T12:00:09.05Z",
|
"dateTime": "2014-11-28T12:00:09.05Z",
|
||||||
|
"zonedDateTime": "2014-11-28T12:00:09.05-05:00[America/New_York]",
|
||||||
|
"timeZone": "Asia/Tokyo",
|
||||||
|
"utcOffset": "+10:00",
|
||||||
"duration": "P1y1m1dT1h1m1.1s",
|
"duration": "P1y1m1dT1h1m1.1s",
|
||||||
}),
|
}),
|
||||||
vec![],
|
vec![],
|
||||||
|
|
Loading…
Reference in a new issue