Polish jiff crate integration (#1278, #1270)

This commit is contained in:
tyranron 2024-09-10 02:28:28 +03:00
parent 834b7373b9
commit 102710558a
No known key found for this signature in database
GPG key ID: 762E144FB230A4F0
3 changed files with 74 additions and 71 deletions

View file

@ -402,7 +402,7 @@ mod date_scalar {
| [`jiff::Timestamp`] | [`DateTime`] | [`jiff`] | | [`jiff::Timestamp`] | [`DateTime`] | [`jiff`] |
| [`jiff::Zoned`] | `ZonedDateTime` | [`jiff`] | | [`jiff::Zoned`] | `ZonedDateTime` | [`jiff`] |
| [`jiff::tz::TimeZone`] | `TimeZoneOrUtcOffset` | [`jiff`] | | [`jiff::tz::TimeZone`] | `TimeZoneOrUtcOffset` | [`jiff`] |
| [`jiff::tz::TimeZone`] | [`TimeZone`] [^n1] | [`jiff`] | | [`jiff::tz::TimeZone`] via [`juniper::integrations::jiff::TimeZone`] | [`TimeZone`] | [`jiff`] |
| [`jiff::tz::Offset`] | [`UtcOffset`] | [`jiff`] | | [`jiff::tz::Offset`] | [`UtcOffset`] | [`jiff`] |
| [`jiff::Span`] | [`Duration`] | [`jiff`] | | [`jiff::Span`] | [`Duration`] | [`jiff`] |
| [`time::Date`] | [`LocalDate`] | [`time`] | | [`time::Date`] | [`LocalDate`] | [`time`] |
@ -413,8 +413,6 @@ 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].
@ -442,6 +440,7 @@ mod date_scalar {
[`jiff::tz::Offset`]: https://docs.rs/jiff/latest/jiff/tz/struct.Offset.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::tz::TimeZone`]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
[`jiff::Zoned`]: https://docs.rs/jiff/latest/jiff/struct.Zoned.html [`jiff::Zoned`]: https://docs.rs/jiff/latest/jiff/struct.Zoned.html
[`juniper::integrations::jiff::TimeZone`]: https://docs.rs/juniper/0.16.1/juniper/integrations/jiff/struct.TimeZone.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
@ -481,4 +480,3 @@ 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

View file

@ -40,7 +40,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- `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::Zoned` as `ZonedDateTime` scalar.
- `jiff::tz::TimeZone` as `TimeZoneOrUtcOffset` scalar. - `jiff::tz::TimeZone` as `TimeZoneOrUtcOffset` and `TimeZone` scalars.
- `jiff::tz::Offset` as `UtcOffset` scalar. - `jiff::tz::Offset` as `UtcOffset` scalar.
- `jiff::Span` as `Duration` scalar. - `jiff::Span` as `Duration` scalar.

View file

@ -3,30 +3,31 @@
//! # 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] |
//! | [`Zoned`][^1] | [RFC 9557] string | `ZonedDateTime` | //! | [`Zoned`] [^1] | [RFC 9557] string | `ZonedDateTime` |
//! | [`tz::TimeZone`][^1] | [IANA database][1]/`±hh:mm` | `TimeZoneOrUtcOffset` | //! | [`tz::TimeZone`] [^1] | [IANA] identifier/`±hh:mm` | `TimeZoneOrUtcOffset` |
//! | [`tz::TimeZone`] via [`TimeZone`][^1] | [IANA database][1] | [`TimeZone`][s5] | //! | [`tz::TimeZone`] via [`TimeZone`] [^1] | [IANA] identifier | [`TimeZone`][s5] |
//! | [`tz::Offset`] | `±hh:mm` | [`UtcOffset`][s6] | //! | [`tz::Offset`] | `±hh:mm` | [`UtcOffset`][s6] |
//! | [`Span`] | [ISO 8601] duration | [`Duration`][s7] | //! | [`Span`] | [ISO 8601] duration | [`Duration`][s7] |
//! //!
//! # [`tz::TimeZone`] types
//!
//! [`tz::TimeZone`] values can be either [IANA] identifiers or fixed offsets, corresponding to
//! GraphQL scalars [`TimeZone`][s5] and [`UtcOffset`][s6] accordingly. While a [`UtcOffset`][s6]
//! GraphQL scalar can be serialized from a [`tz::Offset`] directly, the newtype [`TimeZone`]
//! handles serialization to a [`TimeZone`][s5] GraphQL scalar, with implementations [`TryFrom`] and
//! [`Into`] a [`tz::TimeZone`].
//!
//! In addition, a [`tz::TimeZone`] serializes to a `TimeZoneOrUtcOffset` GraphQL scalar, containing
//! either an [IANA] identifier or a fixed offset for clients being able to consume both values.
//!
//! [^1]: For these, crate [`jiff`] must be installed with a feature flag that provides access to //! [^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 //! the [IANA Time Zone Database][IANA] (e.g. by using the crate's default feature flags).
//! features][tz] for details. //! See [`jiff` time zone features][1] for details.
//!
//! # Time zone types
//!
//! `tz::TimeZone` values can be IANA time zone identifiers or fixed offsets, corresponding to
//! 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
@ -36,6 +37,7 @@
//! [`tz::Offset`]: jiff::tz::Offset //! [`tz::Offset`]: jiff::tz::Offset
//! [`tz::TimeZone`]: jiff::tz::TimeZone //! [`tz::TimeZone`]: jiff::tz::TimeZone
//! [`Zoned`]: jiff::Zoned //! [`Zoned`]: jiff::Zoned
//! [IANA]: http://iana.org/time-zones
//! [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
//! [RFC 9557]: https://datatracker.ietf.org/doc/html/rfc9557#section-4.1 //! [RFC 9557]: https://datatracker.ietf.org/doc/html/rfc9557#section-4.1
@ -46,8 +48,7 @@
//! [s5]: https://graphql-scalars.dev/docs/scalars/time-zone //! [s5]: https://graphql-scalars.dev/docs/scalars/time-zone
//! [s6]: https://graphql-scalars.dev/docs/scalars/utc-offset //! [s6]: https://graphql-scalars.dev/docs/scalars/utc-offset
//! [s7]: https://graphql-scalars.dev/docs/scalars/duration //! [s7]: https://graphql-scalars.dev/docs/scalars/duration
//! [tz]: https://docs.rs/jiff/latest/jiff/index.html#time-zone-features //! [1]: 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 std::{error::Error, fmt, str};
@ -264,7 +265,6 @@ mod date_time {
/// Time zone aware instant in time. /// Time zone aware instant in time.
/// ///
/// Can be thought of as combination of the following types, all rolled into one: /// Can be thought of as combination of the following types, all rolled into one:
///
/// - [`Timestamp`][3] for indicating precise instant in time. /// - [`Timestamp`][3] for indicating precise instant in time.
/// - [`DateTime`][4] for indicating "civil" calendar date and clock time. /// - [`DateTime`][4] for indicating "civil" calendar date and clock time.
/// - [`TimeZone`][5] for indicating how to apply time zone transitions while performing arithmetic. /// - [`TimeZone`][5] for indicating how to apply time zone transitions while performing arithmetic.
@ -281,6 +281,7 @@ mod date_time {
#[graphql_scalar( #[graphql_scalar(
with = zoned_date_time, with = zoned_date_time,
parse_token(String), parse_token(String),
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc9557#section-4.1",
)] )]
pub type ZonedDateTime = jiff::Zoned; pub type ZonedDateTime = jiff::Zoned;
@ -348,14 +349,20 @@ mod duration {
} }
} }
/// Representation of time zone or UTC offset. /// Representation of a time zone or UTC offset.
/// ///
/// [IANA database][1] or `±hh:mm`. /// Can be one of three possible representations:
/// - Identifier from the [IANA Time Zone Database][0].
/// - Fixed offset from UTC (`±hh:mm`).
///
/// May be seen as a combination of both [`TimeZone`][3] and [`UtcOffset` scalars][4].
/// ///
/// See also [`jiff::tz::TimeZone`][2] for details. /// See also [`jiff::tz::TimeZone`][2] for details.
/// ///
/// [1]: http://www.iana.org/time-zones /// [0]: http://iana.org/time-zones
/// [2]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html /// [2]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
/// [3]: https://graphql-scalars.dev/docs/scalars/time-zone
/// [4]: https://graphql-scalars.dev/docs/scalars/utc-offset
#[graphql_scalar( #[graphql_scalar(
with = time_zone_or_utc_offset, with = time_zone_or_utc_offset,
parse_token(String), parse_token(String),
@ -365,7 +372,7 @@ pub type TimeZoneOrUtcOffset = jiff::tz::TimeZone;
mod time_zone_or_utc_offset { mod time_zone_or_utc_offset {
use super::*; use super::*;
/// Format of a `TimeZoneOrUtcOffset` scalar. /// Format of a [`TimeZoneOrUtcOffset`] scalar.
const FORMAT: &str = "%:V"; const FORMAT: &str = "%:V";
pub(super) fn to_output<S>(v: &TimeZoneOrUtcOffset) -> Value<S> pub(super) fn to_output<S>(v: &TimeZoneOrUtcOffset) -> Value<S>
@ -376,8 +383,7 @@ mod time_zone_or_utc_offset {
|| { || {
// If no IANA time zone identifier is available, fall back to displaying the time // 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`). // offset directly (using format `[+-]HH:MM[:SS]` from RFC 9557, e.g. `+05:30`).
// // See: https://github.com/graphql-rust/juniper/pull/1278#discussion_r1719161686
// <https://github.com/graphql-rust/juniper/pull/1278#discussion_r1719161686>
jiff::Zoned::now() jiff::Zoned::now()
.with_time_zone(v.clone()) .with_time_zone(v.clone())
.strftime(FORMAT) .strftime(FORMAT)
@ -395,50 +401,51 @@ mod time_zone_or_utc_offset {
.ok_or_else(|| format!("Expected `String`, found: {v}")) .ok_or_else(|| format!("Expected `String`, found: {v}"))
.and_then(|s| { .and_then(|s| {
TimeZoneOrUtcOffset::get(s) TimeZoneOrUtcOffset::get(s)
.map_err(TimeZoneError::InvalidTimeZone) .map_err(TimeZoneParsingError::InvalidTimeZone)
.or_else(|_| utc_offset::utc_offset_from_str(s).map(TimeZoneOrUtcOffset::fixed)) .or_else(|_| utc_offset::utc_offset_from_str(s).map(TimeZoneOrUtcOffset::fixed))
.map_err(|e| format!("Invalid `TimeZoneOrUtcOffset`: {e}")) .map_err(|e| format!("Invalid `TimeZoneOrUtcOffset`: {e}"))
}) })
} }
} }
/// Error while handling [`TimeZone`] value. /// Error parsing a [`TimeZone`] value.
#[derive(Clone)] #[derive(Clone)]
pub enum TimeZoneError { pub enum TimeZoneParsingError {
/// Identifier could not be parsed by [`tz::TimeZone::get`](jiff::tz::TimeZone::get). /// Identifier cannot not be parsed by the [`jiff::tz::TimeZone::get()`] method.
InvalidTimeZone(jiff::Error), InvalidTimeZone(jiff::Error),
/// GraphQL scalar [`TimeZone`] requires `tz::TimeZone` with IANA name. /// GraphQL scalar [`TimeZone`] requires `tz::TimeZone` with IANA name.
MissingIanaName(jiff::tz::TimeZone), MissingIanaName(jiff::tz::TimeZone),
} }
impl fmt::Debug for TimeZoneError { impl fmt::Debug for TimeZoneParsingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::InvalidTimeZone(err) => write!(f, "TimeZoneError::InvalidTimeZone({err:?})"), Self::InvalidTimeZone(e) => write!(f, "TimeZoneParsingError::InvalidTimeZone({e:?})"),
Self::MissingIanaName(_value) => write!(f, "TimeZoneError::MissingIanaName(..)"), Self::MissingIanaName(_) => write!(f, "TimeZoneParsingError::MissingIanaName(..)"),
} }
} }
} }
impl fmt::Display for TimeZoneError { impl fmt::Display for TimeZoneParsingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::InvalidTimeZone(err) => err.fmt(f), Self::InvalidTimeZone(e) => e.fmt(f),
Self::MissingIanaName(_value) => write!(f, "missing IANA name"), Self::MissingIanaName(..) => write!(f, "missing IANA name"),
} }
} }
} }
impl Error for TimeZoneError { impl Error for TimeZoneParsingError {
fn source(&self) -> Option<&(dyn Error + 'static)> { fn source(&self) -> Option<&(dyn Error + 'static)> {
match self { match self {
Self::InvalidTimeZone(err) => Some(err), Self::InvalidTimeZone(e) => Some(e),
Self::MissingIanaName(_) => None, Self::MissingIanaName(..) => None,
} }
} }
} }
/// Representation of time zone. /// Representation of a time zone from the [IANA Time Zone Database][0].
/// ///
/// A set of rules for determining the civil time, via an offset from UTC, in a particular /// 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 /// geographic region. In many cases, the offset in a particular time zone can vary over the course
@ -448,6 +455,7 @@ impl Error for TimeZoneError {
/// ///
/// See also [`jiff::tz::TimeZone`][2] for details. /// See also [`jiff::tz::TimeZone`][2] for details.
/// ///
/// [0]: http://iana.org/time-zones
/// [1]: https://graphql-scalars.dev/docs/scalars/time-zone /// [1]: https://graphql-scalars.dev/docs/scalars/time-zone
/// [2]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html /// [2]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
#[graphql_scalar( #[graphql_scalar(
@ -459,21 +467,22 @@ impl Error for TimeZoneError {
pub struct TimeZone(jiff::tz::TimeZone); pub struct TimeZone(jiff::tz::TimeZone);
impl TryFrom<jiff::tz::TimeZone> for TimeZone { impl TryFrom<jiff::tz::TimeZone> for TimeZone {
type Error = TimeZoneError; type Error = TimeZoneParsingError;
fn try_from(value: jiff::tz::TimeZone) -> Result<Self, Self::Error> { fn try_from(value: jiff::tz::TimeZone) -> Result<Self, Self::Error> {
if value.iana_name().is_none() { if value.iana_name().is_none() {
return Err(TimeZoneError::MissingIanaName(value)); return Err(TimeZoneParsingError::MissingIanaName(value));
} }
Ok(Self(value)) Ok(Self(value))
} }
} }
impl str::FromStr for TimeZone { impl str::FromStr for TimeZone {
type Err = TimeZoneError; type Err = TimeZoneParsingError;
fn from_str(value: &str) -> Result<Self, Self::Err> { fn from_str(value: &str) -> Result<Self, Self::Err> {
let value = jiff::tz::TimeZone::get(value).map_err(TimeZoneError::InvalidTimeZone)?; let value =
jiff::tz::TimeZone::get(value).map_err(TimeZoneParsingError::InvalidTimeZone)?;
value.try_into() value.try_into()
} }
} }
@ -482,8 +491,10 @@ impl fmt::Display for TimeZone {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0 self.0
.iana_name() .iana_name()
.unwrap_or_else(|| {
// PANIC: We made sure that IANA name is available when constructing `Self`. // PANIC: We made sure that IANA name is available when constructing `Self`.
.unwrap_or_else(|| panic!("Failed to display `TimeZone`: no IANA name")) panic!("failed to display `TimeZone`: no IANA name")
})
.fmt(f) .fmt(f)
} }
} }
@ -495,8 +506,6 @@ impl From<TimeZone> for jiff::tz::TimeZone {
} }
mod time_zone { mod time_zone {
use std::str::FromStr as _;
use super::*; use super::*;
pub(super) fn to_output<S>(v: &TimeZone) -> Value<S> pub(super) fn to_output<S>(v: &TimeZone) -> Value<S>
@ -512,11 +521,11 @@ mod time_zone {
{ {
v.as_string_value() v.as_string_value()
.ok_or_else(|| format!("Expected `String`, found: {v}")) .ok_or_else(|| format!("Expected `String`, found: {v}"))
.and_then(|s| TimeZone::from_str(s).map_err(|e| format!("Invalid `TimeZone`: {e}"))) .and_then(|s| s.parse().map_err(|e| format!("Invalid `TimeZone`: {e}")))
} }
} }
/// Represents fixed time zone offset. /// Representation of a fixed time zone offset.
/// ///
/// [`UtcOffset` scalar][1] compliant. /// [`UtcOffset` scalar][1] compliant.
/// ///
@ -547,20 +556,16 @@ mod utc_offset {
Ok(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> pub(super) fn to_output<S>(v: &UtcOffset) -> Value<S>
where where
S: ScalarValue, S: ScalarValue,
{ {
Value::scalar(utc_offset_to_string(*v)) let mut buf = String::new();
let tm = jiff::fmt::strtime::BrokenDownTime::from(
&jiff::Zoned::now().with_time_zone(jiff::tz::TimeZone::fixed(*v)),
);
tm.format(FORMAT, &mut buf).unwrap();
Value::scalar(buf)
} }
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<UtcOffset, String> pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<UtcOffset, String>