made_core/value_objects/
duration.rs1use std::fmt;
8use std::ops::Add;
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::DomainError;
13
14#[derive(
16 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
17)]
18#[serde(transparent)]
19pub struct DurationMs(u64);
20
21impl DurationMs {
22 pub const ZERO: Self = Self(0);
23
24 #[must_use]
25 pub const fn from_millis(value: u64) -> Self {
26 Self(value)
27 }
28
29 #[must_use]
30 pub fn get(self) -> u64 {
31 self.0
32 }
33
34 #[must_use]
37 pub fn saturating_add(self, other: Self) -> Self {
38 Self(self.0.saturating_add(other.0))
39 }
40}
41
42impl Add for DurationMs {
43 type Output = Self;
44 fn add(self, rhs: Self) -> Self {
45 self.saturating_add(rhs)
46 }
47}
48
49impl fmt::Display for DurationMs {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "{}ms", self.0)
52 }
53}
54
55impl From<u64> for DurationMs {
56 fn from(value: u64) -> Self {
57 Self::from_millis(value)
58 }
59}
60
61impl TryFrom<i64> for DurationMs {
62 type Error = DomainError;
63 fn try_from(value: i64) -> Result<Self, Self::Error> {
64 if value < 0 {
65 return Err(DomainError::OutOfRange {
66 field: "duration_ms",
67 value: value as f64,
68 min: 0.0,
69 max: f64::from(u32::MAX),
70 });
71 }
72 Ok(Self(value as u64))
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn zero_constant_is_zero() {
82 assert_eq!(DurationMs::ZERO.get(), 0);
83 }
84
85 #[test]
86 fn from_millis_is_identity() {
87 assert_eq!(DurationMs::from_millis(250).get(), 250);
88 }
89
90 #[test]
91 fn negative_i64_is_rejected() {
92 assert!(DurationMs::try_from(-1_i64).is_err());
93 }
94
95 #[test]
96 fn non_negative_i64_is_accepted() {
97 assert_eq!(DurationMs::try_from(42_i64).unwrap().get(), 42);
98 }
99
100 #[test]
101 fn saturating_add_cannot_overflow() {
102 let big = DurationMs::from_millis(u64::MAX);
103 assert_eq!(
104 big.saturating_add(DurationMs::from_millis(1)).get(),
105 u64::MAX
106 );
107 }
108
109 #[test]
110 fn add_uses_saturating_semantics() {
111 let a = DurationMs::from_millis(u64::MAX - 1);
112 let b = DurationMs::from_millis(10);
113 assert_eq!((a + b).get(), u64::MAX);
114 }
115
116 #[test]
117 fn ordering_is_natural() {
118 assert!(DurationMs::from_millis(1) < DurationMs::from_millis(2));
119 }
120
121 #[test]
122 fn display_includes_unit() {
123 assert_eq!(DurationMs::from_millis(5).to_string(), "5ms");
124 }
125
126 #[test]
127 fn serde_is_transparent() {
128 assert_eq!(
129 serde_json::to_string(&DurationMs::from_millis(7)).unwrap(),
130 "7"
131 );
132 }
133}