Skip to main content

gnss_time/
serde_impls.rs

1//! Serde support for `gnss-time` types
2//!
3//! Enabled with the `serde` feature flag:
4//!
5//! ```toml
6//! [dependensies]
7//! gnss-time = { version = "0.5", features = ["serde"] }
8//! ```
9//!
10//! # Formats
11//!
12//! ### `Time<S>`
13//!
14//! **Human-readable** (JSON, TOML, YAML, ...):
15//!
16//! ```json
17//! { "scale": "GPS", "nanos": 1356566418000000000 }
18//! ```
19//!
20//! The `scale` field is validated during deserialization — deserializing a
21//! JSON object with `"scale": "UTC"` into `Time<Gps>` returns an error.
22//!
23//! **Compact** (postcard, bincode, `MessagePack`, …):
24//!
25//! Raw `u64` nanoseconds. No scale tag is stored.
26//!
27//! ### `Duration`
28//!
29//! **Human-readable**: `{ "nanos": -7000000000 }`
30//!
31//! **Compact**: raw `i64` nanoseconds.
32//!
33//! ### `DurationParts`
34//!
35//! **Human-readable**: `{ "seconds": 5, "nanos": 500000000 }`
36//!
37//! **Compact**: 2-element tuple `[u64, u32]`.
38//!
39//! ## `no_std` compatibility
40//!
41//! All implementations use `serde`'s `no_std`-compatible API.
42//!
43//! ## Examples
44//!
45//! ```rust
46//! # #[cfg(feature = "serde")] {
47//! use gnss_time::{Duration, DurationParts, Gps, Time};
48//!
49//! // Time<Gps> — JSON round-trip
50//! let gps = Time::<Gps>::from_seconds(1_356_566_418);
51//! let json = serde_json::to_string(&gps).unwrap();
52//! assert_eq!(json, r#"{"scale":"GPS","nanos":1356566418000000000}"#);
53//! let back: Time<Gps> = serde_json::from_str(&json).unwrap();
54//! assert_eq!(gps, back);
55//!
56//! // Duration — JSON round-trip
57//! let d = Duration::from_seconds(-7);
58//! let json = serde_json::to_string(&d).unwrap();
59//! assert_eq!(json, r#"{"nanos":-7000000000}"#);
60//! let back: Duration = serde_json::from_str(&json).unwrap();
61//! assert_eq!(d, back);
62//! # }
63//! ```
64
65use core::{fmt, marker::PhantomData};
66
67use serde::{
68    de::{self, Deserializer, MapAccess, SeqAccess, Visitor},
69    ser::{SerializeStruct, SerializeTuple, Serializer},
70    Deserialize, Serialize,
71};
72
73use crate::{Duration, DurationParts, GnssTimeError, Time, TimeScale};
74
75const TIME_FIELDS: &[&str] = &["scale", "nanos"];
76const DURATION_PARTS_FIELDS: &[&str] = &["seconds", "nanos"];
77
78enum TimeField {
79    Scale,
80    Nanos,
81}
82
83enum DurationField {
84    Nanos,
85}
86
87enum DurationPartsField {
88    Seconds,
89    Nanos,
90}
91
92struct TimeVisitor<S: TimeScale>(PhantomData<S>);
93
94// Error helper that implements `fmt::Display` without allocation.
95struct ScaleMismatch<'a> {
96    expected: &'a str,
97    got: &'a str,
98}
99
100struct DurationVisitor;
101
102struct DurationPartsMapVisitor;
103
104struct DurationPartsTupleVisitor;
105
106impl<S: TimeScale> Serialize for Time<S> {
107    fn serialize<Ser: Serializer>(
108        &self,
109        serializer: Ser,
110    ) -> Result<Ser::Ok, Ser::Error> {
111        if serializer.is_human_readable() {
112            // JSON / TOML: { "scale": "GPS", "nanos": 12345678 }
113            let mut s = serializer.serialize_struct("Time", 2)?;
114            s.serialize_field("scale", S::NAME)?;
115            s.serialize_field("nanos", &self.as_nanos())?;
116            s.end()
117        } else {
118            // postcard / bincode: raw u64 nanoseconds
119            serializer.serialize_u64(self.as_nanos())
120        }
121    }
122}
123
124impl<'de, S: TimeScale> Deserialize<'de> for Time<S> {
125    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
126        if deserializer.is_human_readable() {
127            deserializer.deserialize_struct("Time", TIME_FIELDS, TimeVisitor::<S>(PhantomData))
128        } else {
129            let nanos = u64::deserialize(deserializer)?;
130            Ok(Time::from_nanos(nanos))
131        }
132    }
133}
134
135impl<'de, S: TimeScale> Visitor<'de> for TimeVisitor<S> {
136    type Value = Time<S>;
137
138    fn expecting(
139        &self,
140        f: &mut fmt::Formatter<'_>,
141    ) -> fmt::Result {
142        write!(f, r#"a map {{ "scale": "{}", "nanos": u64 }}"#, S::NAME)
143    }
144
145    fn visit_map<A: MapAccess<'de>>(
146        self,
147        mut map: A,
148    ) -> Result<Self::Value, A::Error> {
149        let mut nanos: Option<u64> = None;
150
151        while let Some(key) = map.next_key::<TimeField>()? {
152            match key {
153                TimeField::Scale => {
154                    let value: &str = map.next_value()?;
155                    if value != S::NAME {
156                        return Err(de::Error::custom(ScaleMismatch {
157                            expected: S::NAME,
158                            got: value,
159                        }));
160                    }
161                }
162                TimeField::Nanos => {
163                    nanos = Some(map.next_value()?);
164                }
165            }
166        }
167
168        let nanos = nanos.ok_or_else(|| de::Error::missing_field("nanos"))?;
169        Ok(Time::from_nanos(nanos))
170    }
171}
172
173impl<'de> Deserialize<'de> for TimeField {
174    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
175        struct TimeFieldVisitor;
176
177        impl Visitor<'_> for TimeFieldVisitor {
178            type Value = TimeField;
179
180            fn expecting(
181                &self,
182                f: &mut fmt::Formatter<'_>,
183            ) -> fmt::Result {
184                f.write_str("`scale` or `nanos`")
185            }
186
187            fn visit_str<E: de::Error>(
188                self,
189                v: &str,
190            ) -> Result<TimeField, E> {
191                match v {
192                    "scale" => Ok(TimeField::Scale),
193                    "nanos" => Ok(TimeField::Nanos),
194                    other => Err(de::Error::unknown_field(other, TIME_FIELDS)),
195                }
196            }
197        }
198
199        deserializer.deserialize_identifier(TimeFieldVisitor)
200    }
201}
202
203impl Serialize for Duration {
204    fn serialize<Ser: Serializer>(
205        &self,
206        serializer: Ser,
207    ) -> Result<Ser::Ok, Ser::Error> {
208        if serializer.is_human_readable() {
209            // { "nanos": -7000000000 }
210            let mut s = serializer.serialize_struct("Duration", 1)?;
211
212            s.serialize_field("nanos", &self.as_nanos())?;
213            s.end()
214        } else {
215            serializer.serialize_i64(self.as_nanos())
216        }
217    }
218}
219
220impl<'de> Deserialize<'de> for Duration {
221    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
222        if deserializer.is_human_readable() {
223            deserializer.deserialize_struct("Duration", &["nanos"], DurationVisitor)
224        } else {
225            let nanos = i64::deserialize(deserializer)?;
226
227            Ok(Duration::from_nanos(nanos))
228        }
229    }
230}
231
232impl<'de> Visitor<'de> for DurationVisitor {
233    type Value = Duration;
234
235    fn expecting(
236        &self,
237        f: &mut fmt::Formatter<'_>,
238    ) -> fmt::Result {
239        f.write_str(r#"a map { "nanos": i64 }"#)
240    }
241
242    fn visit_map<A: MapAccess<'de>>(
243        self,
244        mut map: A,
245    ) -> Result<Duration, A::Error> {
246        let mut nanos: Option<i64> = None;
247
248        while let Some(key) = map.next_key::<DurationField>()? {
249            match key {
250                DurationField::Nanos => {
251                    nanos = Some(map.next_value()?);
252                }
253            }
254        }
255
256        let nanos = nanos.ok_or_else(|| de::Error::missing_field("nanos"))?;
257
258        Ok(Duration::from_nanos(nanos))
259    }
260}
261
262impl<'de> Deserialize<'de> for DurationField {
263    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
264        struct DurationFieldVisitor;
265
266        impl Visitor<'_> for DurationFieldVisitor {
267            type Value = DurationField;
268
269            fn expecting(
270                &self,
271                f: &mut fmt::Formatter<'_>,
272            ) -> fmt::Result {
273                f.write_str("`nanos`")
274            }
275
276            fn visit_str<E: de::Error>(
277                self,
278                v: &str,
279            ) -> Result<DurationField, E> {
280                match v {
281                    "nanos" => Ok(DurationField::Nanos),
282                    other => Err(de::Error::unknown_field(other, &["nanos"])),
283                }
284            }
285        }
286
287        deserializer.deserialize_identifier(DurationFieldVisitor)
288    }
289}
290
291impl Serialize for DurationParts {
292    fn serialize<Ser: Serializer>(
293        &self,
294        serializer: Ser,
295    ) -> Result<Ser::Ok, Ser::Error> {
296        if serializer.is_human_readable() {
297            // { "seconds": 5, "nanos": 500000000 }
298            let mut s = serializer.serialize_struct("DurationParts", 2)?;
299
300            s.serialize_field("seconds", &self.seconds)?;
301            s.serialize_field("nanos", &self.nanos)?;
302            s.end()
303        } else {
304            // Compact: [u64, u32]
305            let mut t = serializer.serialize_tuple(2)?;
306
307            t.serialize_element(&self.seconds)?;
308            t.serialize_element(&self.nanos)?;
309            t.end()
310        }
311    }
312}
313
314impl<'de> Deserialize<'de> for DurationParts {
315    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
316        if deserializer.is_human_readable() {
317            deserializer.deserialize_struct(
318                "DurationParts",
319                DURATION_PARTS_FIELDS,
320                DurationPartsMapVisitor,
321            )
322        } else {
323            deserializer.deserialize_tuple(2, DurationPartsTupleVisitor)
324        }
325    }
326}
327
328impl<'de> Visitor<'de> for DurationPartsMapVisitor {
329    type Value = DurationParts;
330
331    fn expecting(
332        &self,
333        f: &mut fmt::Formatter<'_>,
334    ) -> fmt::Result {
335        f.write_str(r#"a map { "seconds": u64, "nanos": u32 }"#)
336    }
337
338    fn visit_map<A: MapAccess<'de>>(
339        self,
340        mut map: A,
341    ) -> Result<DurationParts, A::Error> {
342        let mut seconds: Option<u64> = None;
343        let mut nanos: Option<u32> = None;
344
345        while let Some(key) = map.next_key::<DurationPartsField>()? {
346            match key {
347                DurationPartsField::Seconds => {
348                    seconds = Some(map.next_value()?);
349                }
350                DurationPartsField::Nanos => {
351                    nanos = Some(map.next_value()?);
352                }
353            }
354        }
355
356        let seconds = seconds.ok_or_else(|| de::Error::missing_field("seconds"))?;
357        let nanos = nanos.ok_or_else(|| de::Error::missing_field("nanos"))?;
358
359        DurationParts::new(seconds, nanos).map_err(|e| match e {
360            GnssTimeError::InvalidInput(msg) => de::Error::custom(msg),
361            _ => de::Error::custom("invalid DurationParts"),
362        })
363    }
364}
365
366impl<'de> Deserialize<'de> for DurationPartsField {
367    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
368        struct DurationPartsFieldVisitor;
369
370        impl Visitor<'_> for DurationPartsFieldVisitor {
371            type Value = DurationPartsField;
372
373            fn expecting(
374                &self,
375                f: &mut fmt::Formatter<'_>,
376            ) -> fmt::Result {
377                f.write_str("`seconds` or `nanos`")
378            }
379
380            fn visit_str<E: de::Error>(
381                self,
382                v: &str,
383            ) -> Result<DurationPartsField, E> {
384                match v {
385                    "seconds" => Ok(DurationPartsField::Seconds),
386                    "nanos" => Ok(DurationPartsField::Nanos),
387                    other => Err(de::Error::unknown_field(other, DURATION_PARTS_FIELDS)),
388                }
389            }
390        }
391
392        deserializer.deserialize_identifier(DurationPartsFieldVisitor)
393    }
394}
395
396impl<'de> Visitor<'de> for DurationPartsTupleVisitor {
397    type Value = DurationParts;
398
399    fn expecting(
400        &self,
401        f: &mut fmt::Formatter<'_>,
402    ) -> fmt::Result {
403        f.write_str("a 2-element tuple [u64, u32]")
404    }
405
406    fn visit_seq<A: SeqAccess<'de>>(
407        self,
408        mut seq: A,
409    ) -> Result<DurationParts, A::Error> {
410        let seconds: u64 = seq
411            .next_element()?
412            .ok_or_else(|| de::Error::invalid_length(0, &self))?;
413        let nanos: u32 = seq
414            .next_element()?
415            .ok_or_else(|| de::Error::invalid_length(1, &self))?;
416
417        DurationParts::new(seconds, nanos).map_err(|e| match e {
418            GnssTimeError::InvalidInput(msg) => de::Error::custom(msg),
419            _ => de::Error::custom("invalid DurationParts"),
420        })
421    }
422}
423
424impl fmt::Display for ScaleMismatch<'_> {
425    fn fmt(
426        &self,
427        f: &mut fmt::Formatter<'_>,
428    ) -> fmt::Result {
429        write!(
430            f,
431            "scale mismatch: expected \"{}\", got \"{}\"",
432            self.expected, self.got,
433        )
434    }
435}
436
437////////////////////////////////////////////////////////////////////////////////
438// Tests
439////////////////////////////////////////////////////////////////////////////////
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use crate::{std::string::ToString, Beidou, Galileo, Glonass, Gps, Tai, Utc};
445
446    #[test]
447    fn test_gps_serialize_json_exact_format() {
448        let t = Time::<Gps>::from_seconds(1_356_566_418);
449        let json = serde_json::to_string(&t).unwrap();
450
451        assert_eq!(json, r#"{"scale":"GPS","nanos":1356566418000000000}"#);
452    }
453
454    #[test]
455    fn test_gps_deserialize_json() {
456        let json = r#"{"scale":"GPS","nanos":1356566418000000000}"#;
457        let t: Time<Gps> = serde_json::from_str(json).unwrap();
458
459        assert_eq!(t, Time::<Gps>::from_seconds(1_356_566_418));
460    }
461
462    #[test]
463    fn test_gps_json_roundtrip_with_sub_second() {
464        let original = Time::<Gps>::from_nanos(1_356_566_418_123_456_789);
465        let json = serde_json::to_string(&original).unwrap();
466        let back: Time<Gps> = serde_json::from_str(&json).unwrap();
467
468        assert_eq!(original, back);
469    }
470
471    #[test]
472    fn test_gps_epoch_json_roundtrip() {
473        let t = Time::<Gps>::EPOCH;
474        let json = serde_json::to_string(&t).unwrap();
475
476        assert_eq!(json, r#"{"scale":"GPS","nanos":0}"#);
477
478        let back: Time<Gps> = serde_json::from_str(&json).unwrap();
479
480        assert_eq!(t, back);
481    }
482
483    #[test]
484    fn test_gps_max_json_roundtrip() {
485        let t = Time::<Gps>::MAX;
486        let json = serde_json::to_string(&t).unwrap();
487        let back: Time<Gps> = serde_json::from_str(&json).unwrap();
488
489        assert_eq!(t, back);
490    }
491
492    #[test]
493    fn test_utc_json_roundtrip() {
494        let t = Time::<Utc>::from_nanos(1_514_764_800_000_000_000);
495        let json = serde_json::to_string(&t).unwrap();
496        let back: Time<Utc> = serde_json::from_str(&json).unwrap();
497
498        assert_eq!(t, back);
499    }
500
501    #[test]
502    fn test_tai_json_roundtrip() {
503        let t = Time::<Tai>::from_seconds(100_000_000);
504        let json = serde_json::to_string(&t).unwrap();
505        let back: Time<Tai> = serde_json::from_str(&json).unwrap();
506
507        assert_eq!(t, back);
508    }
509
510    #[test]
511    fn test_galileo_json_roundtrip() {
512        let t = Time::<Galileo>::from_nanos(999_999_999_999);
513        let json = serde_json::to_string(&t).unwrap();
514        let back: Time<Galileo> = serde_json::from_str(&json).unwrap();
515
516        assert_eq!(t, back);
517    }
518
519    #[test]
520    fn test_beidou_json_roundtrip() {
521        let t = Time::<Beidou>::EPOCH;
522        let json = serde_json::to_string(&t).unwrap();
523        let back: Time<Beidou> = serde_json::from_str(&json).unwrap();
524
525        assert_eq!(t, back);
526    }
527
528    #[test]
529    fn test_glonass_json_roundtrip() {
530        let t = Time::<Glonass>::from_nanos(42_000_000_000);
531        let json = serde_json::to_string(&t).unwrap();
532        let back: Time<Glonass> = serde_json::from_str(&json).unwrap();
533
534        assert_eq!(t, back);
535    }
536
537    #[test]
538    fn test_all_scales_json_scale_field() {
539        let gps_v: serde_json::Value = serde_json::to_value(Time::<Gps>::EPOCH).unwrap();
540        let utc_v: serde_json::Value = serde_json::to_value(Time::<Utc>::EPOCH).unwrap();
541        let tai_v: serde_json::Value = serde_json::to_value(Time::<Tai>::EPOCH).unwrap();
542        let gal_v: serde_json::Value = serde_json::to_value(Time::<Galileo>::EPOCH).unwrap();
543        let bdt_v: serde_json::Value = serde_json::to_value(Time::<Beidou>::EPOCH).unwrap();
544        let glo_v: serde_json::Value = serde_json::to_value(Time::<Glonass>::EPOCH).unwrap();
545
546        assert_eq!(gps_v["scale"], "GPS");
547        assert_eq!(utc_v["scale"], "UTC");
548        assert_eq!(tai_v["scale"], "TAI");
549        assert_eq!(gal_v["scale"], "GAL");
550        assert_eq!(bdt_v["scale"], "BDT");
551        assert_eq!(glo_v["scale"], "GLO");
552    }
553
554    #[test]
555    fn test_scale_mismatch_gps_into_utc_fails() {
556        let json = r#"{"scale":"GPS","nanos":0}"#;
557        let result: Result<Time<Utc>, _> = serde_json::from_str(json);
558
559        assert!(result.is_err(), "scale mismatch must fail");
560
561        let msg = result.unwrap_err().to_string();
562
563        assert!(
564            msg.contains("GPS") || msg.contains("UTC") || msg.contains("scale"),
565            "error should mention scale: {msg}"
566        );
567    }
568
569    #[test]
570    fn test_scale_mismatch_tai_into_gps_fails() {
571        let json = r#"{"scale":"TAI","nanos":0}"#;
572        let result: Result<Time<Gps>, _> = serde_json::from_str(json);
573
574        assert!(result.is_err());
575    }
576
577    #[test]
578    fn test_deserialize_without_scale_field_uses_nanos() {
579        // scale field is optional — only validated when present
580        let json = r#"{"nanos":12345678}"#;
581        let t: Time<Gps> = serde_json::from_str(json).unwrap();
582
583        assert_eq!(t.as_nanos(), 12_345_678);
584    }
585
586    #[test]
587    fn test_deserialize_missing_nanos_field_fails() {
588        let json = r#"{"scale":"GPS"}"#;
589        let result: Result<Time<Gps>, _> = serde_json::from_str(json);
590
591        assert!(result.is_err());
592    }
593
594    #[test]
595    fn test_gps_postcard_roundtrip() {
596        let original = Time::<Gps>::from_nanos(1_356_566_418_123_456_789);
597        let bytes = postcard::to_allocvec(&original).unwrap();
598        let back: Time<Gps> = postcard::from_bytes(&bytes).unwrap();
599
600        assert_eq!(original, back);
601    }
602
603    #[test]
604    fn test_utc_postcard_roundtrip() {
605        let t = Time::<Utc>::from_nanos(1_514_764_800_000_000_000);
606        let bytes = postcard::to_allocvec(&t).unwrap();
607        let back: Time<Utc> = postcard::from_bytes(&bytes).unwrap();
608
609        assert_eq!(t, back);
610    }
611
612    #[test]
613    fn test_gps_epoch_postcard_roundtrip() {
614        let t = Time::<Gps>::EPOCH;
615        let bytes = postcard::to_allocvec(&t).unwrap();
616        let back: Time<Gps> = postcard::from_bytes(&bytes).unwrap();
617
618        assert_eq!(t, back);
619    }
620
621    #[test]
622    fn test_gps_max_postcard_roundtrip() {
623        let t = Time::<Gps>::MAX;
624        let bytes = postcard::to_allocvec(&t).unwrap();
625        let back: Time<Gps> = postcard::from_bytes(&bytes).unwrap();
626
627        assert_eq!(t, back);
628    }
629
630    #[test]
631    fn test_postcard_is_smaller_than_json() {
632        let t = Time::<Gps>::from_seconds(1_000_000);
633        let json_len = serde_json::to_vec(&t).unwrap().len();
634        let postcard_len = postcard::to_allocvec(&t).unwrap().len();
635
636        assert!(
637            postcard_len < json_len,
638            "postcard ({postcard_len}B) should be smaller than JSON ({json_len}B)"
639        );
640    }
641
642    #[test]
643    fn test_duration_serialize_json_exact() {
644        let d = Duration::from_seconds(-7);
645        let json = serde_json::to_string(&d).unwrap();
646
647        assert_eq!(json, r#"{"nanos":-7000000000}"#);
648    }
649
650    #[test]
651    fn test_duration_zero_json() {
652        let d = Duration::ZERO;
653        let json = serde_json::to_string(&d).unwrap();
654
655        assert_eq!(json, r#"{"nanos":0}"#);
656
657        let back: Duration = serde_json::from_str(&json).unwrap();
658
659        assert_eq!(d, back);
660    }
661
662    #[test]
663    fn test_duration_json_roundtrip_cases() {
664        let cases = [
665            Duration::ZERO,
666            Duration::from_seconds(42),
667            Duration::from_seconds(-100),
668            Duration::from_nanos(1),
669            Duration::from_nanos(-1),
670            Duration::from_millis(500),
671            Duration::MAX,
672            Duration::MIN,
673        ];
674        for d in cases {
675            let json = serde_json::to_string(&d).unwrap();
676            let back: Duration = serde_json::from_str(&json).unwrap();
677
678            assert_eq!(d, back, "round-trip failed for {d:?}");
679        }
680    }
681
682    #[test]
683    fn test_duration_missing_nanos_field_fails() {
684        let result: Result<Duration, _> = serde_json::from_str(r"{}");
685
686        assert!(result.is_err());
687    }
688
689    #[test]
690    fn test_duration_postcard_roundtrip() {
691        let cases = [
692            Duration::ZERO,
693            Duration::from_seconds(1),
694            Duration::from_seconds(-1),
695            Duration::from_nanos(123_456_789),
696            Duration::MAX,
697            Duration::MIN,
698        ];
699        for d in cases {
700            let bytes = postcard::to_allocvec(&d).unwrap();
701            let back: Duration = postcard::from_bytes(&bytes).unwrap();
702
703            assert_eq!(d, back);
704        }
705    }
706
707    #[test]
708    fn test_duration_parts_serialize_json_exact() {
709        let p = DurationParts {
710            seconds: 5,
711            nanos: 500_000_000,
712        };
713        let json = serde_json::to_string(&p).unwrap();
714
715        assert_eq!(json, r#"{"seconds":5,"nanos":500000000}"#);
716    }
717
718    #[test]
719    fn test_duration_parts_zero_json() {
720        let p = DurationParts {
721            seconds: 0,
722            nanos: 0,
723        };
724        let json = serde_json::to_string(&p).unwrap();
725
726        assert_eq!(json, r#"{"seconds":0,"nanos":0}"#);
727
728        let back: DurationParts = serde_json::from_str(&json).unwrap();
729
730        assert_eq!(p, back);
731    }
732
733    #[test]
734    fn test_duration_parts_json_roundtrip() {
735        let cases = [
736            DurationParts {
737                seconds: 0,
738                nanos: 0,
739            },
740            DurationParts {
741                seconds: 1,
742                nanos: 0,
743            },
744            DurationParts {
745                seconds: 86_400,
746                nanos: 123_456_789,
747            },
748            DurationParts {
749                seconds: 604_799,
750                nanos: 999_999_999,
751            },
752        ];
753        for p in cases {
754            let json = serde_json::to_string(&p).unwrap();
755            let back: DurationParts = serde_json::from_str(&json).unwrap();
756
757            assert_eq!(p, back);
758        }
759    }
760
761    #[test]
762    fn test_duration_parts_invalid_nanos_rejected() {
763        // nanos >= 1_000_000_000 must be rejected
764        let json = r#"{"seconds":0,"nanos":1000000000}"#;
765        let result: Result<DurationParts, _> = serde_json::from_str(json);
766
767        assert!(result.is_err(), "nanos >= 1_000_000_000 must fail");
768    }
769
770    #[test]
771    fn test_duration_parts_missing_seconds_fails() {
772        let result: Result<DurationParts, _> = serde_json::from_str(r#"{"nanos":0}"#);
773
774        assert!(result.is_err());
775    }
776
777    #[test]
778    fn test_duration_parts_missing_nanos_fails() {
779        let result: Result<DurationParts, _> = serde_json::from_str(r#"{"seconds":0}"#);
780
781        assert!(result.is_err());
782    }
783
784    #[test]
785    fn test_duration_parts_postcard_roundtrip() {
786        let cases = [
787            DurationParts {
788                seconds: 0,
789                nanos: 0,
790            },
791            DurationParts {
792                seconds: 86_400,
793                nanos: 123_456_789,
794            },
795            DurationParts {
796                seconds: u64::MAX / 1_000_000_000,
797                nanos: 999_999_999,
798            },
799        ];
800
801        for p in cases {
802            let bytes = postcard::to_allocvec(&p).unwrap();
803            let back: DurationParts = postcard::from_bytes(&bytes).unwrap();
804
805            assert_eq!(p, back);
806        }
807    }
808
809    #[test]
810    fn test_from_week_tow_json_roundtrip() {
811        let tow = DurationParts {
812            seconds: 432_000,
813            nanos: 0,
814        };
815        let gps = Time::<Gps>::from_week_tow(2345, tow).unwrap();
816
817        // Both types roundtrip independently
818        let gps_json = serde_json::to_string(&gps).unwrap();
819        let tow_json = serde_json::to_string(&tow).unwrap();
820
821        let gps_back: Time<Gps> = serde_json::from_str(&gps_json).unwrap();
822        let tow_back: DurationParts = serde_json::from_str(&tow_json).unwrap();
823
824        assert_eq!(gps, gps_back);
825        assert_eq!(tow, tow_back);
826
827        // Reconstructed GPS timestamp matches original
828        let gps_from_tow = Time::<Gps>::from_week_tow(2345, tow_back).unwrap();
829
830        assert_eq!(gps, gps_from_tow);
831    }
832
833    #[test]
834    fn test_time_duration_combined_json() {
835        // Simulate a "timestamp + offset" structure
836        let t = Time::<Gps>::from_seconds(1_000_000);
837        let d = Duration::from_seconds(3600);
838
839        let t_json = serde_json::to_string(&t).unwrap();
840        let d_json = serde_json::to_string(&d).unwrap();
841
842        let t_back: Time<Gps> = serde_json::from_str(&t_json).unwrap();
843        let d_back: Duration = serde_json::from_str(&d_json).unwrap();
844
845        assert_eq!(t + d, t_back + d_back);
846    }
847}