juniper/integrations/
chrono.rs

1//! GraphQL support for [`chrono`] crate types.
2//!
3//! # Supported types
4//!
5//! | Rust type         | Format                | GraphQL scalar        |
6//! |-------------------|-----------------------|-----------------------|
7//! | [`NaiveDate`]     | `yyyy-MM-dd`          | [`LocalDate`][s1]     |
8//! | [`NaiveTime`]     | `HH:mm[:ss[.SSS]]`    | [`LocalTime`][s2]     |
9//! | [`NaiveDateTime`] | `yyyy-MM-ddTHH:mm:ss` | [`LocalDateTime`][s3] |
10//! | [`DateTime`]      | [RFC 3339] string     | [`DateTime`][s4]      |
11//!
12//! [`DateTime`]: chrono::DateTime
13//! [`NaiveDate`]: chrono::naive::NaiveDate
14//! [`NaiveDateTime`]: chrono::naive::NaiveDateTime
15//! [`NaiveTime`]: chrono::naive::NaiveTime
16//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
17//! [s1]: https://graphql-scalars.dev/docs/scalars/local-date
18//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
19//! [s3]: https://graphql-scalars.dev/docs/scalars/local-date-time
20//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
21
22use std::fmt;
23
24use chrono::{FixedOffset, TimeZone};
25
26use crate::graphql_scalar;
27
28/// Date in the proleptic Gregorian calendar (without time zone).
29///
30/// Represents a description of the date (as used for birthdays, for example).
31/// It cannot represent an instant on the time-line.
32///
33/// [`LocalDate` scalar][1] compliant.
34///
35/// See also [`chrono::NaiveDate`][2] for details.
36///
37/// [1]: https://graphql-scalars.dev/docs/scalars/local-date
38/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
39#[graphql_scalar]
40#[graphql(
41    with = local_date,
42    parse_token(String),
43    specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-date",
44)]
45pub type LocalDate = chrono::NaiveDate;
46
47mod local_date {
48    use std::fmt::Display;
49
50    use super::LocalDate;
51
52    /// Format of a [`LocalDate` scalar][1].
53    ///
54    /// [1]: https://graphql-scalars.dev/docs/scalars/local-date
55    const FORMAT: &str = "%Y-%m-%d";
56
57    pub(super) fn to_output(v: &LocalDate) -> impl Display {
58        v.format(FORMAT)
59    }
60
61    pub(super) fn from_input(s: &str) -> Result<LocalDate, Box<str>> {
62        LocalDate::parse_from_str(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}").into())
63    }
64}
65
66/// Clock time within a given date (without time zone) in `HH:mm[:ss[.SSS]]`
67/// format.
68///
69/// All minutes are assumed to have exactly 60 seconds; no attempt is made to
70/// handle leap seconds (either positive or negative).
71///
72/// [`LocalTime` scalar][1] compliant.
73///
74/// See also [`chrono::NaiveTime`][2] for details.
75///
76/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
77/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html
78#[graphql_scalar]
79#[graphql(
80    with = local_time,
81    parse_token(String),
82    specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-time",
83)]
84pub type LocalTime = chrono::NaiveTime;
85
86mod local_time {
87    use std::fmt::Display;
88
89    use chrono::Timelike as _;
90
91    use super::LocalTime;
92
93    /// Full format of a [`LocalTime` scalar][1].
94    ///
95    /// [1]: https://graphql-scalars.dev/docs/scalars/local-time
96    const FORMAT: &str = "%H:%M:%S%.3f";
97
98    /// Format of a [`LocalTime` scalar][1] without milliseconds.
99    ///
100    /// [1]: https://graphql-scalars.dev/docs/scalars/local-time
101    const FORMAT_NO_MILLIS: &str = "%H:%M:%S";
102
103    /// Format of a [`LocalTime` scalar][1] without seconds.
104    ///
105    /// [1]: https://graphql-scalars.dev/docs/scalars/local-time
106    const FORMAT_NO_SECS: &str = "%H:%M";
107
108    pub(super) fn to_output(v: &LocalTime) -> impl Display {
109        if v.nanosecond() == 0 {
110            v.format(FORMAT_NO_MILLIS)
111        } else {
112            v.format(FORMAT)
113        }
114    }
115
116    pub(super) fn from_input(s: &str) -> Result<LocalTime, Box<str>> {
117        // First, try to parse the most used format.
118        // At the end, try to parse the full format for the parsing error to be most informative.
119        LocalTime::parse_from_str(s, FORMAT_NO_MILLIS)
120            .or_else(|_| LocalTime::parse_from_str(s, FORMAT_NO_SECS))
121            .or_else(|_| LocalTime::parse_from_str(s, FORMAT))
122            .map_err(|e| format!("Invalid `LocalTime`: {e}").into())
123    }
124}
125
126/// Combined date and time (without time zone) in `yyyy-MM-ddTHH:mm:ss` format.
127///
128/// [`LocalDateTime` scalar][1] compliant.
129///
130/// See also [`chrono::NaiveDateTime`][2] for details.
131///
132/// [1]: https://graphql-scalars.dev/docs/scalars/local-date-time
133/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
134#[graphql_scalar]
135#[graphql(
136    with = local_date_time,
137    parse_token(String),
138    specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-date-time",
139)]
140pub type LocalDateTime = chrono::NaiveDateTime;
141
142mod local_date_time {
143    use std::fmt::Display;
144
145    use super::LocalDateTime;
146
147    /// Format of a [`LocalDateTime` scalar][1].
148    ///
149    /// [1]: https://graphql-scalars.dev/docs/scalars/local-date-time
150    const FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
151
152    pub(super) fn to_output(v: &LocalDateTime) -> impl Display {
153        v.format(FORMAT)
154    }
155
156    pub(super) fn from_input(s: &str) -> Result<LocalDateTime, Box<str>> {
157        LocalDateTime::parse_from_str(s, FORMAT)
158            .map_err(|e| format!("Invalid `LocalDateTime`: {e}").into())
159    }
160}
161
162/// Combined date and time (with time zone) in [RFC 3339][0] format.
163///
164/// Represents a description of an exact instant on the time-line (such as the
165/// instant that a user account was created).
166///
167/// [`DateTime` scalar][1] compliant.
168///
169/// See also [`chrono::DateTime`][2] for details.
170///
171/// [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5
172/// [1]: https://graphql-scalars.dev/docs/scalars/date-time
173/// [2]: https://docs.rs/chrono/latest/chrono/struct.DateTime.html
174#[graphql_scalar]
175#[graphql(
176    with = date_time,
177    parse_token(String),
178    specified_by_url = "https://graphql-scalars.dev/docs/scalars/date-time",
179    where(
180        Tz: TimeZone + FromFixedOffset,
181        Tz::Offset: fmt::Display,
182    )
183)]
184pub type DateTime<Tz> = chrono::DateTime<Tz>;
185
186mod date_time {
187    use std::fmt::Display;
188
189    use chrono::{FixedOffset, SecondsFormat, TimeZone, Utc};
190
191    use super::{DateTime, FromFixedOffset};
192
193    pub(super) fn to_output<Tz>(v: &DateTime<Tz>) -> String
194    where
195        Tz: TimeZone,
196        Tz::Offset: Display,
197    {
198        v.with_timezone(&Utc)
199            .to_rfc3339_opts(SecondsFormat::AutoSi, true)
200    }
201
202    pub(super) fn from_input<Tz>(s: &str) -> Result<DateTime<Tz>, Box<str>>
203    where
204        Tz: TimeZone + FromFixedOffset,
205    {
206        DateTime::<FixedOffset>::parse_from_rfc3339(s)
207            .map(FromFixedOffset::from_fixed_offset)
208            .map_err(|e| format!("Invalid `DateTime`: {e}").into())
209    }
210}
211
212/// Trait allowing to implement a custom [`TimeZone`], which preserves its
213/// [`TimeZone`] information when parsed in a [`DateTime`] GraphQL scalar.
214///
215/// # Example
216///
217/// Creating a custom [CET] [`TimeZone`] using [`chrono-tz`] crate. This is
218/// required because [`chrono-tz`] uses enum to represent all [`TimeZone`]s, so
219/// we have no knowledge of the concrete underlying [`TimeZone`] on the type
220/// level.
221///
222/// ```rust
223/// # use chrono::{FixedOffset, TimeZone};
224/// # use juniper::{
225/// #     integrations::chrono::{FromFixedOffset, DateTime},
226/// #     graphql_object,
227/// # };
228/// #
229/// #[derive(Clone, Copy)]
230/// struct CET;
231///
232/// impl TimeZone for CET {
233///     type Offset = <chrono_tz::Tz as TimeZone>::Offset;
234///
235///     fn from_offset(_: &Self::Offset) -> Self {
236///         CET
237///     }
238///
239///     fn offset_from_local_date(
240///         &self,
241///         local: &chrono::NaiveDate,
242///     ) -> chrono::LocalResult<Self::Offset> {
243///         chrono_tz::CET.offset_from_local_date(local)
244///     }
245///
246///     fn offset_from_local_datetime(
247///         &self,
248///         local: &chrono::NaiveDateTime,
249///     ) -> chrono::LocalResult<Self::Offset> {
250///         chrono_tz::CET.offset_from_local_datetime(local)
251///     }
252///
253///     fn offset_from_utc_date(&self, utc: &chrono::NaiveDate) -> Self::Offset {
254///         chrono_tz::CET.offset_from_utc_date(utc)
255///     }
256///
257///     fn offset_from_utc_datetime(&self, utc: &chrono::NaiveDateTime) -> Self::Offset {
258///         chrono_tz::CET.offset_from_utc_datetime(utc)
259///     }
260/// }
261///
262/// impl FromFixedOffset for CET {
263///     fn from_fixed_offset(dt: DateTime<FixedOffset>) -> DateTime<Self> {
264///         dt.with_timezone(&CET)
265///     }
266/// }
267///
268/// struct Root;
269///
270/// #[graphql_object]
271/// impl Root {
272///     fn pass_date_time(dt: DateTime<CET>) -> DateTime<CET> {
273///         dt
274///     }
275/// }
276/// ```
277///
278/// [`chrono-tz`]: chrono_tz
279/// [CET]: https://en.wikipedia.org/wiki/Central_European_Time
280pub trait FromFixedOffset: TimeZone {
281    /// Converts the given [`DateTime`]`<`[`FixedOffset`]`>` into a
282    /// [`DateTime`]`<Self>`.
283    fn from_fixed_offset(dt: DateTime<FixedOffset>) -> DateTime<Self>;
284}
285
286impl FromFixedOffset for FixedOffset {
287    fn from_fixed_offset(dt: DateTime<Self>) -> DateTime<Self> {
288        dt
289    }
290}
291
292impl FromFixedOffset for chrono::Utc {
293    fn from_fixed_offset(dt: DateTime<FixedOffset>) -> DateTime<Self> {
294        dt.into()
295    }
296}
297
298#[cfg(feature = "chrono-clock")]
299impl FromFixedOffset for chrono::Local {
300    fn from_fixed_offset(dt: DateTime<FixedOffset>) -> DateTime<Self> {
301        dt.into()
302    }
303}
304
305#[cfg(feature = "chrono-tz")]
306impl FromFixedOffset for chrono_tz::Tz {
307    fn from_fixed_offset(dt: DateTime<FixedOffset>) -> DateTime<Self> {
308        dt.with_timezone(&chrono_tz::UTC)
309    }
310}
311
312#[cfg(test)]
313mod local_date_test {
314    use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql_input_value};
315
316    use super::LocalDate;
317
318    #[test]
319    fn parses_correct_input() {
320        for (raw, expected) in [
321            ("1996-12-19", LocalDate::from_ymd_opt(1996, 12, 19)),
322            ("1564-01-30", LocalDate::from_ymd_opt(1564, 01, 30)),
323        ] {
324            let input: InputValue = graphql_input_value!((raw));
325            let parsed = LocalDate::from_input_value(&input);
326
327            assert!(
328                parsed.is_ok(),
329                "failed to parse `{raw}`: {:?}",
330                parsed.unwrap_err(),
331            );
332            assert_eq!(parsed.unwrap(), expected.unwrap(), "input: {raw}");
333        }
334    }
335
336    #[test]
337    fn fails_on_invalid_input() {
338        for input in [
339            graphql_input_value!("1996-13-19"),
340            graphql_input_value!("1564-01-61"),
341            graphql_input_value!("2021-11-31"),
342            graphql_input_value!("11-31"),
343            graphql_input_value!("2021-11"),
344            graphql_input_value!("2021"),
345            graphql_input_value!("31"),
346            graphql_input_value!("i'm not even a date"),
347            graphql_input_value!(2.32),
348            graphql_input_value!(1),
349            graphql_input_value!(null),
350            graphql_input_value!(false),
351        ] {
352            let input: InputValue = input;
353            let parsed = LocalDate::from_input_value(&input);
354
355            assert!(parsed.is_err(), "allows input: {input:?}");
356        }
357    }
358
359    #[test]
360    fn formats_correctly() {
361        for (val, expected) in [
362            (
363                LocalDate::from_ymd_opt(1996, 12, 19),
364                graphql_input_value!("1996-12-19"),
365            ),
366            (
367                LocalDate::from_ymd_opt(1564, 01, 30),
368                graphql_input_value!("1564-01-30"),
369            ),
370            (
371                LocalDate::from_ymd_opt(2020, 01, 01),
372                graphql_input_value!("2020-01-01"),
373            ),
374        ] {
375            let val = val.unwrap();
376            let actual: InputValue = val.to_input_value();
377
378            assert_eq!(actual, expected, "on value: {val}");
379        }
380    }
381}
382
383#[cfg(test)]
384mod local_time_test {
385    use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql_input_value};
386
387    use super::LocalTime;
388
389    #[test]
390    fn parses_correct_input() {
391        for (raw, expected) in [
392            ("14:23:43", LocalTime::from_hms_opt(14, 23, 43)),
393            ("14:00:00", LocalTime::from_hms_opt(14, 00, 00)),
394            ("14:00", LocalTime::from_hms_opt(14, 00, 00)),
395            ("14:32", LocalTime::from_hms_opt(14, 32, 00)),
396            ("14:00:00.000", LocalTime::from_hms_opt(14, 00, 00)),
397            (
398                "14:23:43.345",
399                LocalTime::from_hms_milli_opt(14, 23, 43, 345),
400            ),
401        ] {
402            let input: InputValue = graphql_input_value!((raw));
403            let parsed = LocalTime::from_input_value(&input);
404
405            assert!(
406                parsed.is_ok(),
407                "failed to parse `{raw}`: {:?}",
408                parsed.unwrap_err(),
409            );
410            assert_eq!(parsed.unwrap(), expected.unwrap(), "input: {raw}");
411        }
412    }
413
414    #[test]
415    fn fails_on_invalid_input() {
416        for input in [
417            graphql_input_value!("12"),
418            graphql_input_value!("12:"),
419            graphql_input_value!("56:34:22"),
420            graphql_input_value!("23:78:43"),
421            graphql_input_value!("23:78:"),
422            graphql_input_value!("23:18:99"),
423            graphql_input_value!("23:18:22."),
424            graphql_input_value!("22.03"),
425            graphql_input_value!("24:00"),
426            graphql_input_value!("24:00:00"),
427            graphql_input_value!("24:00:00.000"),
428            graphql_input_value!("i'm not even a time"),
429            graphql_input_value!(2.32),
430            graphql_input_value!(1),
431            graphql_input_value!(null),
432            graphql_input_value!(false),
433        ] {
434            let input: InputValue = input;
435            let parsed = LocalTime::from_input_value(&input);
436
437            assert!(parsed.is_err(), "allows input: {input:?}");
438        }
439    }
440
441    #[test]
442    fn formats_correctly() {
443        for (val, expected) in [
444            (
445                LocalTime::from_hms_micro_opt(1, 2, 3, 4005),
446                graphql_input_value!("01:02:03.004"),
447            ),
448            (
449                LocalTime::from_hms_opt(0, 0, 0),
450                graphql_input_value!("00:00:00"),
451            ),
452            (
453                LocalTime::from_hms_opt(12, 0, 0),
454                graphql_input_value!("12:00:00"),
455            ),
456            (
457                LocalTime::from_hms_opt(1, 2, 3),
458                graphql_input_value!("01:02:03"),
459            ),
460        ] {
461            let val = val.unwrap();
462            let actual: InputValue = val.to_input_value();
463
464            assert_eq!(actual, expected, "on value: {val}");
465        }
466    }
467}
468
469#[cfg(test)]
470mod local_date_time_test {
471    use chrono::naive::{NaiveDate, NaiveTime};
472
473    use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql_input_value};
474
475    use super::LocalDateTime;
476
477    #[test]
478    fn parses_correct_input() {
479        for (raw, expected) in [
480            (
481                "1996-12-19T14:23:43",
482                LocalDateTime::new(
483                    NaiveDate::from_ymd_opt(1996, 12, 19).unwrap(),
484                    NaiveTime::from_hms_opt(14, 23, 43).unwrap(),
485                ),
486            ),
487            (
488                "1564-01-30T14:00:00",
489                LocalDateTime::new(
490                    NaiveDate::from_ymd_opt(1564, 1, 30).unwrap(),
491                    NaiveTime::from_hms_opt(14, 00, 00).unwrap(),
492                ),
493            ),
494        ] {
495            let input: InputValue = graphql_input_value!((raw));
496            let parsed = LocalDateTime::from_input_value(&input);
497
498            assert!(
499                parsed.is_ok(),
500                "failed to parse `{raw}`: {:?}",
501                parsed.unwrap_err(),
502            );
503            assert_eq!(parsed.unwrap(), expected, "input: {raw}");
504        }
505    }
506
507    #[test]
508    fn fails_on_invalid_input() {
509        for input in [
510            graphql_input_value!("12"),
511            graphql_input_value!("12:"),
512            graphql_input_value!("56:34:22"),
513            graphql_input_value!("56:34:22.000"),
514            graphql_input_value!("1996-12-1914:23:43"),
515            graphql_input_value!("1996-12-19 14:23:43"),
516            graphql_input_value!("1996-12-19Q14:23:43"),
517            graphql_input_value!("1996-12-19T14:23:43Z"),
518            graphql_input_value!("1996-12-19T14:23:43.543"),
519            graphql_input_value!("1996-12-19T14:23"),
520            graphql_input_value!("1996-12-19T14:23:"),
521            graphql_input_value!("1996-12-19T23:78:43"),
522            graphql_input_value!("1996-12-19T23:18:99"),
523            graphql_input_value!("1996-12-19T24:00:00"),
524            graphql_input_value!("1996-12-19T99:02:13"),
525            graphql_input_value!("i'm not even a datetime"),
526            graphql_input_value!(2.32),
527            graphql_input_value!(1),
528            graphql_input_value!(null),
529            graphql_input_value!(false),
530        ] {
531            let input: InputValue = input;
532            let parsed = LocalDateTime::from_input_value(&input);
533
534            assert!(parsed.is_err(), "allows input: {input:?}");
535        }
536    }
537
538    #[test]
539    fn formats_correctly() {
540        for (val, expected) in [
541            (
542                LocalDateTime::new(
543                    NaiveDate::from_ymd_opt(1996, 12, 19).unwrap(),
544                    NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
545                ),
546                graphql_input_value!("1996-12-19T00:00:00"),
547            ),
548            (
549                LocalDateTime::new(
550                    NaiveDate::from_ymd_opt(1564, 1, 30).unwrap(),
551                    NaiveTime::from_hms_opt(14, 0, 0).unwrap(),
552                ),
553                graphql_input_value!("1564-01-30T14:00:00"),
554            ),
555        ] {
556            let actual: InputValue = val.to_input_value();
557
558            assert_eq!(actual, expected, "on value: {val}");
559        }
560    }
561}
562
563#[cfg(test)]
564mod date_time_test {
565    use chrono::{
566        FixedOffset,
567        naive::{NaiveDate, NaiveDateTime, NaiveTime},
568    };
569
570    use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql_input_value};
571
572    use super::DateTime;
573
574    #[test]
575    fn parses_correct_input() {
576        for (raw, expected) in [
577            (
578                "2014-11-28T21:00:09+09:00",
579                DateTime::<FixedOffset>::from_naive_utc_and_offset(
580                    NaiveDateTime::new(
581                        NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
582                        NaiveTime::from_hms_opt(12, 0, 9).unwrap(),
583                    ),
584                    FixedOffset::east_opt(9 * 3600).unwrap(),
585                ),
586            ),
587            (
588                "2014-11-28T21:00:09Z",
589                DateTime::<FixedOffset>::from_naive_utc_and_offset(
590                    NaiveDateTime::new(
591                        NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
592                        NaiveTime::from_hms_opt(21, 0, 9).unwrap(),
593                    ),
594                    FixedOffset::east_opt(0).unwrap(),
595                ),
596            ),
597            (
598                "2014-11-28 21:00:09z",
599                DateTime::<FixedOffset>::from_naive_utc_and_offset(
600                    NaiveDateTime::new(
601                        NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
602                        NaiveTime::from_hms_opt(21, 0, 9).unwrap(),
603                    ),
604                    FixedOffset::east_opt(0).unwrap(),
605                ),
606            ),
607            (
608                "2014-11-28T21:00:09+00:00",
609                DateTime::<FixedOffset>::from_naive_utc_and_offset(
610                    NaiveDateTime::new(
611                        NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
612                        NaiveTime::from_hms_opt(21, 0, 9).unwrap(),
613                    ),
614                    FixedOffset::east_opt(0).unwrap(),
615                ),
616            ),
617            (
618                "2014-11-28T21:00:09.05+09:00",
619                DateTime::<FixedOffset>::from_naive_utc_and_offset(
620                    NaiveDateTime::new(
621                        NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
622                        NaiveTime::from_hms_milli_opt(12, 0, 9, 50).unwrap(),
623                    ),
624                    FixedOffset::east_opt(0).unwrap(),
625                ),
626            ),
627            (
628                "2014-11-28 21:00:09.05+09:00",
629                DateTime::<FixedOffset>::from_naive_utc_and_offset(
630                    NaiveDateTime::new(
631                        NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
632                        NaiveTime::from_hms_milli_opt(12, 0, 9, 50).unwrap(),
633                    ),
634                    FixedOffset::east_opt(0).unwrap(),
635                ),
636            ),
637        ] {
638            let input: InputValue = graphql_input_value!((raw));
639            let parsed = DateTime::<FixedOffset>::from_input_value(&input);
640
641            assert!(
642                parsed.is_ok(),
643                "failed to parse `{raw}`: {:?}",
644                parsed.unwrap_err(),
645            );
646            assert_eq!(parsed.unwrap(), expected, "input: {raw}");
647        }
648    }
649
650    #[test]
651    fn fails_on_invalid_input() {
652        for input in [
653            graphql_input_value!("12"),
654            graphql_input_value!("12:"),
655            graphql_input_value!("56:34:22"),
656            graphql_input_value!("56:34:22.000"),
657            graphql_input_value!("1996-12-1914:23:43"),
658            graphql_input_value!("1996-12-19Q14:23:43Z"),
659            graphql_input_value!("1996-12-19T14:23:43"),
660            graphql_input_value!("1996-12-19T14:23:43ZZ"),
661            graphql_input_value!("1996-12-19T14:23:43.543"),
662            graphql_input_value!("1996-12-19T14:23"),
663            graphql_input_value!("1996-12-19T14:23:1"),
664            graphql_input_value!("1996-12-19T14:23:"),
665            graphql_input_value!("1996-12-19T23:78:43Z"),
666            graphql_input_value!("1996-12-19T23:18:99Z"),
667            graphql_input_value!("1996-12-19T24:00:00Z"),
668            graphql_input_value!("1996-12-19T99:02:13Z"),
669            graphql_input_value!("1996-12-19T99:02:13Z"),
670            graphql_input_value!("1996-12-19T12:02:13+4444444"),
671            graphql_input_value!("i'm not even a datetime"),
672            graphql_input_value!(2.32),
673            graphql_input_value!(1),
674            graphql_input_value!(null),
675            graphql_input_value!(false),
676        ] {
677            let input: InputValue = input;
678            let parsed = DateTime::<FixedOffset>::from_input_value(&input);
679
680            assert!(parsed.is_err(), "allows input: {input:?}");
681        }
682    }
683
684    #[test]
685    fn formats_correctly() {
686        for (val, expected) in [
687            (
688                DateTime::<FixedOffset>::from_naive_utc_and_offset(
689                    NaiveDateTime::new(
690                        NaiveDate::from_ymd_opt(1996, 12, 19).unwrap(),
691                        NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
692                    ),
693                    FixedOffset::east_opt(0).unwrap(),
694                ),
695                graphql_input_value!("1996-12-19T00:00:00Z"),
696            ),
697            (
698                DateTime::<FixedOffset>::from_naive_utc_and_offset(
699                    NaiveDateTime::new(
700                        NaiveDate::from_ymd_opt(1564, 1, 30).unwrap(),
701                        NaiveTime::from_hms_milli_opt(5, 0, 0, 123).unwrap(),
702                    ),
703                    FixedOffset::east_opt(9 * 3600).unwrap(),
704                ),
705                graphql_input_value!("1564-01-30T05:00:00.123Z"),
706            ),
707        ] {
708            let actual: InputValue = val.to_input_value();
709
710            assert_eq!(actual, expected, "on value: {val}");
711        }
712    }
713}
714
715#[cfg(test)]
716mod integration_test {
717    use crate::{
718        execute, graphql_object, graphql_value, graphql_vars,
719        schema::model::RootNode,
720        types::scalars::{EmptyMutation, EmptySubscription},
721    };
722
723    use super::{
724        DateTime, FixedOffset, FromFixedOffset, LocalDate, LocalDateTime, LocalTime, TimeZone,
725    };
726
727    #[tokio::test]
728    async fn serializes() {
729        #[derive(Clone, Copy)]
730        struct CET;
731
732        impl TimeZone for CET {
733            type Offset = <chrono_tz::Tz as TimeZone>::Offset;
734
735            fn from_offset(_: &Self::Offset) -> Self {
736                CET
737            }
738
739            fn offset_from_local_date(
740                &self,
741                local: &chrono::NaiveDate,
742            ) -> chrono::LocalResult<Self::Offset> {
743                chrono_tz::CET.offset_from_local_date(local)
744            }
745
746            fn offset_from_local_datetime(
747                &self,
748                local: &chrono::NaiveDateTime,
749            ) -> chrono::LocalResult<Self::Offset> {
750                chrono_tz::CET.offset_from_local_datetime(local)
751            }
752
753            fn offset_from_utc_date(&self, utc: &chrono::NaiveDate) -> Self::Offset {
754                chrono_tz::CET.offset_from_utc_date(utc)
755            }
756
757            fn offset_from_utc_datetime(&self, utc: &chrono::NaiveDateTime) -> Self::Offset {
758                chrono_tz::CET.offset_from_utc_datetime(utc)
759            }
760        }
761
762        impl FromFixedOffset for CET {
763            fn from_fixed_offset(dt: DateTime<FixedOffset>) -> DateTime<Self> {
764                dt.with_timezone(&CET)
765            }
766        }
767
768        struct Root;
769
770        #[graphql_object]
771        impl Root {
772            fn local_date() -> LocalDate {
773                LocalDate::from_ymd_opt(2015, 3, 14).unwrap()
774            }
775
776            fn local_time() -> LocalTime {
777                LocalTime::from_hms_opt(16, 7, 8).unwrap()
778            }
779
780            fn local_date_time() -> LocalDateTime {
781                LocalDateTime::new(
782                    LocalDate::from_ymd_opt(2016, 7, 8).unwrap(),
783                    LocalTime::from_hms_opt(9, 10, 11).unwrap(),
784                )
785            }
786
787            fn date_time() -> DateTime<chrono::Utc> {
788                DateTime::from_naive_utc_and_offset(
789                    LocalDateTime::new(
790                        LocalDate::from_ymd_opt(1996, 12, 20).unwrap(),
791                        LocalTime::from_hms_opt(0, 39, 57).unwrap(),
792                    ),
793                    chrono::Utc,
794                )
795            }
796
797            fn pass_date_time(dt: DateTime<CET>) -> DateTime<CET> {
798                dt
799            }
800
801            fn transform_date_time(dt: DateTime<CET>) -> DateTime<chrono::Utc> {
802                dt.with_timezone(&chrono::Utc)
803            }
804        }
805
806        const DOC: &str = r#"{
807            localDate
808            localTime
809            localDateTime
810            dateTime,
811            passDateTime(dt: "2014-11-28T21:00:09+09:00")
812            transformDateTime(dt: "2014-11-28T21:00:09+09:00")
813        }"#;
814
815        let schema = RootNode::new(
816            Root,
817            EmptyMutation::<()>::new(),
818            EmptySubscription::<()>::new(),
819        );
820
821        assert_eq!(
822            execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
823            Ok((
824                graphql_value!({
825                    "localDate": "2015-03-14",
826                    "localTime": "16:07:08",
827                    "localDateTime": "2016-07-08T09:10:11",
828                    "dateTime": "1996-12-20T00:39:57Z",
829                    "passDateTime": "2014-11-28T12:00:09Z",
830                    "transformDateTime": "2014-11-28T12:00:09Z",
831                }),
832                vec![],
833            )),
834        );
835    }
836}