use std::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(all(feature = "jiff", feature = "chrono"))]
compile_error!("features `jiff` and `chrono` are mutually exclusive");
#[cfg(feature = "jiff")]
mod backend {
use std::fmt;
pub type Inner = jiff::Timestamp;
pub fn parse(s: &str) -> Result<Inner, impl fmt::Display> {
s.parse::<jiff::Timestamp>()
}
pub fn now_formatted() -> impl fmt::Display {
jiff::Timestamp::now().strftime("%Y-%m-%dT%H:%M:%S%:z")
}
pub fn format(ts: &Inner, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(ts, f)
}
}
#[cfg(feature = "chrono")]
mod backend {
use std::fmt;
use chrono::SecondsFormat;
pub type Inner = chrono::DateTime<chrono::FixedOffset>;
pub fn parse(s: &str) -> Result<Inner, impl fmt::Display> {
chrono::DateTime::parse_from_rfc3339(s)
}
pub fn now_formatted() -> impl fmt::Display {
chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%:z")
}
pub fn format(ts: &Inner, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&ts.to_rfc3339_opts(SecondsFormat::AutoSi, true))
}
}
use backend::Inner;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(transparent)]
pub struct Rfc3339(pub Inner);
impl<'de> Deserialize<'de> for Rfc3339 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
backend::parse(&s).map(Rfc3339).map_err(|e| {
serde::de::Error::custom(format!(
"invalid RFC 3339 timestamp '{}': {}\nExample: current time is {}",
s,
e,
backend::now_formatted()
))
})
}
}
impl JsonSchema for Rfc3339 {
fn schema_name() -> String {
"Rfc3339".to_string()
}
fn is_referenceable() -> bool {
false
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
format: Some("date-time".to_string()),
..Default::default()
}
.into()
}
}
impl fmt::Display for Rfc3339 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
backend::format(&self.0, f)
}
}
#[cfg(test)]
mod tests {
use super::{Rfc3339, backend};
#[test]
fn deserialize_valid_timestamps() {
let _utc: Rfc3339 = serde_json::from_str(r#""2024-03-11T10:00:00Z""#).expect("valid UTC");
let _offset: Rfc3339 =
serde_json::from_str(r#""2024-03-11T12:00:00+02:00""#).expect("valid offset");
}
#[test]
fn display_outputs_valid_rfc3339() {
let cases = [
r#""2024-03-11T10:00:00Z""#,
r#""2024-03-11T12:00:00+02:00""#,
r#""2024-12-31T23:59:59-05:00""#,
r#""2000-01-01T00:00:00+00:00""#,
];
for input in cases {
let ts: Rfc3339 = serde_json::from_str(input).expect("valid input");
let displayed = ts.to_string();
backend::parse(&displayed).unwrap_or_else(|e| {
panic!(
"Display output '{}' is not valid RFC 3339: {}",
displayed, e
)
});
}
}
#[test]
fn error_message_format() {
let err = serde_json::from_str::<Rfc3339>(r#""2025-05-25 14:30:00""#).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("invalid RFC 3339 timestamp '2025-05-25 14:30:00'"));
assert!(msg.contains("Example: current time is"));
}
#[test]
fn roundtrip() {
let ts: Rfc3339 = serde_json::from_str(r#""2024-03-11T10:00:00Z""#).expect("valid");
let serialized = serde_json::to_string(&ts).expect("serializes");
let reparsed: Rfc3339 = serde_json::from_str(&serialized).expect("valid");
assert_eq!(reparsed, ts);
}
#[test]
fn json_schema() {
let schema = schemars::schema_for!(Rfc3339);
let json = serde_json::to_string_pretty(&schema).expect("schema serializes");
insta::assert_snapshot!(json, @r#"
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Rfc3339",
"type": "string",
"format": "date-time"
}
"#);
}
}