use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use crate::{Error, Result};
pub fn parse_rfc3339_timestamp(value: &str) -> Result<OffsetDateTime> {
let value = value.trim();
if value.is_empty() {
return Err(Error::InvalidParameter(
"timestamp must be a non-empty RFC3339 value".to_owned(),
));
}
OffsetDateTime::parse(value, &Rfc3339)
.map_err(|_| Error::InvalidParameter("timestamp must be a valid RFC3339 value".to_owned()))
}
pub fn parse_optional_rfc3339_timestamp(value: Option<&str>) -> Result<Option<OffsetDateTime>> {
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
return Ok(None);
};
parse_rfc3339_timestamp(value).map(Some)
}
pub trait TimestampExt {
fn parse_openbao_timestamp(&self) -> Result<OffsetDateTime>;
}
impl TimestampExt for str {
fn parse_openbao_timestamp(&self) -> Result<OffsetDateTime> {
parse_rfc3339_timestamp(self)
}
}
impl TimestampExt for String {
fn parse_openbao_timestamp(&self) -> Result<OffsetDateTime> {
self.as_str().parse_openbao_timestamp()
}
}
pub trait OptionalTimestampExt {
fn parse_optional_openbao_timestamp(&self) -> Result<Option<OffsetDateTime>>;
}
impl OptionalTimestampExt for Option<&str> {
fn parse_optional_openbao_timestamp(&self) -> Result<Option<OffsetDateTime>> {
parse_optional_rfc3339_timestamp(*self)
}
}
impl OptionalTimestampExt for Option<String> {
fn parse_optional_openbao_timestamp(&self) -> Result<Option<OffsetDateTime>> {
parse_optional_rfc3339_timestamp(self.as_deref())
}
}
#[cfg(test)]
mod tests {
use time::UtcOffset;
use crate::{Error, Result};
use super::{
OptionalTimestampExt, TimestampExt, parse_optional_rfc3339_timestamp,
parse_rfc3339_timestamp,
};
#[test]
fn parses_rfc3339_timestamp() -> Result<()> {
let parsed = parse_rfc3339_timestamp("2026-05-28T00:00:00Z")?;
assert_eq!(parsed.date().year(), 2026);
assert_eq!(parsed.offset(), UtcOffset::UTC);
Ok(())
}
#[test]
fn parses_offset_timestamp() -> Result<()> {
let parsed = "2026-05-28T02:30:00+02:00".parse_openbao_timestamp()?;
assert_eq!(parsed.date().year(), 2026);
assert_eq!(parsed.offset().whole_hours(), 2);
Ok(())
}
#[test]
fn treats_empty_optional_timestamp_as_absent() -> Result<()> {
assert_eq!(parse_optional_rfc3339_timestamp(None)?, None);
assert_eq!(Some("").parse_optional_openbao_timestamp()?, None);
Ok(())
}
#[test]
fn rejects_invalid_timestamp_without_echoing_value() -> Result<()> {
let error = match parse_rfc3339_timestamp("not-a-timestamp") {
Ok(_) => return Err(Error::Internal("invalid timestamp unexpectedly parsed")),
Err(error) => error.to_string(),
};
assert!(!error.contains("not-a-timestamp"));
assert!(error.contains("RFC3339"));
Ok(())
}
}