Add scalars for the `time` crate's datetime types.

With the `time` feature flag enabled, `time::OffsetDateTime` and
`time::PrimitiveDateTime` can be used directly as scalar values,
similar to the previous `chrono::DateTime` and `chrono::NaiveDateTime`
scalar implementations.
This commit is contained in:
Jenan Wise 2022-01-19 11:12:16 -08:00
parent c6bb4c026c
commit a68a9be6cf
6 changed files with 156 additions and 0 deletions

View File

@ -65,6 +65,7 @@ uuid = { version = "0.8.2", optional = true, features = ["v4", "serde"] }
rust_decimal = { version = "1.14.3", optional = true }
url = { version = "2.2.1", optional = true }
smol_str = { version = "0.1.21", optional = true }
time = { version = "0.3.5", optional = true, features = ["parsing", "formatting", "macros"] }
# Non-feature optional dependencies
blocking = { version = "1.0.2", optional = true }

View File

@ -80,6 +80,7 @@ This crate offers the following features, all of which are not activated by defa
- `cbor`: Support for [serde_cbor](https://crates.io/crates/serde_cbor).
- `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str).
- `hashbrown`: Integrate with the [`hashbrown` crate](https://github.com/rust-lang/hashbrown).
- `time`: Integrate with the [`time` crate](https://github.com/time-rs/time).
## Apollo Studio

View File

@ -75,6 +75,7 @@
//! - `cbor`: Support for [serde_cbor](https://crates.io/crates/serde_cbor).
//! - `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str).
//! - `hashbrown`: Integrate with the [`hashbrown` crate](https://github.com/rust-lang/hashbrown).
//! - `time`: Integrate with the [`time` crate](https://github.com/time-rs/time).
//!
//! ## Integrations
//!

View File

@ -28,6 +28,10 @@ mod naive_time;
mod secrecy;
#[cfg(feature = "smol_str")]
mod smol_str;
#[cfg(feature = "time")]
mod time_offset_date_time;
#[cfg(feature = "time")]
mod time_primitive_date_time;
#[cfg(feature = "url")]
mod url;
#[cfg(feature = "uuid")]

View File

@ -0,0 +1,78 @@
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
use time::{format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset};
/// A datetime with timezone offset.
///
/// The input is a string in RFC3339 format, e.g. "2022-01-12T04:00:19.12345Z"
/// or "2022-01-12T04:00:19+03:00". The output is also a string in RFC3339
/// format, but it is always normalized to the UTC (Z) offset, e.g.
/// "2022-01-12T04:00:19.12345Z".
#[Scalar(
internal,
name = "DateTime",
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339"
)]
impl ScalarType for OffsetDateTime {
fn parse(value: Value) -> InputValueResult<Self> {
match &value {
Value::String(s) => Ok(Self::parse(s, &Rfc3339)?),
_ => Err(InputValueError::expected_type(value)),
}
}
fn to_value(&self) -> Value {
Value::String(
self.to_offset(UtcOffset::UTC)
.format(&Rfc3339)
.unwrap_or_else(|e| panic!("Failed to format `OffsetDateTime`: {}", e)),
)
}
}
#[cfg(test)]
mod tests {
use crate::{ScalarType, Value};
use time::{macros::datetime, OffsetDateTime};
#[test]
fn test_offset_date_time_to_value() {
let cases = [
(
datetime!(2022-01-12 07:30:19.12345 +3:30),
"2022-01-12T04:00:19.12345Z",
),
(datetime!(2022-01-12 07:30:19-0), "2022-01-12T07:30:19Z"),
];
for (value, expected) in cases {
let value = value.to_value();
if let Value::String(s) = value {
assert_eq!(s, expected);
} else {
panic!(
"Unexpected Value type when formatting OffsetDateTime: {:?}",
value
);
}
}
}
#[test]
fn test_offset_date_time_parse() {
let cases = [
(
"2022-01-12T04:00:19.12345Z",
datetime!(2022-01-12 07:30:19.12345 +3:30),
),
(
"2022-01-12T23:22:19.12345-00:00",
datetime!(2022-01-12 23:22:19.12345-0),
),
];
for (value, expected) in cases {
let value = Value::String(value.to_string());
let parsed = <OffsetDateTime as ScalarType>::parse(value).unwrap();
assert_eq!(parsed, expected);
}
}
}

View File

@ -0,0 +1,71 @@
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
use time::{format_description::FormatItem, macros::format_description, PrimitiveDateTime};
const PRIMITIVE_DATE_TIME_FORMAT: &[FormatItem<'_>] =
format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]");
/// A local datetime without timezone offset.
///
/// The input/output is a string in ISO 8601 format without timezone, including
/// subseconds. E.g. "2022-01-12T07:30:19.12345".
#[Scalar(internal, name = "LocalDateTime")]
impl ScalarType for PrimitiveDateTime {
fn parse(value: Value) -> InputValueResult<Self> {
match &value {
Value::String(s) => Ok(Self::parse(s, &PRIMITIVE_DATE_TIME_FORMAT)?),
_ => Err(InputValueError::expected_type(value)),
}
}
fn to_value(&self) -> Value {
Value::String(
self.format(&PRIMITIVE_DATE_TIME_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `PrimitiveDateTime`: {}", e)),
)
}
}
#[cfg(test)]
mod tests {
use crate::{ScalarType, Value};
use time::{macros::datetime, PrimitiveDateTime};
#[test]
fn test_primitive_date_time_to_value() {
let cases = [
(
datetime!(2022-01-12 07:30:19.12345),
"2022-01-12T07:30:19.12345",
),
(datetime!(2022-01-12 07:30:19), "2022-01-12T07:30:19.0"),
];
for (value, expected) in cases {
let value = value.to_value();
if let Value::String(s) = value {
assert_eq!(s, expected);
} else {
panic!(
"Unexpected Value type when formatting PrimitiveDateTime: {:?}",
value
);
}
}
}
#[test]
fn test_primitive_date_time_parse() {
let cases = [
(
"2022-01-12T07:30:19.12345",
datetime!(2022-01-12 07:30:19.12345),
),
("2022-01-12T07:30:19.0", datetime!(2022-01-12 07:30:19)),
];
for (value, expected) in cases {
let value = Value::String(value.to_string());
let parsed = <PrimitiveDateTime as ScalarType>::parse(value).unwrap();
assert_eq!(parsed, expected);
}
}
}