030e59e
zhenyi
refactor(lib): migrate storage and security tier to central lib/config (auth, cache, db, email, jwt, payment, queue, rate_limit, storage)
030e59e
use serde::{Deserialize, Deserializer};
use std::time::Duration as StdDuration;
use time::Duration as TimeDuration;
fn de_error<E: serde::de::Error>(msg: impl std::fmt::Display) -> E {
E::custom(msg)
}
fn parse_native_time(s: &str) -> Result<TimeDuration, String> {
let (seconds, nanos) = s
.split_once('.')
.ok_or_else(|| format!("expected 'seconds.nanoseconds', got {s:?}"))?;
let seconds: i64 = seconds
.parse()
.map_err(|_| format!("invalid seconds in {s:?}"))?;
let nanos: i64 = nanos
.parse()
.map_err(|_| format!("invalid nanoseconds in {s:?}"))?;
Ok(TimeDuration::seconds(seconds) + TimeDuration::nanoseconds(nanos))
}
fn parse_human_time(s: &str) -> Result<TimeDuration, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty duration string".to_string());
}
if let Ok(secs) = s.parse::<i64>() {
return Ok(TimeDuration::seconds(secs));
}
let mut total = TimeDuration::ZERO;
let mut buf = String::new();
let mut any = false;
for ch in s.chars() {
if ch.is_ascii_digit() || ch == '-' {
buf.push(ch);
} else {
let value: i64 = buf
.parse()
.map_err(|_| format!("invalid number in duration {s:?}"))?;
match ch {
's' => total += TimeDuration::seconds(value),
'm' => total += TimeDuration::minutes(value),
'h' => total += TimeDuration::hours(value),
'd' => total += TimeDuration::days(value),
other => return Err(format!("unknown duration unit {other:?} in {s:?}")),
}
buf.clear();
any = true;
}
}
if !buf.is_empty() {
return Err(format!("trailing number without unit in {s:?}"));
}
if !any {
return Err(format!("cannot parse duration {s:?}"));
}
Ok(total)
}
pub fn deserialize_time_duration<'de, D>(d: D) -> Result<TimeDuration, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Secs(i64),
SecsF64(f64),
Text(String),
}
match Repr::deserialize(d)? {
Repr::Secs(secs) => Ok(TimeDuration::seconds(secs)),
Repr::SecsF64(secs) => Ok(TimeDuration::seconds_f64(secs)),
Repr::Text(t) if t.contains('.') => parse_native_time(&t).map_err(de_error),
Repr::Text(t) => parse_human_time(&t).map_err(de_error),
}
}
pub fn deserialize_std_duration<'de, D>(d: D) -> Result<StdDuration, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Secs(u64),
Struct { secs: u64, nanos: u32 },
}
match Repr::deserialize(d)? {
Repr::Secs(secs) => Ok(StdDuration::from_secs(secs)),
Repr::Struct { secs, nanos } => Ok(StdDuration::new(secs, nanos)),
}
}
pub fn deserialize_opt_std_duration<'de, D>(d: D) -> Result<Option<StdDuration>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Secs(u64),
Struct { secs: u64, nanos: u32 },
}
match Option::<Repr>::deserialize(d)? {
Some(Repr::Secs(secs)) => Ok(Some(StdDuration::from_secs(secs))),
Some(Repr::Struct { secs, nanos }) => Ok(Some(StdDuration::new(secs, nanos))),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct TimeWrap {
#[serde(deserialize_with = "deserialize_time_duration")]
d: TimeDuration,
}
#[derive(Deserialize)]
struct StdWrap {
#[serde(deserialize_with = "deserialize_std_duration")]
d: StdDuration,
}
#[derive(Deserialize)]
struct OptStdWrap {
#[serde(default, deserialize_with = "deserialize_opt_std_duration")]
d: Option<StdDuration>,
}
#[test]
fn time_duration_from_bare_seconds() {
let w: TimeWrap = serde_json::from_str(r#"{"d":900}"#).unwrap();
assert_eq!(w.d, TimeDuration::seconds(900));
}
#[test]
fn time_duration_from_human_string() {
let w: TimeWrap = serde_json::from_str(r#"{"d":"1h30m"}"#).unwrap();
assert_eq!(w.d, TimeDuration::hours(1) + TimeDuration::minutes(30));
}
#[test]
fn time_duration_from_native_string() {
let w: TimeWrap = serde_json::from_str(r#"{"d":"900.000000000"}"#).unwrap();
assert_eq!(w.d, TimeDuration::seconds(900));
}
#[test]
fn std_duration_from_bare_seconds() {
let w: StdWrap = serde_json::from_str(r#"{"d":30}"#).unwrap();
assert_eq!(w.d, StdDuration::from_secs(30));
}
#[test]
fn std_duration_from_native_struct() {
let w: StdWrap = serde_json::from_str(r#"{"d":{"secs":30,"nanos":0}}"#).unwrap();
assert_eq!(w.d, StdDuration::from_secs(30));
}
#[test]
fn opt_std_duration_none_when_absent() {
let w: OptStdWrap = serde_json::from_str(r#"{}"#).unwrap();
assert_eq!(w.d, None);
}
#[test]
fn opt_std_duration_some_from_seconds() {
let w: OptStdWrap = serde_json::from_str(r#"{"d":300}"#).unwrap();
assert_eq!(w.d, Some(StdDuration::from_secs(300)));
}
}