Skip to content

Commit

Permalink
Implement first iteration of custom parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Techassi committed Sep 8, 2023
1 parent 0e5658f commit 794dca8
Showing 1 changed file with 184 additions and 79 deletions.
263 changes: 184 additions & 79 deletions src/duration.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,161 @@
//! This module contains a common [`Duration`] struct which is able to parse
//! human-readable duration formats, like `2y 2h 20m 42s`, `15d 2m 2s`, or
//! `2018-01-01T12:53:00Z` defined by [RFC 3339][1]. The [`Duration`] exported
//! in this module doesn't provide the parsing logic by itself, but instead
//! uses the crate [humantime]. It additionally implements many required
//! traits, like [`Derivative`], [`JsonSchema`], [`Deserialize`], and
//! [`Serialize`].
//! human-readable duration formats, like `2y 2h 20m 42s` or`15d 2m 2s`. It
//! additionally implements many required traits, like [`Derivative`],
//! [`JsonSchema`], [`Deserialize`], and [`Serialize`].
//!
//! Furthermore, it implements [`Deref`], which enables us to use all associated
//! functions of [`humantime::Duration`] without re-implementing the public
//! functions of [`std::time::Duration`] without re-implementing the public
//! functions on our own type.
//!
//! All operators should opt for [`Duration`] instead of the plain
//! [`std::time::Duration`] when dealing with durations of any form, like
//! timeouts or retries.
//!
//! [1]: https://www.rfc-editor.org/rfc/rfc3339
use std::{fmt::Display, ops::Deref, str::FromStr};
use std::{num::ParseIntError, ops::Deref, str::FromStr};

use derivative::Derivative;
use schemars::{
gen::SchemaGenerator,
schema::{InstanceType, Schema, SchemaObject},
JsonSchema,
};
use serde::{de::Visitor, Deserialize, Serialize};
use serde::{de::Visitor, Deserialize};
use strum::Display;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum DurationParseError {
#[error("failed to parse string as number")]
ParseIntError(#[from] ParseIntError),

#[error("expected a number, found character")]
ExpectedNumber,

#[error("expected a character, found number")]
ExpectedChar,

#[error("found invalid character")]
InvalidInput,

#[error("found invalid unit")]
InvalidUnit,

#[error("number overflow")]
NumberOverflow,
}

/// A [`Duration`] which is capable of parsing human-readable duration formats,
/// like `2y 2h 20m 42s`, `15d 2m 2s`, or `2018-01-01T12:53:00Z` defined by
/// [RFC 3339][1]. It additionally provides many required trait implementations,
/// which makes it suited for use in CRDs for example.
///
/// [1]: https://www.rfc-editor.org/rfc/rfc3339
#[derive(Clone, Copy, Debug, Derivative, Hash, PartialEq)]
pub struct Duration(humantime::Duration);
/// like `2y 2h 20m 42s` or `15d 2m 2s`. It additionally provides many required
/// trait implementations, which makes it suited for use in CRDs for example.
#[derive(Clone, Copy, Debug, Derivative, Hash, PartialEq, PartialOrd)]
pub struct Duration(std::time::Duration);

#[derive(Copy, Clone, Debug, Display)]
enum DurationParseState {
Value,
Space,
Init,
Unit,
End,
}

impl FromStr for Duration {
type Err = DurationParseError;

fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut state = DurationParseState::Init;
let mut buffer = String::new();
let mut iter = input.chars();
let mut is_unit = false;
let mut cur = 0 as char;

let mut dur = std::time::Duration::from_secs(0);
let mut val = 0;

loop {
state = match state {
DurationParseState::Init => match iter.next() {
Some(c) => {
cur = c;

match c {
'0'..='9' => DurationParseState::Value,
'a'..='z' => DurationParseState::Unit,
' ' => DurationParseState::Space,
_ => return Err(DurationParseError::InvalidInput),
}
}
None => DurationParseState::End,
},
DurationParseState::Value => {
if is_unit {
return Err(DurationParseError::ExpectedChar);
}

buffer.push(cur);
DurationParseState::Init
}
DurationParseState::Unit => {
if !is_unit {
is_unit = true;

val = buffer.parse::<u64>()?;
buffer.clear();
}

buffer.push(cur);
DurationParseState::Init
}
DurationParseState::Space => {
if !is_unit {
return Err(DurationParseError::ExpectedChar);
}

let factor = parse_unit(&buffer)?;

dur = dur
.checked_add(std::time::Duration::from_secs(val * factor))
.ok_or(DurationParseError::NumberOverflow)?;

is_unit = false;
buffer.clear();

DurationParseState::Init
}
DurationParseState::End => {
if !is_unit {
return Err(DurationParseError::ExpectedChar);
}

let factor = parse_unit(&buffer)?;

dur = dur
.checked_add(std::time::Duration::from_secs(val * factor))
.ok_or(DurationParseError::NumberOverflow)?;

break;
}
}
}

Ok(Duration(dur))
}
}

