1use std::fmt;
23
24use chrono::{FixedOffset, TimeZone};
25
26use crate::graphql_scalar;
27
28#[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 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#[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 const FORMAT: &str = "%H:%M:%S%.3f";
97
98 const FORMAT_NO_MILLIS: &str = "%H:%M:%S";
102
103 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 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#[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 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#[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
212pub trait FromFixedOffset: TimeZone {
281 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}