1#![allow(clippy::tabs_in_doc_comments)]
2use std::fmt::{Display, Formatter};
90use std::ops::{Add, Sub};
91use std::str::FromStr;
92use std::time;
93
94use chrono::{DateTime, Duration, TimeZone};
95#[cfg(feature = "clap")]
96use clap::builder::OsStr;
97use once_cell::sync::Lazy;
98use regex::{Match, Regex};
99#[cfg(feature = "serde")]
100use serde::de::{Error, Unexpected, Visitor};
101#[cfg(feature = "serde")]
102use serde::{Deserialize, Deserializer, Serialize, Serializer};
103
104const SECS_PER_MINUTES: i64 = 60;
105const SECS_PER_HOUR: i64 = 60 * SECS_PER_MINUTES;
106const SECS_PER_DAY: i64 = 24 * SECS_PER_HOUR;
107const SECS_PER_WEEK: i64 = 7 * SECS_PER_DAY;
108
109#[derive(Copy, Clone, Debug)]
111pub enum DurationFlexError {
112 InvalidFormat,
114
115 OutOfRange,
117}
118
119#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
141#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
142#[cfg_attr(feature = "utoipa", schema(as = String, example = "1h30m"))]
143pub struct DurationFlex {
144 secs: i64,
145 nanos: i32,
146}
147
148#[cfg(feature = "validator")]
149impl validator::ValidateRange<DurationFlex> for DurationFlex {
150 fn greater_than(&self, max: DurationFlex) -> Option<bool> {
151 Some(self > &max)
152 }
153
154 fn less_than(&self, min: DurationFlex) -> Option<bool> {
155 Some(self < &min)
156 }
157}
158
159#[cfg(feature = "validator")]
160impl validator::ValidateRange<&str> for DurationFlex {
161 fn greater_than(&self, max: &str) -> Option<bool> {
162 let max = DurationFlex::try_from(max).expect("invalid duration string in validator bounds");
163 Some(self > &max)
164 }
165
166 fn less_than(&self, min: &str) -> Option<bool> {
167 let min = DurationFlex::try_from(min).expect("invalid duration string in validator bounds");
168 Some(self < &min)
169 }
170}
171
172#[cfg(feature = "validator")]
173impl validator::ValidateRange<i64> for DurationFlex {
174 fn greater_than(&self, max: i64) -> Option<bool> {
175 Some(self.secs > max)
176 }
177
178 fn less_than(&self, min: i64) -> Option<bool> {
179 Some(self.secs < min)
180 }
181}
182
183static REGEX_STR: &str =
184 r"^((?P<weeks>\d+)w)?((?P<days>\d+)d)?((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\d+)s)?$";
185
186static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(REGEX_STR).unwrap());
187
188impl DurationFlex {
189 pub fn secs(&self) -> i64 {
191 self.secs
192 }
193
194 pub fn nanos(&self) -> i32 {
196 self.nanos
197 }
198
199 fn de_component(r#match: Match) -> i64 {
200 r#match.as_str().parse().unwrap()
201 }
202
203 fn ser_component(secs: &mut i64, component: &str, component_secs: i64, f: &mut Formatter<'_>) -> std::fmt::Result {
204 let value = *secs / component_secs;
205 *secs -= value * component_secs;
206
207 if value == 0 {
208 Ok(())
209 } else {
210 write!(f, "{}{}", value, component)
211 }
212 }
213}
214
215impl Sub<Duration> for DurationFlex {
216 type Output = Duration;
217
218 fn sub(self, rhs: Duration) -> Self::Output {
219 Duration::from(self) - rhs
220 }
221}
222
223impl Add<Duration> for DurationFlex {
224 type Output = Duration;
225
226 fn add(self, rhs: Duration) -> Self::Output {
227 Duration::from(self) + rhs
228 }
229}
230
231impl<T> Add<DateTime<T>> for DurationFlex
232where
233 T: TimeZone,
234{
235 type Output = DateTime<T>;
236
237 fn add(self, rhs: DateTime<T>) -> Self::Output {
238 rhs + Duration::from(self)
239 }
240}
241
242impl<T> Add<DurationFlex> for DateTime<T>
243where
244 T: TimeZone,
245{
246 type Output = DateTime<T>;
247
248 fn add(self, rhs: DurationFlex) -> Self::Output {
249 self + Duration::from(rhs)
250 }
251}
252
253impl TryFrom<&str> for DurationFlex {
254 type Error = DurationFlexError;
255
256 fn try_from(value: &str) -> Result<Self, Self::Error> {
257 let captures = REGEX.captures(value).ok_or(DurationFlexError::InvalidFormat)?;
258
259 let weeks = Duration::try_weeks(captures.name("weeks").map_or(0i64, Self::de_component))
260 .ok_or(DurationFlexError::OutOfRange)?;
261 let days = Duration::try_days(captures.name("days").map_or(0i64, Self::de_component))
262 .ok_or(DurationFlexError::OutOfRange)?;
263 let hours = Duration::try_hours(captures.name("hours").map_or(0i64, Self::de_component))
264 .ok_or(DurationFlexError::OutOfRange)?;
265 let minutes = Duration::try_minutes(captures.name("minutes").map_or(0i64, Self::de_component))
266 .ok_or(DurationFlexError::OutOfRange)?;
267 let seconds = Duration::try_seconds(captures.name("seconds").map_or(0i64, Self::de_component))
268 .ok_or(DurationFlexError::OutOfRange)?;
269
270 let duration = weeks + days + hours + minutes + seconds;
271
272 Ok(DurationFlex { secs: duration.num_seconds(), nanos: 0i32 })
273 }
274}
275
276impl From<String> for DurationFlex {
277 fn from(value: String) -> Self {
278 DurationFlex::try_from(value.as_str()).unwrap()
279 }
280}
281
282impl From<Duration> for DurationFlex {
283 fn from(value: Duration) -> Self {
284 DurationFlex { secs: value.num_seconds(), nanos: 0i32 }
285 }
286}
287
288impl From<DurationFlex> for Duration {
289 fn from(value: DurationFlex) -> Self {
290 Duration::try_seconds(value.secs()).unwrap() + Duration::nanoseconds(value.nanos() as i64)
291 }
292}
293
294impl From<time::Duration> for DurationFlex {
295 fn from(value: time::Duration) -> Self {
296 DurationFlex { secs: value.as_secs() as i64, nanos: 0i32 }
297 }
298}
299
300impl From<DurationFlex> for time::Duration {
301 fn from(value: DurationFlex) -> Self {
302 time::Duration::from_secs(value.secs as u64).add(time::Duration::from_nanos(value.nanos as u64))
303 }
304}
305
306impl Display for DurationFlex {
307 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
308 let mut secs = self.secs;
309
310 Self::ser_component(&mut secs, "w", SECS_PER_WEEK, f)?;
311 Self::ser_component(&mut secs, "d", SECS_PER_DAY, f)?;
312 Self::ser_component(&mut secs, "h", SECS_PER_HOUR, f)?;
313 Self::ser_component(&mut secs, "m", SECS_PER_MINUTES, f)?;
314 Self::ser_component(&mut secs, "s", 1, f)
315 }
316}
317
318#[cfg(feature = "serde")]
319impl<'de> Deserialize<'de> for DurationFlex {
320 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
321 where
322 D: Deserializer<'de>,
323 {
324 static REGEX_MSG: &str =
325 "a String with the format weeks (w), days (d), hours (h), minutes (m) and/or seconds (s), in order";
326
327 struct DurationFlexVisitor;
328
329 impl<'de> Visitor<'de> for DurationFlexVisitor {
330 type Value = DurationFlex;
331
332 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
333 formatter.write_str(REGEX_MSG)
334 }
335
336 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
337 where
338 E: Error,
339 {
340 match DurationFlex::try_from(v) {
341 Ok(value) => Ok(value),
342 Err(DurationFlexError::InvalidFormat) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
343 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
344 }
345 }
346
347 fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
348 where
349 E: Error,
350 {
351 self.visit_str(v)
352 }
353
354 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
355 where
356 E: Error,
357 {
358 match DurationFlex::try_from(v.as_str()) {
359 Ok(value) => Ok(value),
360 Err(DurationFlexError::InvalidFormat) => {
361 Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self))
362 },
363 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self)),
364 }
365 }
366 }
367
368 deserializer.deserialize_string(DurationFlexVisitor)
369 }
370}
371
372#[cfg(feature = "serde")]
373impl Serialize for DurationFlex {
374 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
375 where
376 S: Serializer,
377 {
378 serializer.serialize_str(format!("{}", self).as_str())
379 }
380}
381
382#[cfg(feature = "clap")]
383impl From<OsStr> for DurationFlex {
384 fn from(value: OsStr) -> Self {
385 DurationFlex::try_from(value.to_str().unwrap()).unwrap()
386 }
387}
388
389#[cfg(feature = "clap")]
390impl From<DurationFlex> for OsStr {
391 fn from(value: DurationFlex) -> Self {
392 format!("{}", value).into()
393 }
394}
395
396impl FromStr for DurationFlex {
397 type Err = DurationFlexError;
398
399 fn from_str(s: &str) -> Result<Self, Self::Err> {
400 DurationFlex::try_from(s)
401 }
402}
403
404#[cfg(test)]
405mod test {
406
407 use serde::{Deserialize, Serialize};
408 use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
409
410 use super::*;
411
412 #[test]
413 fn de_string() {
414 let value = DurationFlex::try_from("1w2d").unwrap();
415 assert_eq!(value.secs(), 9 * SECS_PER_DAY);
416 assert_eq!(value.nanos(), 0);
417
418 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
419 assert_eq!(value.secs(), 9 * SECS_PER_DAY + 3 * SECS_PER_HOUR + 4 * SECS_PER_MINUTES + 5);
420 assert_eq!(value.nanos(), 0);
421
422 let value = DurationFlex::try_from("5s").unwrap();
423 assert_eq!(value.secs(), 5);
424 assert_eq!(value.nanos(), 0);
425
426 let value = DurationFlex::try_from("5s5d");
427 assert!(value.is_err());
428 }
429
430 #[test]
431 fn ser_string() {
432 let value = DurationFlex::try_from("1w2d").unwrap().to_string();
433 assert_eq!(value, "1w2d");
434
435 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap().to_string();
436 assert_eq!(value, "1w2d3h4m5s");
437
438 let value = DurationFlex::try_from("5s").unwrap().to_string();
439 assert_eq!(value, "5s");
440
441 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap().to_string();
442 assert_eq!(value, "2w1d3h4m5s");
443
444 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap().to_string();
445 assert_eq!(value, "2w1d4h4m5s");
446 }
447
448 #[test]
449 fn deserialize_nums() {
450 let value = DurationFlex::try_from("1w2d").unwrap();
451 assert_de_tokens(&value, &[Token::Str("1w2d")]);
452
453 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
454 assert_de_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
455
456 let value = DurationFlex::try_from("5s").unwrap();
457 assert_de_tokens(&value, &[Token::Str("5s")]);
458
459 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
460 assert_de_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
461
462 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
463 assert_de_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
464 }
465
466 #[test]
467 fn serialize() {
468 let value = DurationFlex::try_from("1w2d").unwrap();
469 assert_ser_tokens(&value, &[Token::Str("1w2d")]);
470
471 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
472 assert_ser_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
473
474 let value = DurationFlex::try_from("5s").unwrap();
475 assert_ser_tokens(&value, &[Token::Str("5s")]);
476
477 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
478 assert_ser_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
479
480 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
481 assert_ser_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
482 }
483
484 #[test]
485 fn in_struct() {
486 #[derive(Serialize, Deserialize)]
487 struct SomeStruct {
488 duration: DurationFlex,
489 }
490
491 let value = SomeStruct { duration: Duration::try_weeks(1).unwrap().into() };
492
493 assert_ser_tokens(
494 &value,
495 &[Token::Struct { name: "SomeStruct", len: 1 }, Token::Str("duration"), Token::Str("1w"), Token::StructEnd],
496 );
497 }
498
499 #[cfg(feature = "validator")]
500 #[test]
501 fn validator() {
502 use validator::Validate;
503
504 #[derive(Validate)]
505 struct SomeStruct {
506 #[validate(range(
507 min = "DurationFlex::try_from(\"1h\").unwrap()",
508 max = "DurationFlex::try_from(\"2h\").unwrap()"
509 ))]
510 duration: DurationFlex,
511 }
512
513 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
514 assert!(value.validate().is_ok());
515
516 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
517 assert!(value.validate().is_err());
518
519 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
520 assert!(value.validate().is_err());
521 }
522
523 #[cfg(feature = "validator")]
524 #[test]
525 fn validator_str() {
526 use validator::Validate;
527
528 #[derive(Validate)]
529 struct SomeStruct {
530 #[validate(range(min = "\"1h\"", max = "\"2h\""))]
531 duration: DurationFlex,
532 }
533
534 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
535 assert!(value.validate().is_ok());
536
537 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
538 assert!(value.validate().is_err());
539
540 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
541 assert!(value.validate().is_err());
542 }
543
544 #[cfg(feature = "validator")]
545 #[test]
546 fn validator_int() {
547 use validator::Validate;
548
549 #[derive(Validate)]
550 struct SomeStruct {
551 #[validate(range(min = 3600, max = 7200))]
552 duration: DurationFlex,
553 }
554
555 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
556 assert!(value.validate().is_ok());
557
558 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
559 assert!(value.validate().is_err());
560
561 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
562 assert!(value.validate().is_err());
563 }
564}