fn parse_unit(buffer: &str) -> Result<u64, DurationParseError> {
let factor = match buffer {
"seconds" | "second" | "secs" | "sec" | "s" => 1,
"minutes" | "minute" | "mins" | "min" | "m" => 60,
"hours" | "hour" | "hrs" | "hr" | "h" => 3600,
"days" | "day" | "d" => 86400,
"weeks" | "week" | "w" => 86400 * 7,
"months" | "month" | "M" => 2_630_016,
"years" | "year" | "y" => 31_557_600,
_ => return Err(DurationParseError::InvalidUnit),
};

Ok(factor)
}

struct DurationVisitor;

Expand All @@ -48,11 +170,8 @@ impl<'de> Visitor<'de> for DurationVisitor {
where
E: serde::de::Error,
{
let dur = v
.parse::<humantime::Duration>()
.map_err(serde::de::Error::custom)?;

Ok(Duration(dur))
let dur = v.parse::<Duration>().map_err(serde::de::Error::custom)?;
Ok(dur)
}
}

Expand All @@ -65,14 +184,14 @@ impl<'de> Deserialize<'de> for Duration {
}
}

impl Serialize for Duration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.to_string().as_str())
}
}
// impl Serialize for Duration {
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
// where
// S: serde::Serializer,
// {
// serializer.serialize_str(self.0.to_string().as_str())
// }
// }

impl JsonSchema for Duration {
fn schema_name() -> String {
Expand All @@ -88,43 +207,35 @@ impl JsonSchema for Duration {
}
}

impl FromStr for Duration {
type Err = humantime::DurationError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse::<humantime::Duration>()?))
}
}

impl PartialOrd for Duration {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(&other.0)
}
}

impl Display for Duration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
// impl Display for Duration {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// write!(f, "{}", self.0)
// }
// }

impl Deref for Duration {
type Target = humantime::Duration;
type Target = std::time::Duration;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl From<std::time::Duration> for Duration {
fn from(value: std::time::Duration) -> Self {
Self(value)
}
}

impl Duration {
/// Creates a new [`Duration`] from the specified number of whole seconds.
pub fn from_secs(secs: u64) -> Self {
Self(std::time::Duration::from_secs(secs).into())
pub const fn from_secs(secs: u64) -> Self {
Self(std::time::Duration::from_secs(secs))
}

/// Creates a new [`Duration`] from the specified number of milliseconds.
pub fn from_millis(millis: u64) -> Self {
Self(std::time::Duration::from_millis(millis).into())
pub const fn from_millis(millis: u64) -> Self {
Self(std::time::Duration::from_millis(millis))
}
}

Expand All @@ -133,12 +244,6 @@ mod test {
use super::*;
use rstest::rstest;

#[test]
fn deref() {
let dur: Duration = "1h".parse().unwrap();
assert_eq!(dur.as_secs(), 3600);
}

#[rstest]
#[case("2y 2h 20m 42s", 63123642)]
#[case("15d 2m 2s", 1296122)]
Expand All @@ -161,25 +266,25 @@ mod test {
assert_eq!(s.dur.as_secs(), 1296122);
}

#[test]
fn serialize() {
#[derive(Serialize)]
struct S {
dur: Duration,
}
// #[test]
// fn serialize() {
// #[derive(Serialize)]
// struct S {
// dur: Duration,
// }

let s = S {
dur: "15d 2m 2s".parse().unwrap(),
};
assert_eq!(serde_yaml::to_string(&s).unwrap(), "dur: 15days 2m 2s\n");
}
// let s = S {
// dur: "15d 2m 2s".parse().unwrap(),
// };
// assert_eq!(serde_yaml::to_string(&s).unwrap(), "dur: 15days 2m 2s\n");
// }

#[test]
fn from_impls() {
let dur = Duration::from_secs(10);
assert_eq!(dur.to_string(), "10s");
// #[test]
// fn from_impls() {
// let dur = Duration::from_secs(10);
// assert_eq!(dur.to_string(), "10s");

let dur = Duration::from_millis(1000);
assert_eq!(dur.to_string(), "1s");
}
// let dur = Duration::from_millis(1000);
// assert_eq!(dur.to_string(), "1s");
// }
}

0 comments on commit 794dca8

Please sign in to comment.