juniper/integrations/
bson.rs

1//! GraphQL support for [`bson`] crate types.
2//!
3//! # Supported types
4//!
5//! | Rust type         | Format            | GraphQL scalar   |
6//! |-------------------|-------------------|------------------|
7//! | [`oid::ObjectId`] | HEX string        | [`ObjectID`][s1] |
8//! | [`DateTime`]      | [RFC 3339] string | [`DateTime`][s4] |
9//!
10//! [`DateTime`]: bson::DateTime
11//! [`oid::ObjectId`]: bson::oid::ObjectId
12//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
13//! [s1]: https://graphql-scalars.dev/docs/scalars/object-id
14//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
15
16use crate::graphql_scalar;
17
18// TODO: Try remove on upgrade of `bson` crate.
19mod for_minimal_versions_check_only {
20    use tap as _;
21}
22
23/// [BSON ObjectId][0] represented as a HEX string.
24///
25/// [`ObjectID` scalar][1] compliant.
26///
27/// See also [`bson::oid::ObjectId`][2] for details.
28///
29/// [0]: https://www.mongodb.com/docs/manual/reference/bson-types#objectid
30/// [1]: https://graphql-scalars.dev/docs/scalars/object-id
31/// [2]: https://docs.rs/bson/*/bson/oid/struct.ObjectId.html
32#[graphql_scalar]
33#[graphql(
34    name = "ObjectID",
35    with = object_id,
36    parse_token(String),
37    specified_by_url = "https://graphql-scalars.dev/docs/scalars/object-id",
38)]
39type ObjectId = bson::oid::ObjectId;
40
41mod object_id {
42    use super::ObjectId;
43
44    pub(super) fn to_output(v: &ObjectId) -> String {
45        v.to_hex()
46    }
47
48    pub(super) fn from_input(s: &str) -> Result<ObjectId, Box<str>> {
49        ObjectId::parse_str(s).map_err(|e| format!("Failed to parse `ObjectID`: {e}").into())
50    }
51}
52
53/// [BSON date][3] in [RFC 3339][0] format.
54///
55/// [BSON datetimes][3] have millisecond precision and are always in UTC (inputs with other
56/// timezones are coerced).
57///
58/// [`DateTime` scalar][1] compliant.
59///
60/// See also [`bson::DateTime`][2] for details.
61///
62/// [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
63/// [1]: https://graphql-scalars.dev/docs/scalars/date-time
64/// [2]: https://docs.rs/bson/*/bson/struct.DateTime.html
65/// [3]: https://www.mongodb.com/docs/manual/reference/bson-types#date
66#[graphql_scalar]
67#[graphql(
68    with = date_time,
69    parse_token(String),
70    specified_by_url = "https://graphql-scalars.dev/docs/scalars/date-time",
71)]
72type DateTime = bson::DateTime;
73
74mod date_time {
75    use super::DateTime;
76
77    pub(super) fn to_output(v: &DateTime) -> String {
78        (*v).try_to_rfc3339_string()
79            .unwrap_or_else(|e| panic!("failed to format `DateTime` as RFC 3339: {e}"))
80    }
81
82    pub(super) fn from_input(s: &str) -> Result<DateTime, Box<str>> {
83        DateTime::parse_rfc3339_str(s)
84            .map_err(|e| format!("Failed to parse `DateTime`: {e}").into())
85    }
86}
87
88#[cfg(test)]
89mod test {
90    use bson::oid::ObjectId;
91
92    use crate::{FromInputValue, InputValue, graphql_input_value};
93
94    #[test]
95    fn objectid_from_input() {
96        let raw = "53e37d08776f724e42000000";
97        let input: InputValue = graphql_input_value!((raw));
98
99        let parsed: ObjectId = FromInputValue::from_input_value(&input).unwrap();
100        let id = ObjectId::parse_str(raw).unwrap();
101
102        assert_eq!(parsed, id);
103    }
104}
105
106#[cfg(test)]
107mod date_time_test {
108    use crate::{FromInputValue as _, InputValue, ToInputValue as _, graphql_input_value};
109
110    use super::DateTime;
111
112    #[test]
113    fn parses_correct_input() {
114        for (raw, expected) in [
115            (
116                "2014-11-28T21:00:09+09:00",
117                DateTime::builder()
118                    .year(2014)
119                    .month(11)
120                    .day(28)
121                    .hour(12)
122                    .second(9)
123                    .build()
124                    .unwrap(),
125            ),
126            (
127                "2014-11-28T21:00:09Z",
128                DateTime::builder()
129                    .year(2014)
130                    .month(11)
131                    .day(28)
132                    .hour(21)
133                    .second(9)
134                    .build()
135                    .unwrap(),
136            ),
137            (
138                "2014-11-28 21:00:09z",
139                DateTime::builder()
140                    .year(2014)
141                    .month(11)
142                    .day(28)
143                    .hour(21)
144                    .second(9)
145                    .build()
146                    .unwrap(),
147            ),
148            (
149                "2014-11-28T21:00:09+00:00",
150                DateTime::builder()
151                    .year(2014)
152                    .month(11)
153                    .day(28)
154                    .hour(21)
155                    .second(9)
156                    .build()
157                    .unwrap(),
158            ),
159            (
160                "2014-11-28T21:00:09.05+09:00",
161                DateTime::builder()
162                    .year(2014)
163                    .month(11)
164                    .day(28)
165                    .hour(12)
166                    .second(9)
167                    .millisecond(50)
168                    .build()
169                    .unwrap(),
170            ),
171            (
172                "2014-11-28 21:00:09.05+09:00",
173                DateTime::builder()
174                    .year(2014)
175                    .month(11)
176                    .day(28)
177                    .hour(12)
178                    .second(9)
179                    .millisecond(50)
180                    .build()
181                    .unwrap(),
182            ),
183        ] {
184            let input: InputValue = graphql_input_value!((raw));
185            let parsed = DateTime::from_input_value(&input);
186
187            assert!(
188                parsed.is_ok(),
189                "failed to parse `{raw}`: {:?}",
190                parsed.unwrap_err(),
191            );
192            assert_eq!(parsed.unwrap(), expected, "input: {raw}");
193        }
194    }
195
196    #[test]
197    fn fails_on_invalid_input() {
198        for input in [
199            graphql_input_value!("12"),
200            graphql_input_value!("12:"),
201            graphql_input_value!("56:34:22"),
202            graphql_input_value!("56:34:22.000"),
203            graphql_input_value!("1996-12-1914:23:43"),
204            graphql_input_value!("1996-12-19T14:23:43"),
205            graphql_input_value!("1996-12-19T14:23:43ZZ"),
206            graphql_input_value!("1996-12-19T14:23:43.543"),
207            graphql_input_value!("1996-12-19T14:23"),
208            graphql_input_value!("1996-12-19T14:23:1"),
209            graphql_input_value!("1996-12-19T14:23:"),
210            graphql_input_value!("1996-12-19T23:78:43Z"),
211            graphql_input_value!("1996-12-19T23:18:99Z"),
212            graphql_input_value!("1996-12-19T24:00:00Z"),
213            graphql_input_value!("1996-12-19T99:02:13Z"),
214            graphql_input_value!("1996-12-19T99:02:13Z"),
215            graphql_input_value!("1996-12-19T12:02:13+4444444"),
216            graphql_input_value!("i'm not even a datetime"),
217            graphql_input_value!(2.32),
218            graphql_input_value!(1),
219            graphql_input_value!(null),
220            graphql_input_value!(false),
221        ] {
222            let input: InputValue = input;
223            let parsed = DateTime::from_input_value(&input);
224
225            assert!(parsed.is_err(), "allows input: {input:?}");
226        }
227    }
228
229    #[test]
230    fn formats_correctly() {
231        for (val, expected) in [
232            (
233                DateTime::builder()
234                    .year(1996)
235                    .month(12)
236                    .day(19)
237                    .hour(12)
238                    .build()
239                    .unwrap(),
240                graphql_input_value!("1996-12-19T12:00:00Z"),
241            ),
242            (
243                DateTime::builder()
244                    .year(1564)
245                    .month(1)
246                    .day(30)
247                    .hour(5)
248                    .minute(3)
249                    .second(3)
250                    .millisecond(1)
251                    .build()
252                    .unwrap(),
253                graphql_input_value!("1564-01-30T05:03:03.001Z"),
254            ),
255        ] {
256            let actual: InputValue = val.to_input_value();
257
258            assert_eq!(actual, expected, "on value: {val}");
259        }
260    }
261}