timeout-macro-parse 0.2.0

A companion-crate for the tokio-timeout proc-macro-lib
Documentation
use std::time::Duration;

pub(crate) fn parse_duration(dur: &str) -> Result<Duration, String> {
    let dur = dur.trim_matches('"');
    let mut it = dur.chars().enumerate();
    let mut prev_ind = None;
    let mut parsed_dur = Duration::ZERO;
    let mut dirty = false;
    loop {
        let Some((ind, ch)) = it.next() else {
            return if parsed_dur == Duration::ZERO {
                Err(format!("parsing '{dur}' resulted in a zero duration"))
            } else if dirty {
                Err(format!(
                    "parsing '{dur}' resulted in an unfinished calculation"
                ))
            } else {
                Ok(parsed_dur)
            };
        };
        parse_next(
            &mut prev_ind,
            ind,
            ch,
            dur,
            &mut parsed_dur,
            &mut it,
            &mut dirty,
        )?;
    }
}

fn parse_next(
    prev_ind: &mut Option<usize>,
    ind: usize,
    ch: char,
    dur: &str,
    cumulative_dur: &mut Duration,
    iterator: &mut impl Iterator<Item = (usize, char)>,
    dirty: &mut bool,
) -> Result<(), String> {
    if ch.is_alphabetic() {
        let pi = prev_ind.unwrap_or_default();
        let Some(prev) = dur.get(pi..ind) else {
            return Err(format!("failed to parse duration from: '{dur}'"));
        };
        let num = parse_num(prev)?;
        let (add_dur, rem, add) = create_duration(num, ch, iterator)?;
        *cumulative_dur = cumulative_dur.saturating_add(add_dur);
        *dirty = false;
        *prev_ind = Some(ind + add);
        if let Some((next_ind, ch)) = rem {
            parse_next(prev_ind, next_ind, ch, dur, cumulative_dur, iterator, dirty)?;
        }
    } else {
        *dirty = true;
    }
    Ok(())
}

fn parse_num(sect: &str) -> Result<u64, String> {
    if sect.is_empty() {
        return Err("failed to parse num, empty section".to_string());
    };
    sect.parse()
        .map_err(|e| format!("failed to parse num from '{sect}': {e}"))
}

#[allow(clippy::type_complexity)]
fn create_duration(
    num: u64,
    lead_char: char,
    iterator: &mut impl Iterator<Item = (usize, char)>,
) -> Result<(Duration, Option<(usize, char)>, usize), String> {
    let (unit, rem, add) = parse_unit(lead_char, iterator)?;
    let dur = match unit {
        AcceptedUnits::Hour => Duration::from_secs(num * 60 * 60),
        AcceptedUnits::Minute => Duration::from_secs(num * 60),
        AcceptedUnits::Second => Duration::from_secs(num),
        AcceptedUnits::Millisecond => Duration::from_millis(num),
    };
    Ok((dur, rem, add))
}

#[allow(clippy::type_complexity)]
fn parse_unit(
    start: char,
    iterator: &mut impl Iterator<Item = (usize, char)>,
) -> Result<(AcceptedUnits, Option<(usize, char)>, usize), String> {
    match start {
        'h' => Ok((AcceptedUnits::Hour, None, 1)),
        'm' => {
            let next = iterator.next();
            if let Some((_, 's')) = next {
                Ok((AcceptedUnits::Millisecond, None, 2))
            } else {
                Ok((AcceptedUnits::Minute, next, 1))
            }
        }
        's' => Ok((AcceptedUnits::Second, None, 1)),
        unk => Err(format!("unknown unit start: '{unk}'")),
    }
}

enum AcceptedUnits {
    Hour,
    Minute,
    Second,
    Millisecond,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_reasonable_durations() {
        let hours = parse_duration("3h").unwrap();
        assert_eq!(hours, Duration::from_secs(3 * 3600));
        let minutes = parse_duration("11m").unwrap();
        assert_eq!(minutes, Duration::from_secs(11 * 60));
        let seconds = parse_duration("55s").unwrap();
        assert_eq!(seconds, Duration::from_secs(55));
        let millis = parse_duration("100ms").unwrap();
        assert_eq!(millis, Duration::from_millis(100));
        let combined = "1h2m3s4ms";
        let dur = parse_duration(combined).unwrap();
        let expect = Duration::from_secs(3600)
            + Duration::from_secs(120)
            + Duration::from_secs(3)
            + Duration::from_millis(4);
        assert_eq!(dur, expect);
    }

    #[test]
    fn parse_unreasonable_additive_durations() {
        let dur = "1h1h1h1h";
        let dur = parse_duration(dur).unwrap();
        assert_eq!(Duration::from_secs(3600 * 4), dur);
        let dur = "1m1m1m";
        let dur = parse_duration(dur).unwrap();
        assert_eq!(Duration::from_secs(60 * 3), dur);
        let dur = "1s1s";
        let dur = parse_duration(dur).unwrap();
        assert_eq!(Duration::from_secs(2), dur);
        let dur = "1ms1ms1ms1ms1ms1ms1ms";
        let dur = parse_duration(dur).unwrap();
        assert_eq!(Duration::from_millis(7), dur);
        let dur = "5ms2s1h5ms1m1s";
        let dur = parse_duration(dur).unwrap();
        assert_eq!(
            Duration::from_millis(10) + Duration::from_secs(63) + Duration::from_secs(3600),
            dur
        );
    }
}