1use serde::{Deserialize, Deserializer, Serialize, de};
4use uuid::Uuid;
5
6pub mod iso8601_offset {
38 use chrono::{DateTime, Utc};
39 use serde::{Deserialize, Deserializer, Serializer, de};
40
41 pub fn serialize<S>(dt: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
46 where
47 S: Serializer,
48 {
49 let formatted = dt.format("%Y-%m-%dT%H:%M:%S%.6f%:z").to_string();
50 s.serialize_str(&formatted)
51 }
52
53 pub fn deserialize<'de, D>(d: D) -> Result<DateTime<Utc>, D::Error>
56 where
57 D: Deserializer<'de>,
58 {
59 let s = String::deserialize(d)?;
60 DateTime::parse_from_rfc3339(&s)
61 .map(|dt| dt.with_timezone(&Utc))
62 .map_err(|err| de::Error::custom(format!("invalid RFC 3339 timestamp {s:?}: {err}")))
63 }
64}
65
66pub mod iso8601_offset_option {
84 use chrono::{DateTime, Utc};
85 use serde::{Deserialize, Deserializer, Serializer, de};
86
87 pub fn serialize<S>(dt: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error>
88 where
89 S: Serializer,
90 {
91 match dt {
92 Some(t) => {
93 let formatted = t.format("%Y-%m-%dT%H:%M:%S%.6f%:z").to_string();
94 s.serialize_some(&formatted)
95 }
96 None => s.serialize_none(),
97 }
98 }
99
100 pub fn deserialize<'de, D>(d: D) -> Result<Option<DateTime<Utc>>, D::Error>
101 where
102 D: Deserializer<'de>,
103 {
104 let opt: Option<String> = Option::deserialize(d)?;
105 match opt {
106 None => Ok(None),
107 Some(s) => DateTime::parse_from_rfc3339(&s)
108 .map(|dt| Some(dt.with_timezone(&Utc)))
109 .map_err(|err| {
110 de::Error::custom(format!("invalid RFC 3339 timestamp {s:?}: {err}"))
111 }),
112 }
113 }
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq)]
143pub struct DatasetIdRef(pub Option<Uuid>);
144
145impl Serialize for DatasetIdRef {
146 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147 where
148 S: serde::Serializer,
149 {
150 self.0.serialize(serializer)
151 }
152}
153
154impl DatasetIdRef {
155 pub fn into_inner(self) -> Option<Uuid> {
157 self.0
158 }
159
160 pub fn as_option(&self) -> Option<Uuid> {
162 self.0
163 }
164}
165
166impl From<DatasetIdRef> for Option<Uuid> {
167 fn from(d: DatasetIdRef) -> Self {
168 d.0
169 }
170}
171
172impl utoipa::ToSchema for DatasetIdRef {
175 fn name() -> std::borrow::Cow<'static, str> {
176 std::borrow::Cow::Borrowed("DatasetIdRef")
177 }
178}
179
180impl utoipa::PartialSchema for DatasetIdRef {
181 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
182 utoipa::openapi::RefOr::T(utoipa::openapi::Schema::Object(
184 utoipa::openapi::ObjectBuilder::new()
185 .schema_type(utoipa::openapi::schema::Type::String)
186 .description(Some(
187 "Optional dataset UUID. Null, empty string, or a valid UUID string.",
188 ))
189 .build(),
190 ))
191 }
192}
193
194impl<'de> Deserialize<'de> for DatasetIdRef {
195 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196 where
197 D: Deserializer<'de>,
198 {
199 let opt: Option<String> = Option::deserialize(deserializer)?;
202 match opt {
203 None => Ok(DatasetIdRef(None)),
204 Some(s) if s.trim().is_empty() => Ok(DatasetIdRef(None)),
205 Some(s) => {
206 let uuid = Uuid::parse_str(&s).map_err(|_| {
207 de::Error::custom(format!(
208 "invalid dataset_id: expected a UUID string or empty, got {s:?}"
209 ))
210 })?;
211 Ok(DatasetIdRef(Some(uuid)))
212 }
213 }
214 }
215}
216
217#[cfg(test)]
220#[allow(
221 clippy::unwrap_used,
222 clippy::expect_used,
223 reason = "test code — panics are acceptable failures"
224)]
225mod tests {
226 use super::*;
227 use serde::Deserialize;
228 use serde_json::json;
229
230 #[derive(Debug, Deserialize)]
231 struct Wrapper {
232 #[serde(default)]
233 id: DatasetIdRef,
234 }
235
236 fn parse(v: serde_json::Value) -> Result<DatasetIdRef, serde_json::Error> {
237 #[derive(Deserialize)]
238 struct W {
239 id: DatasetIdRef,
240 }
241 let w: W = serde_json::from_value(json!({ "id": v }))?;
242 Ok(w.id)
243 }
244
245 #[test]
246 fn null_deserialises_to_none() {
247 let result = parse(json!(null)).expect("should succeed");
248 assert_eq!(result, DatasetIdRef(None));
249 }
250
251 #[test]
252 fn empty_string_deserialises_to_none() {
253 let result = parse(json!("")).expect("should succeed");
254 assert_eq!(result, DatasetIdRef(None));
255 }
256
257 #[test]
258 fn whitespace_only_string_deserialises_to_none() {
259 let result = parse(json!(" ")).expect("should succeed");
260 assert_eq!(result, DatasetIdRef(None));
261 }
262
263 #[test]
264 fn valid_uuid_deserialises_to_some() {
265 let id = Uuid::new_v4();
266 let result = parse(json!(id.to_string())).expect("should succeed");
267 assert_eq!(result, DatasetIdRef(Some(id)));
268 }
269
270 #[test]
271 fn invalid_uuid_string_is_rejected() {
272 let err = parse(json!("not-a-uuid")).expect_err("should fail");
273 assert!(
274 err.to_string().contains("invalid dataset_id"),
275 "error message should mention the field: {err}"
276 );
277 }
278
279 #[test]
280 fn non_string_scalar_is_rejected() {
281 let err = parse(json!(42)).expect_err("should fail for integer");
282 assert!(!err.to_string().is_empty());
284 }
285
286 #[test]
287 fn default_is_none() {
288 let w: Wrapper = serde_json::from_str("{}").expect("empty object");
289 assert_eq!(w.id, DatasetIdRef(None));
290 }
291
292 #[test]
293 fn into_inner_works() {
294 let id = Uuid::new_v4();
295 let d = DatasetIdRef(Some(id));
296 assert_eq!(d.into_inner(), Some(id));
297
298 let none = DatasetIdRef(None);
299 assert_eq!(none.into_inner(), None);
300 }
301
302 use chrono::{DateTime, TimeZone, Utc};
305 use serde::Serialize;
306
307 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
308 struct TsWrapper {
309 #[serde(with = "super::iso8601_offset")]
310 ts: DateTime<Utc>,
311 }
312
313 #[test]
314 fn serializes_utc_with_plus_zero_zero() {
315 let ts = Utc
317 .with_ymd_and_hms(2026, 4, 29, 14, 32, 1)
318 .single()
319 .expect("valid UTC datetime");
320 let w = TsWrapper { ts };
321 let s = serde_json::to_string(&w).expect("serialize");
322 assert!(
323 s.contains("\"2026-04-29T14:32:01.000000+00:00\""),
324 "expected +00:00 offset in: {s}"
325 );
326 assert!(
327 !s.contains("Z\""),
328 "should not emit chrono's default Z suffix: {s}"
329 );
330 }
331
332 #[test]
333 fn deserializes_z_suffix() {
334 let json = r#"{"ts":"2026-04-29T14:32:01Z"}"#;
335 let w: TsWrapper = serde_json::from_str(json).expect("Z suffix should parse");
336 let expected = Utc
337 .with_ymd_and_hms(2026, 4, 29, 14, 32, 1)
338 .single()
339 .expect("valid UTC datetime");
340 assert_eq!(w.ts, expected);
341 }
342
343 #[test]
344 fn deserializes_plus_zero_zero() {
345 let json = r#"{"ts":"2026-04-29T14:32:01+00:00"}"#;
346 let w: TsWrapper = serde_json::from_str(json).expect("+00:00 offset should parse");
347 let expected = Utc
348 .with_ymd_and_hms(2026, 4, 29, 14, 32, 1)
349 .single()
350 .expect("valid UTC datetime");
351 assert_eq!(w.ts, expected);
352 }
353
354 #[test]
355 fn round_trip_microsecond_precision() {
356 let json = r#"{"ts":"2026-04-29T14:32:01.123456+00:00"}"#;
357 let w: TsWrapper = serde_json::from_str(json).expect("microsecond input");
358 let s = serde_json::to_string(&w).expect("serialize");
359 assert!(
360 s.contains("\"2026-04-29T14:32:01.123456+00:00\""),
361 "round-trip should preserve microseconds: {s}"
362 );
363 }
364
365 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
368 struct OptTsWrapper {
369 #[serde(with = "super::iso8601_offset_option", default)]
370 ts: Option<DateTime<Utc>>,
371 }
372
373 #[test]
374 fn iso8601_offset_option_round_trip_some_and_none() {
375 let ts = Utc
377 .with_ymd_and_hms(2026, 4, 29, 14, 32, 1)
378 .single()
379 .expect("valid UTC datetime");
380 let w = OptTsWrapper { ts: Some(ts) };
381 let s = serde_json::to_string(&w).expect("serialize Some");
382 assert!(
383 s.contains("\"2026-04-29T14:32:01.000000+00:00\""),
384 "Some(...) should emit +00:00 offset shape: {s}"
385 );
386
387 let w_none = OptTsWrapper { ts: None };
389 let s_none = serde_json::to_string(&w_none).expect("serialize None");
390 assert_eq!(s_none, r#"{"ts":null}"#);
391
392 let parsed_null: OptTsWrapper = serde_json::from_str(r#"{"ts":null}"#).expect("null");
394 assert_eq!(parsed_null.ts, None);
395
396 let parsed_some: OptTsWrapper =
397 serde_json::from_str(r#"{"ts":"2026-04-29T14:32:01.000000+00:00"}"#).expect("some");
398 assert_eq!(parsed_some.ts, Some(ts));
399 }
400
401 #[test]
402 fn truncates_nanoseconds_to_microseconds_on_serialize() {
403 let ts = Utc
406 .with_ymd_and_hms(2026, 4, 29, 14, 32, 1)
407 .single()
408 .expect("valid UTC datetime")
409 + chrono::Duration::nanoseconds(123_456_789);
410 let w = TsWrapper { ts };
411 let s = serde_json::to_string(&w).expect("serialize");
412 assert!(
413 s.contains("\"2026-04-29T14:32:01.123456+00:00\""),
414 "expected microsecond truncation, got: {s}"
415 );
416 assert!(
417 !s.contains("123456789"),
418 "nanoseconds should be truncated, got: {s}"
419 );
420 }
421}