1use core::fmt;
4use core::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8const KIB: u64 = 1024;
11const MIB: u64 = 1024 * KIB;
12const GIB: u64 = 1024 * MIB;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct MemSize(u64);
33
34impl MemSize {
35 #[inline]
37 #[must_use]
38 pub const fn from_bytes(bytes: u64) -> Self {
39 Self(bytes)
40 }
41
42 #[inline]
44 #[must_use]
45 pub const fn bytes(self) -> u64 {
46 self.0
47 }
48}
49
50impl FromStr for MemSize {
51 type Err = ParseMemSizeError;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
64 if s.is_empty() {
65 return Err(ParseMemSizeError::Empty);
66 }
67 let (digits, multiplier) = match s.as_bytes()[s.len() - 1] {
68 b'G' => (&s[..s.len() - 1], GIB),
69 b'M' => (&s[..s.len() - 1], MIB),
70 b'K' => (&s[..s.len() - 1], KIB),
71 _ => (s, 1),
72 };
73 if digits.is_empty() {
74 return Err(ParseMemSizeError::MissingDigits);
75 }
76 if !digits.bytes().all(|b| b.is_ascii_digit()) {
77 return Err(ParseMemSizeError::InvalidCharacter);
78 }
79 let value: u64 = digits.parse().map_err(|_| ParseMemSizeError::Overflow)?;
80 value
81 .checked_mul(multiplier)
82 .map(Self)
83 .ok_or(ParseMemSizeError::Overflow)
84 }
85}
86
87impl fmt::Display for MemSize {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self.0 {
92 0 => f.write_str("0"),
93 b if b % GIB == 0 => write!(f, "{}G", b / GIB),
94 b if b % MIB == 0 => write!(f, "{}M", b / MIB),
95 b if b % KIB == 0 => write!(f, "{}K", b / KIB),
96 b => write!(f, "{b}"),
97 }
98 }
99}
100
101impl Serialize for MemSize {
102 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
103 serializer.collect_str(self)
104 }
105}
106
107impl<'de> Deserialize<'de> for MemSize {
108 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
109 let s = String::deserialize(deserializer)?;
111 s.parse().map_err(serde::de::Error::custom)
112 }
113}
114
115#[non_exhaustive]
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum ParseMemSizeError {
126 Empty,
128 MissingDigits,
130 InvalidCharacter,
134 Overflow,
136}
137
138impl fmt::Display for ParseMemSizeError {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 f.write_str(match self {
141 Self::Empty => "memory size is empty",
142 Self::MissingDigits => "memory size has a unit suffix but no digits",
143 Self::InvalidCharacter => {
144 "memory size must be ASCII digits with an optional trailing G, M, or K"
145 }
146 Self::Overflow => "memory size in bytes overflows u64",
147 })
148 }
149}
150
151impl core::error::Error for ParseMemSizeError {}
152
153#[cfg(feature = "schema")]
162impl schemars::JsonSchema for MemSize {
163 fn schema_name() -> std::borrow::Cow<'static, str> {
164 "MemSize".into()
165 }
166
167 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
168 schemars::json_schema!({
169 "type": "string",
170 "pattern": r"^\d+(G|M|K)?$",
171 "description": "A byte quantity: digits, optionally suffixed G, M or K (binary units).",
172 })
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
191pub struct UpDuration(core::time::Duration);
192
193impl UpDuration {
194 #[inline]
196 #[must_use]
197 pub const fn from_millis(ms: u64) -> Self {
198 Self(core::time::Duration::from_millis(ms))
199 }
200
201 #[inline]
203 #[must_use]
204 pub const fn as_duration(self) -> core::time::Duration {
205 self.0
206 }
207
208 #[inline]
210 #[must_use]
211 pub const fn as_millis(self) -> u64 {
212 self.0.as_millis() as u64
218 }
219}
220
221impl FromStr for UpDuration {
222 type Err = ParseUpDurationError;
223
224 fn from_str(s: &str) -> Result<Self, Self::Err> {
234 if s.is_empty() {
235 return Err(ParseUpDurationError::Empty);
236 }
237 let (digits, ms_per_unit) = match s.as_bytes()[s.len() - 1] {
238 b'h' => (&s[..s.len() - 1], 3_600_000),
239 b'm' => (&s[..s.len() - 1], 60_000),
240 b's' => (&s[..s.len() - 1], 1_000),
241 _ => (s, 1),
242 };
243 if digits.is_empty() {
244 return Err(ParseUpDurationError::MissingDigits);
245 }
246 if !digits.bytes().all(|b| b.is_ascii_digit()) {
247 return Err(ParseUpDurationError::InvalidCharacter);
248 }
249 let value: u64 = digits.parse().map_err(|_| ParseUpDurationError::Overflow)?;
250 value
251 .checked_mul(ms_per_unit)
252 .map(Self::from_millis)
253 .ok_or(ParseUpDurationError::Overflow)
254 }
255}
256
257impl fmt::Display for UpDuration {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 let ms = self.as_millis();
261 match ms {
262 0 => f.write_str("0"),
263 v if v % 3_600_000 == 0 => write!(f, "{}h", v / 3_600_000),
264 v if v % 60_000 == 0 => write!(f, "{}m", v / 60_000),
265 v if v % 1_000 == 0 => write!(f, "{}s", v / 1_000),
266 v => write!(f, "{v}"),
267 }
268 }
269}
270
271impl Serialize for UpDuration {
272 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
273 serializer.collect_str(self)
274 }
275}
276
277impl<'de> Deserialize<'de> for UpDuration {
278 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
279 let s = String::deserialize(deserializer)?;
281 s.parse().map_err(serde::de::Error::custom)
282 }
283}
284
285#[non_exhaustive]
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub enum ParseUpDurationError {
296 Empty,
298 MissingDigits,
300 InvalidCharacter,
303 Overflow,
305}
306
307impl fmt::Display for ParseUpDurationError {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 f.write_str(match self {
310 Self::Empty => "duration is empty",
311 Self::MissingDigits => "duration has a unit suffix but no digits",
312 Self::InvalidCharacter => {
313 "duration must be ASCII digits with an optional trailing h, m, or s"
314 }
315 Self::Overflow => "duration in milliseconds overflows u64",
316 })
317 }
318}
319
320impl core::error::Error for ParseUpDurationError {}
321
322#[cfg(feature = "schema")]
326impl schemars::JsonSchema for UpDuration {
327 fn schema_name() -> std::borrow::Cow<'static, str> {
328 "UpDuration".into()
329 }
330
331 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
332 schemars::json_schema!({
333 "type": "string",
334 "pattern": r"^\d+(h|m|s)?$",
335 "description": "A duration: digits, optionally suffixed h, m or s. Plain digits are milliseconds.",
336 })
337 }
338}
339
340#[cfg(test)]
341mod mem_size_tests {
342 use super::*;
343
344 #[test]
345 fn plain_digits_parse_as_bytes() {
346 assert_eq!("123".parse::<MemSize>().unwrap().bytes(), 123);
347 }
348
349 #[test]
350 fn units_are_binary() {
351 assert_eq!("7K".parse::<MemSize>().unwrap().bytes(), 7 * 1024);
352 assert_eq!("512M".parse::<MemSize>().unwrap().bytes(), 512 << 20);
353 assert_eq!("3G".parse::<MemSize>().unwrap().bytes(), 3 << 30);
354 }
355
356 #[test]
357 fn rejects_spec_violations() {
358 use ParseMemSizeError::*;
359 assert_eq!("".parse::<MemSize>(), Err(Empty));
360 assert_eq!("G".parse::<MemSize>(), Err(MissingDigits));
361 assert_eq!("512m".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!(" 512M".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!("1.5G".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!("512MB".parse::<MemSize>(), Err(InvalidCharacter)); assert_eq!("18446744073709551616".parse::<MemSize>(), Err(Overflow));
366 assert_eq!("17179869184G".parse::<MemSize>(), Err(Overflow));
367 }
368
369 #[test]
370 fn display_uses_largest_exact_unit_and_round_trips() {
371 for bytes in [
372 0u64,
373 1,
374 1023,
375 1024,
376 1536,
377 1 << 20,
378 (1 << 30) + 1024,
379 u64::MAX,
380 ] {
381 let size = MemSize::from_bytes(bytes);
382 let reparsed: MemSize = size.to_string().parse().unwrap();
383 assert_eq!(reparsed, size, "display of {bytes} bytes must reparse");
384 }
385 assert_eq!(MemSize::from_bytes(3 << 30).to_string(), "3G");
386 assert_eq!(MemSize::from_bytes(1536).to_string(), "1536");
387 }
388
389 #[test]
390 fn serde_uses_string_form() {
391 let size: MemSize = serde_json::from_str("\"512M\"").unwrap();
392 assert_eq!(size.bytes(), 512 << 20);
393 assert_eq!(serde_json::to_string(&size).unwrap(), "\"512M\"");
394 assert!(serde_json::from_str::<MemSize>("\"512MB\"").is_err());
395 }
396
397 #[cfg(feature = "schema")]
404 #[test]
405 fn the_schema_pattern_agrees_with_from_str() {
406 let schema = serde_json::to_value(schemars::schema_for!(MemSize)).unwrap();
407 let pattern = schema["pattern"].as_str().unwrap();
408 let re = regex::Regex::new(pattern).unwrap();
409 for accepted in ["512M", "1G", "4096", "7K"] {
410 assert!(re.is_match(accepted), "pattern rejects {accepted}");
411 assert!(
412 accepted.parse::<MemSize>().is_ok(),
413 "FromStr rejects {accepted}"
414 );
415 }
416 for rejected in ["512MB", "512m", "1.5G", "", "M", "512T", "1P", "512g"] {
417 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
418 assert!(
419 rejected.parse::<MemSize>().is_err(),
420 "FromStr accepts {rejected}"
421 );
422 }
423 }
424}
425
426#[cfg(test)]
427mod up_duration_tests {
428 use super::*;
429
430 #[test]
431 fn plain_digits_are_milliseconds() {
432 assert_eq!("1600".parse::<UpDuration>().unwrap().as_millis(), 1600);
433 }
434
435 #[test]
436 fn units_seconds_minutes_hours() {
437 assert_eq!("30s".parse::<UpDuration>().unwrap().as_millis(), 30_000);
438 assert_eq!("5m".parse::<UpDuration>().unwrap().as_millis(), 300_000);
439 assert_eq!("2h".parse::<UpDuration>().unwrap().as_millis(), 7_200_000);
440 }
441
442 #[test]
443 fn rejects_spec_violations() {
444 use ParseUpDurationError::*;
445 assert_eq!("".parse::<UpDuration>(), Err(Empty));
446 assert_eq!("s".parse::<UpDuration>(), Err(MissingDigits));
447 assert_eq!("30S".parse::<UpDuration>(), Err(InvalidCharacter)); assert_eq!("1.5s".parse::<UpDuration>(), Err(InvalidCharacter));
449 assert_eq!("30 s".parse::<UpDuration>(), Err(InvalidCharacter));
450 assert_eq!("99999999999999999999h".parse::<UpDuration>(), Err(Overflow));
452 assert_eq!("9999999999999999h".parse::<UpDuration>(), Err(Overflow));
455 }
456
457 #[test]
458 fn display_round_trips() {
459 for ms in [
460 0u64, 1, 999, 1000, 1600, 30_000, 300_000, 7_200_000, 3_601_000,
461 ] {
462 let d = UpDuration::from_millis(ms);
463 assert_eq!(d.to_string().parse::<UpDuration>().unwrap(), d, "{ms}ms");
464 }
465 assert_eq!(UpDuration::from_millis(30_000).to_string(), "30s");
466 assert_eq!(UpDuration::from_millis(1600).to_string(), "1600");
467 assert_eq!(UpDuration::from_millis(7_200_000).to_string(), "2h");
468 }
469
470 #[test]
471 fn serde_uses_string_form() {
472 let d: UpDuration = serde_json::from_str("\"30s\"").unwrap();
473 assert_eq!(d.as_millis(), 30_000);
474 assert_eq!(serde_json::to_string(&d).unwrap(), "\"30s\"");
475 }
476
477 #[cfg(feature = "schema")]
481 #[test]
482 fn the_schema_pattern_agrees_with_from_str() {
483 let schema = serde_json::to_value(schemars::schema_for!(UpDuration)).unwrap();
484 let pattern = schema["pattern"].as_str().unwrap();
485 let re = regex::Regex::new(pattern).unwrap();
486 for accepted in ["1600", "30s", "5m", "2h"] {
487 assert!(re.is_match(accepted), "pattern rejects {accepted}");
488 assert!(
489 accepted.parse::<UpDuration>().is_ok(),
490 "FromStr rejects {accepted}"
491 );
492 }
493 for rejected in ["30S", "1.5s", "30 s", "", "s", "30d", "30w"] {
494 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
495 assert!(
496 rejected.parse::<UpDuration>().is_err(),
497 "FromStr accepts {rejected}"
498 );
499 }
500 }
501}