1use std::{
2 borrow::Cow,
3 fmt::{Debug, Display},
4 str::FromStr,
5 time::Duration,
6};
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize, de::Error};
10
11#[derive(Clone, Copy, PartialEq, Eq, Hash)]
12pub struct PrettyDuration {
13 unit: DurationUnit,
14 amount: u64,
15}
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
18pub enum DurationUnit {
19 Seconds,
20 Minutes,
21 Hours,
22 Days,
23}
24
25impl PrettyDuration {
26 pub fn as_duration(&self) -> Duration {
27 match self.unit {
28 DurationUnit::Seconds => Duration::from_secs(self.amount),
29 DurationUnit::Minutes => Duration::from_secs(self.amount * 60),
30 DurationUnit::Hours => Duration::from_secs(self.amount * 60 * 60),
31 DurationUnit::Days => Duration::from_secs(self.amount * 60 * 60 * 24),
32 }
33 }
34}
35
36impl Default for PrettyDuration {
37 fn default() -> Self {
38 Self {
39 unit: DurationUnit::Seconds,
40 amount: 0,
41 }
42 }
43}
44
45impl PartialOrd for PrettyDuration {
46 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
47 Some(self.cmp(other))
48 }
49}
50
51impl Ord for PrettyDuration {
52 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
53 self.as_duration().cmp(&other.as_duration())
54 }
55}
56
57impl JsonSchema for PrettyDuration {
58 fn schema_name() -> String {
59 "PrettyDuration".to_owned()
60 }
61
62 fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
63 String::json_schema(r#gen)
64 }
65}
66
67impl Display for DurationUnit {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 DurationUnit::Seconds => write!(f, "s"),
71 DurationUnit::Minutes => write!(f, "m"),
72 DurationUnit::Hours => write!(f, "h"),
73 DurationUnit::Days => write!(f, "d"),
74 }
75 }
76}
77
78impl FromStr for DurationUnit {
79 type Err = ();
80
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 match s {
83 "s" | "S" => Ok(Self::Seconds),
84 "m" | "M" => Ok(Self::Minutes),
85 "h" | "H" => Ok(Self::Hours),
86 "d" | "D" => Ok(Self::Days),
87 _ => Err(()),
88 }
89 }
90}
91
92impl Display for PrettyDuration {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 write!(f, "{}{}", self.amount, self.unit)
95 }
96}
97
98impl Debug for PrettyDuration {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 <Self as Display>::fmt(self, f)
101 }
102}
103
104impl FromStr for PrettyDuration {
105 type Err = ();
106
107 fn from_str(s: &str) -> Result<Self, Self::Err> {
108 let (amount_str, unit_str) = s.split_at_checked(s.len() - 1).ok_or(())?;
109 Ok(Self {
110 unit: unit_str.parse()?,
111 amount: amount_str.parse().map_err(|_| ())?,
112 })
113 }
114}
115
116impl Serialize for PrettyDuration {
117 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118 where
119 S: serde::Serializer,
120 {
121 serializer.serialize_str(&self.to_string())
122 }
123}
124
125impl<'de> Deserialize<'de> for PrettyDuration {
126 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127 where
128 D: serde::Deserializer<'de>,
129 {
130 let repr: Cow<'de, str> = Cow::deserialize(deserializer)?;
131 repr.parse()
132 .map_err(|()| D::Error::custom("Failed to parse value as a duration"))
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 pub fn pretty_duration_serialize() {
142 assert_eq!(
143 PrettyDuration {
144 unit: DurationUnit::Seconds,
145 amount: 1234
146 }
147 .to_string(),
148 "1234s"
149 );
150 assert_eq!(
151 PrettyDuration {
152 unit: DurationUnit::Minutes,
153 amount: 345
154 }
155 .to_string(),
156 "345m"
157 );
158 assert_eq!(
159 PrettyDuration {
160 unit: DurationUnit::Hours,
161 amount: 56
162 }
163 .to_string(),
164 "56h"
165 );
166 assert_eq!(
167 PrettyDuration {
168 unit: DurationUnit::Days,
169 amount: 7
170 }
171 .to_string(),
172 "7d"
173 );
174 }
175
176 #[test]
177 pub fn pretty_duration_deserialize() {
178 fn assert_deserializes_to(repr1: &str, repr2: &str, unit: DurationUnit, amount: u64) {
179 let duration = PrettyDuration { unit, amount };
180 assert_eq!(duration, repr1.parse().unwrap());
181 assert_eq!(duration, repr2.parse().unwrap());
182 }
183
184 assert_deserializes_to("12s", "12S", DurationUnit::Seconds, 12);
185 assert_deserializes_to("34m", "34M", DurationUnit::Minutes, 34);
186 assert_deserializes_to("56h", "56H", DurationUnit::Hours, 56);
187 assert_deserializes_to("7d", "7D", DurationUnit::Days, 7);
188 }
189
190 #[test]
191 #[should_panic]
192 pub fn cant_parse_nagative_duration() {
193 _ = "-12s".parse::<PrettyDuration>().unwrap();
194 }
195}