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)]
198pub struct UpDuration(core::time::Duration);
199
200impl UpDuration {
201 #[inline]
203 #[must_use]
204 pub const fn from_millis(ms: u64) -> Self {
205 Self(core::time::Duration::from_millis(ms))
206 }
207
208 #[inline]
210 #[must_use]
211 pub const fn as_duration(self) -> core::time::Duration {
212 self.0
213 }
214
215 #[inline]
217 #[must_use]
218 pub const fn as_millis(self) -> u64 {
219 self.0.as_millis() as u64
225 }
226}
227
228impl FromStr for UpDuration {
229 type Err = ParseUpDurationError;
230
231 fn from_str(s: &str) -> Result<Self, Self::Err> {
244 if s.is_empty() {
245 return Err(ParseUpDurationError::Empty);
246 }
247 let (digits, ms_per_unit) = if let Some(rest) = s.strip_suffix("ms") {
252 (rest, 1)
253 } else {
254 match s.as_bytes()[s.len() - 1] {
255 b'h' => (&s[..s.len() - 1], 3_600_000),
256 b'm' => (&s[..s.len() - 1], 60_000),
257 b's' => (&s[..s.len() - 1], 1_000),
258 _ => (s, 1),
259 }
260 };
261 if digits.is_empty() {
262 return Err(ParseUpDurationError::MissingDigits);
263 }
264 if !digits.bytes().all(|b| b.is_ascii_digit()) {
265 return Err(ParseUpDurationError::InvalidCharacter);
266 }
267 let value: u64 = digits.parse().map_err(|_| ParseUpDurationError::Overflow)?;
268 value
269 .checked_mul(ms_per_unit)
270 .map(Self::from_millis)
271 .ok_or(ParseUpDurationError::Overflow)
272 }
273}
274
275impl fmt::Display for UpDuration {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 let ms = self.as_millis();
279 match ms {
280 0 => f.write_str("0"),
281 v if v % 3_600_000 == 0 => write!(f, "{}h", v / 3_600_000),
282 v if v % 60_000 == 0 => write!(f, "{}m", v / 60_000),
283 v if v % 1_000 == 0 => write!(f, "{}s", v / 1_000),
284 v => write!(f, "{v}"),
285 }
286 }
287}
288
289impl Serialize for UpDuration {
290 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
291 serializer.collect_str(self)
292 }
293}
294
295impl<'de> Deserialize<'de> for UpDuration {
296 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
297 let s = String::deserialize(deserializer)?;
299 s.parse().map_err(serde::de::Error::custom)
300 }
301}
302
303#[non_exhaustive]
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum ParseUpDurationError {
314 Empty,
316 MissingDigits,
318 InvalidCharacter,
321 Overflow,
323}
324
325impl fmt::Display for ParseUpDurationError {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 f.write_str(match self {
328 Self::Empty => "duration is empty",
329 Self::MissingDigits => "duration has a unit suffix but no digits",
330 Self::InvalidCharacter => {
331 "duration must be ASCII digits with an optional trailing h, m, s, or ms"
332 }
333 Self::Overflow => "duration in milliseconds overflows u64",
334 })
335 }
336}
337
338impl core::error::Error for ParseUpDurationError {}
339
340#[cfg(feature = "schema")]
344impl schemars::JsonSchema for UpDuration {
345 fn schema_name() -> std::borrow::Cow<'static, str> {
346 "UpDuration".into()
347 }
348
349 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
350 schemars::json_schema!({
351 "type": "string",
352 "pattern": r"^\d+(ms|h|m|s)?$",
353 "description": "A duration: digits, optionally suffixed ms, h, m or s. Plain digits are milliseconds.",
354 })
355 }
356}
357
358#[cfg(test)]
359mod mem_size_tests {
360 use super::*;
361
362 #[test]
363 fn plain_digits_parse_as_bytes() {
364 assert_eq!("123".parse::<MemSize>().unwrap().bytes(), 123);
365 }
366
367 #[test]
368 fn units_are_binary() {
369 assert_eq!("7K".parse::<MemSize>().unwrap().bytes(), 7 * 1024);
370 assert_eq!("512M".parse::<MemSize>().unwrap().bytes(), 512 << 20);
371 assert_eq!("3G".parse::<MemSize>().unwrap().bytes(), 3 << 30);
372 }
373
374 #[test]
375 fn rejects_spec_violations() {
376 use ParseMemSizeError::*;
377 assert_eq!("".parse::<MemSize>(), Err(Empty));
378 assert_eq!("G".parse::<MemSize>(), Err(MissingDigits));
379 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));
384 assert_eq!("17179869184G".parse::<MemSize>(), Err(Overflow));
385 }
386
387 #[test]
388 fn display_uses_largest_exact_unit_and_round_trips() {
389 for bytes in [
390 0u64,
391 1,
392 1023,
393 1024,
394 1536,
395 1 << 20,
396 (1 << 30) + 1024,
397 u64::MAX,
398 ] {
399 let size = MemSize::from_bytes(bytes);
400 let reparsed: MemSize = size.to_string().parse().unwrap();
401 assert_eq!(reparsed, size, "display of {bytes} bytes must reparse");
402 }
403 assert_eq!(MemSize::from_bytes(3 << 30).to_string(), "3G");
404 assert_eq!(MemSize::from_bytes(1536).to_string(), "1536");
405 }
406
407 #[test]
408 fn serde_uses_string_form() {
409 let size: MemSize = serde_json::from_str("\"512M\"").unwrap();
410 assert_eq!(size.bytes(), 512 << 20);
411 assert_eq!(serde_json::to_string(&size).unwrap(), "\"512M\"");
412 assert!(serde_json::from_str::<MemSize>("\"512MB\"").is_err());
413 }
414
415 #[cfg(feature = "schema")]
422 #[test]
423 fn the_schema_pattern_agrees_with_from_str() {
424 let schema = serde_json::to_value(schemars::schema_for!(MemSize)).unwrap();
425 let pattern = schema["pattern"].as_str().unwrap();
426 let re = regex::Regex::new(pattern).unwrap();
427 for accepted in ["512M", "1G", "4096", "7K"] {
428 assert!(re.is_match(accepted), "pattern rejects {accepted}");
429 assert!(
430 accepted.parse::<MemSize>().is_ok(),
431 "FromStr rejects {accepted}"
432 );
433 }
434 for rejected in ["512MB", "512m", "1.5G", "", "M", "512T", "1P", "512g"] {
435 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
436 assert!(
437 rejected.parse::<MemSize>().is_err(),
438 "FromStr accepts {rejected}"
439 );
440 }
441 }
442}
443
444#[cfg(test)]
445mod up_duration_tests {
446 use super::*;
447
448 #[test]
449 fn plain_digits_are_milliseconds() {
450 assert_eq!("1600".parse::<UpDuration>().unwrap().as_millis(), 1600);
451 }
452
453 #[test]
454 fn units_seconds_minutes_hours() {
455 assert_eq!("30s".parse::<UpDuration>().unwrap().as_millis(), 30_000);
456 assert_eq!("5m".parse::<UpDuration>().unwrap().as_millis(), 300_000);
457 assert_eq!("2h".parse::<UpDuration>().unwrap().as_millis(), 7_200_000);
458 }
459
460 #[test]
465 fn milliseconds_do_not_alias_minutes() {
466 assert_eq!("500ms".parse::<UpDuration>().unwrap().as_millis(), 500);
467 assert_eq!("5ms".parse::<UpDuration>().unwrap().as_millis(), 5);
468 assert_eq!("5m".parse::<UpDuration>().unwrap().as_millis(), 300_000);
469 assert_eq!("1m".parse::<UpDuration>().unwrap().as_millis(), 60_000);
471 }
472
473 #[test]
474 fn rejects_spec_violations() {
475 use ParseUpDurationError::*;
476 assert_eq!("".parse::<UpDuration>(), Err(Empty));
477 assert_eq!("s".parse::<UpDuration>(), Err(MissingDigits));
478 assert_eq!("ms".parse::<UpDuration>(), Err(MissingDigits));
479 assert_eq!("30S".parse::<UpDuration>(), Err(InvalidCharacter)); assert_eq!("1.5s".parse::<UpDuration>(), Err(InvalidCharacter));
481 assert_eq!("30 s".parse::<UpDuration>(), Err(InvalidCharacter));
482 assert_eq!("30MS".parse::<UpDuration>(), Err(InvalidCharacter)); assert_eq!("99999999999999999999h".parse::<UpDuration>(), Err(Overflow));
485 assert_eq!("9999999999999999h".parse::<UpDuration>(), Err(Overflow));
488 }
489
490 #[test]
491 fn display_round_trips() {
492 for ms in [
493 0u64, 1, 999, 1000, 1600, 30_000, 300_000, 7_200_000, 3_601_000,
494 ] {
495 let d = UpDuration::from_millis(ms);
496 assert_eq!(d.to_string().parse::<UpDuration>().unwrap(), d, "{ms}ms");
497 }
498 assert_eq!(UpDuration::from_millis(30_000).to_string(), "30s");
499 assert_eq!(UpDuration::from_millis(1600).to_string(), "1600");
500 assert_eq!(UpDuration::from_millis(7_200_000).to_string(), "2h");
501 }
502
503 #[test]
504 fn serde_uses_string_form() {
505 let d: UpDuration = serde_json::from_str("\"30s\"").unwrap();
506 assert_eq!(d.as_millis(), 30_000);
507 assert_eq!(serde_json::to_string(&d).unwrap(), "\"30s\"");
508 }
509
510 #[cfg(feature = "schema")]
514 #[test]
515 fn the_schema_pattern_agrees_with_from_str() {
516 let schema = serde_json::to_value(schemars::schema_for!(UpDuration)).unwrap();
517 let pattern = schema["pattern"].as_str().unwrap();
518 let re = regex::Regex::new(pattern).unwrap();
519 for accepted in ["1600", "30s", "5m", "2h", "500ms"] {
520 assert!(re.is_match(accepted), "pattern rejects {accepted}");
521 assert!(
522 accepted.parse::<UpDuration>().is_ok(),
523 "FromStr rejects {accepted}"
524 );
525 }
526 for rejected in ["30S", "1.5s", "30 s", "", "s", "30d", "30w", "30MS"] {
527 assert!(!re.is_match(rejected), "pattern accepts {rejected}");
528 assert!(
529 rejected.parse::<UpDuration>().is_err(),
530 "FromStr accepts {rejected}"
531 );
532 }
533 }
534}