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]
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ParseMemSizeError {
125 Empty,
127 MissingDigits,
129 InvalidCharacter,
133 Overflow,
135}
136
137impl fmt::Display for ParseMemSizeError {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.write_str(match self {
140 Self::Empty => "memory size is empty",
141 Self::MissingDigits => "memory size has a unit suffix but no digits",
142 Self::InvalidCharacter => {
143 "memory size must be ASCII digits with an optional trailing G, M, or K"
144 }
145 Self::Overflow => "memory size in bytes overflows u64",
146 })
147 }
148}
149
150impl core::error::Error for ParseMemSizeError {}
151
152#[cfg(feature = "schema")]
161impl schemars::JsonSchema for MemSize {
162 fn schema_name() -> std::borrow::Cow<'static, str> {
163 "MemSize".into()
164 }
165
166 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
167 schemars::json_schema!({
168 "type": "string",
169 "pattern": r"^\d+(G|M|K)?$",
170 "description": "A byte quantity: digits, optionally suffixed G, M or K (binary units).",
171 })
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
197pub struct UpDuration(core::time::Duration);
198
199impl UpDuration {
200 #[inline]
202 #[must_use]
203 pub const fn from_millis(ms: u64) -> Self {
204 Self(core::time::Duration::from_millis(ms))
205 }
206
207 #[inline]
209 #[must_use]
210 pub const fn as_duration(self) -> core::time::Duration {
211 self.0
212 }
213
214 #[inline]
216 #[must_use]
217 pub const fn as_millis(self) -> u64 {
218 self.0.as_millis() as u64
223 }
224}
225
226impl FromStr for UpDuration {
227 type Err = ParseUpDurationError;
228
229 fn from_str(s: &str) -> Result<Self, Self::Err> {
242 if s.is_empty() {
243 return Err(ParseUpDurationError::Empty);
244 }
245 let (digits, ms_per_unit) = if let Some(rest) = s.strip_suffix("ms") {
250 (rest, 1)
251 } else {
252 match s.as_bytes()[s.len() - 1] {
253 b'h' => (&s[..s.len() - 1], 3_600_000),
254 b'm' => (&s[..s.len() - 1], 60_000),
255 b's' => (&s[..s.len() - 1], 1_000),
256 _ => (s, 1),
257 }
258 };
259 if digits.is_empty() {
260 return Err(ParseUpDurationError::MissingDigits);
261 }
262 if !digits.bytes().all(|b| b.is_ascii_digit()) {
263 return Err(ParseUpDurationError::InvalidCharacter);
264 }
265 let value: u64 = digits.parse().map_err(|_| ParseUpDurationError::Overflow)?;
266 value
267 .checked_mul(ms_per_unit)
268 .map(Self::from_millis)
269 .ok_or(ParseUpDurationError::Overflow)
270 }
271}
272
273impl fmt::Display for UpDuration {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 let ms = self.as_millis();
277 match ms {
278 0 => f.write_str("0"),
279 v if v % 3_600_000 == 0 => write!(f, "{}h", v / 3_600_000),
280 v if v % 60_000 == 0 => write!(f, "{}m", v / 60_000),
281 v if v % 1_000 == 0 => write!(f, "{}s", v / 1_000),
282 v => write!(f, "{v}"),
283 }
284 }
285}
286
287impl Serialize for UpDuration {
288 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
289 serializer.collect_str(self)
290 }
291}
292
293impl<'de> Deserialize<'de> for UpDuration {
294 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
295 let s = String::deserialize(deserializer)?;
297 s.parse().map_err(serde::de::Error::custom)
298 }
299}
300
301#[non_exhaustive]
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum ParseUpDurationError {
310 Empty,
312 MissingDigits,
314 InvalidCharacter,
317 Overflow,
319}
320
321impl fmt::Display for ParseUpDurationError {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 f.write_str(match self {
324 Self::Empty => "duration is empty",
325 Self::MissingDigits => "duration has a unit suffix but no digits",
326 Self::InvalidCharacter => {
327 "duration must be ASCII digits with an optional trailing h, m, s, or ms"
328 }
329 Self::Overflow => "duration in milliseconds overflows u64",
330 })
331 }
332}
333
334impl core::error::Error for ParseUpDurationError {}
335
336#[cfg(feature = "schema")]
340impl schemars::JsonSchema for UpDuration {
341 fn schema_name() -> std::borrow::Cow<'static, str> {
342 "UpDuration".into()
343 }
344
345 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
346 schemars::json_schema!({
347 "type": "string",
348 "pattern": r"^\d+(ms|h|m|s)?$",
349 "description": "A duration: digits, optionally suffixed ms, h, m or s. Plain digits are milliseconds.",
350 })
351 }
352}
353
354#[cfg(test)]
355mod mem_size_tests {
356 use super::*;
357
358 #[test]
359 fn plain_digits_parse_as_bytes() {
360 assert_eq!("123".parse::<MemSize>().unwrap().bytes(), 123);
361 }
362
363 #[test]
364 fn units_are_binary() {
365 assert_eq!("7K".parse::<MemSize>().unwrap().bytes(), 7 * 1024);
366 assert_eq!("512M".parse::<MemSize>().unwrap().bytes(), 512 << 20);
367 assert_eq!("3G".parse::<MemSize>().unwrap().bytes(), 3 << 30);
368 }
369
370 #[test]
371 fn rejects_spec_violations() {
372 use ParseMemSizeError::*;
373 assert_eq!("".parse::<MemSize>(), Err(Empty));
374 assert_eq!("G".parse::<MemSize>(), Err(MissingDigits));
375 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));
380 assert_eq!("17179869184G".parse::<MemSize>(), Err(Overflow));
381 }
382
383 #[test]
384 fn display_uses_largest_exact_unit_and_round_trips() {
385 for bytes in [
386 0u64,
387 1,
388 1023,
389 1024,
390 1536,
391 1 << 20,
392 (1 << 30) + 1024,
393 u64::MAX,
394 ] {
395 let size = MemSize::from_bytes(bytes);
396 let reparsed: MemSize = size.to_string().parse().unwrap();
397 assert_eq!(reparsed, size, "display of {bytes} bytes must reparse");
398 }
399 assert_eq!(MemSize::from_bytes(3 << 30).to_string(), "3G");
400 assert_eq!(MemSize::from_bytes(1536).to_string(), "1536");
401 }
402
403 #[test]
404 fn serde_uses_string_form() {
405 let size: MemSize = serde_json::from_str("\"512M\"").unwrap();
406 assert_eq!(size.bytes(), 512 << 20);
407 assert_eq!(serde_json::to_string(&size).unwrap(), "\"512M\"");
408 assert!(serde_json::from_str::<MemSize>("\"512MB\"").is_err());
409 }
410
411 #[cfg(feature = "schema")]
415 #[test]
416 fn the_schema_pattern_agrees_with_from_str() {
417 let schema = serde_json::to_value(schemars::schema_for!(MemSize)).unwrap();
418 let pattern = schema["pattern"].as_str().unwrap();
419 let re = regex::Regex::new(pattern).unwrap();
420 for accepted in ["512M", "1G", "4096", "7K"] {
421 assert!(re.is_match(accepted), "pattern rejects {accepted}");
422 assert!(
423 accepted.parse::<MemSize>().is_ok(),
424 "FromStr rejects {accepted}"
425 );
426 }
427 for rejected in ["512MB", "512m", "1.5G", "", "M", "512T", "1P", "512g"] {
428 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
429 assert!(
430 rejected.parse::<MemSize>().is_err(),
431 "FromStr accepts {rejected}"
432 );
433 }
434 }
435}
436
437#[cfg(test)]
438mod up_duration_tests {
439 use super::*;
440
441 #[test]
442 fn plain_digits_are_milliseconds() {
443 assert_eq!("1600".parse::<UpDuration>().unwrap().as_millis(), 1600);
444 }
445
446 #[test]
447 fn units_seconds_minutes_hours() {
448 assert_eq!("30s".parse::<UpDuration>().unwrap().as_millis(), 30_000);
449 assert_eq!("5m".parse::<UpDuration>().unwrap().as_millis(), 300_000);
450 assert_eq!("2h".parse::<UpDuration>().unwrap().as_millis(), 7_200_000);
451 }
452
453 #[test]
457 fn milliseconds_do_not_alias_minutes() {
458 assert_eq!("500ms".parse::<UpDuration>().unwrap().as_millis(), 500);
459 assert_eq!("5ms".parse::<UpDuration>().unwrap().as_millis(), 5);
460 assert_eq!("5m".parse::<UpDuration>().unwrap().as_millis(), 300_000);
461 assert_eq!("1m".parse::<UpDuration>().unwrap().as_millis(), 60_000);
463 }
464
465 #[test]
466 fn rejects_spec_violations() {
467 use ParseUpDurationError::*;
468 assert_eq!("".parse::<UpDuration>(), Err(Empty));
469 assert_eq!("s".parse::<UpDuration>(), Err(MissingDigits));
470 assert_eq!("ms".parse::<UpDuration>(), Err(MissingDigits));
471 assert_eq!("30S".parse::<UpDuration>(), Err(InvalidCharacter)); assert_eq!("1.5s".parse::<UpDuration>(), Err(InvalidCharacter));
473 assert_eq!("30 s".parse::<UpDuration>(), Err(InvalidCharacter));
474 assert_eq!("30MS".parse::<UpDuration>(), Err(InvalidCharacter)); assert_eq!("99999999999999999999h".parse::<UpDuration>(), Err(Overflow));
477 assert_eq!("9999999999999999h".parse::<UpDuration>(), Err(Overflow));
480 }
481
482 #[test]
483 fn display_round_trips() {
484 for ms in [
485 0u64, 1, 999, 1000, 1600, 30_000, 300_000, 7_200_000, 3_601_000,
486 ] {
487 let d = UpDuration::from_millis(ms);
488 assert_eq!(d.to_string().parse::<UpDuration>().unwrap(), d, "{ms}ms");
489 }
490 assert_eq!(UpDuration::from_millis(30_000).to_string(), "30s");
491 assert_eq!(UpDuration::from_millis(1600).to_string(), "1600");
492 assert_eq!(UpDuration::from_millis(7_200_000).to_string(), "2h");
493 }
494
495 #[test]
496 fn serde_uses_string_form() {
497 let d: UpDuration = serde_json::from_str("\"30s\"").unwrap();
498 assert_eq!(d.as_millis(), 30_000);
499 assert_eq!(serde_json::to_string(&d).unwrap(), "\"30s\"");
500 }
501
502 #[cfg(feature = "schema")]
505 #[test]
506 fn the_schema_pattern_agrees_with_from_str() {
507 let schema = serde_json::to_value(schemars::schema_for!(UpDuration)).unwrap();
508 let pattern = schema["pattern"].as_str().unwrap();
509 let re = regex::Regex::new(pattern).unwrap();
510 for accepted in ["1600", "30s", "5m", "2h", "500ms"] {
511 assert!(re.is_match(accepted), "pattern rejects {accepted}");
512 assert!(
513 accepted.parse::<UpDuration>().is_ok(),
514 "FromStr rejects {accepted}"
515 );
516 }
517 for rejected in ["30S", "1.5s", "30 s", "", "s", "30d", "30w", "30MS"] {
518 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
519 assert!(
520 rejected.parse::<UpDuration>().is_err(),
521 "FromStr accepts {rejected}"
522 );
523 }
524 }
525}