1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
/*
* SPDX-FileCopyrightText: 2023 Misato Kano <me@mirror-kt.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
//! `mwtimestamp` is a library for parsing and formatting MediaWiki
//! timestamps, powered by [`chrono`](https://docs.rs/chrono).
//!
//! The [MediaWiki API](https://www.mediawiki.org/w/api.php#main/datatype/timestamp)
//! typically produces ISO 8601 timestamps. In some cases, like protection or
//! block expiry, it may alternatively return the string "infinity" to represent
//! that there is no end period.
//!
//! ```
//! use mwtimestamp::{Expiry, Timestamp};
//! // Deserializing a fixed timestamp
//! let finite: Timestamp = serde_json::from_str("\"2001-01-15T14:56:00Z\"").unwrap();
//! assert_eq!(
//! finite.date_naive(),
//! chrono::NaiveDate::from_ymd_opt(2001, 1, 15).unwrap(),
//! );
//! // Deserializing an infinite timestamp
//! let infinity: Expiry = serde_json::from_str("\"infinity\"").unwrap();
//! assert!(infinity.is_infinity());
//! ```
//!
//! ## Contributing
//! `mwtimestamp` is a part of the [`mwbot-rs` project](https://www.mediawiki.org/wiki/Mwbot-rs).
//! We're always looking for new contributors, please [reach out](https://www.mediawiki.org/wiki/Mwbot-rs#Contributing)
//! if you're interested!
#![deny(clippy::all)]
#![deny(rustdoc::all)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Deref;
/// Represents a MediaWiki timestamp, which are always in UTC.
/// It is basically a wrapper around [`DateTime`] but serializes and
/// deserializes in the ISO 8601 format MediaWiki wants.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize)]
pub struct Timestamp(DateTime<Utc>);
impl Timestamp {
pub fn new<Tz: TimeZone>(date_time: DateTime<Tz>) -> Self {
let utc_date_time = date_time.naive_utc().and_utc();
Self(utc_date_time)
}
pub fn now() -> Self {
Self(Utc::now())
}
}
impl<Tz: TimeZone> From<DateTime<Tz>> for Timestamp {
fn from(value: DateTime<Tz>) -> Self {
Self::new(value)
}
}
impl Deref for Timestamp {
type Target = DateTime<Utc>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
self.0.to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
)
}
}
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
/// A MediaWiki expiry, which can either be infinity (aka indefinite) or
/// a specific [`Timestamp`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum Expiry {
#[serde(rename = "infinity")]
Infinity,
#[serde(untagged)]
Finite(Timestamp),
}
impl Expiry {
/// Whether the expiry is for an infinite (aka indefinite) amount of time
pub fn is_infinity(&self) -> bool {
matches!(self, Self::Infinity)
}
/// If the expiry is finite, get a timestamp for it
pub fn as_timestamp(&self) -> Option<&Timestamp> {
match self {
Expiry::Infinity => None,
Expiry::Finite(datetime) => Some(datetime),
}
}
}
impl fmt::Display for Expiry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expiry::Infinity => {
write!(f, "infinity")
}
Expiry::Finite(ts) => {
write!(f, "{ts}")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
#[test]
fn test_timestamp_deserialize() {
let ts: Timestamp =
serde_json::from_str("\"2001-01-15T14:56:00.000000Z\"").unwrap();
assert_eq!(
ts.date_naive(),
NaiveDate::from_ymd_opt(2001, 1, 15).unwrap(),
);
assert_eq!(ts.time(), NaiveTime::from_hms_opt(14, 56, 00).unwrap(),);
}
#[test]
fn test_timestamp_serialize() {
let ts = Timestamp(
NaiveDateTime::new(
NaiveDate::from_ymd_opt(2001, 1, 15).unwrap(),
NaiveTime::from_hms_opt(14, 56, 00).unwrap(),
)
.and_utc(),
);
assert_eq!(
serde_json::to_string(&ts).unwrap(),
"\"2001-01-15T14:56:00.000000Z\""
);
assert_eq!(ts.to_string(), "2001-01-15T14:56:00.000000Z");
}
#[test]
fn test_expiry_deserialize() {
let infinity: Expiry = serde_json::from_str("\"infinity\"").unwrap();
assert!(infinity.as_timestamp().is_none());
assert!(infinity.is_infinity());
let finite: Expiry =
serde_json::from_str("\"2001-01-15T14:56:00.000000Z\"").unwrap();
assert_eq!(
finite.as_timestamp().unwrap().date_naive(),
NaiveDate::from_ymd_opt(2001, 1, 15).unwrap(),
);
assert_eq!(
finite.as_timestamp().unwrap().time(),
NaiveTime::from_hms_opt(14, 56, 00).unwrap(),
);
}
#[test]
fn test_expiry_serialize() {
assert_eq!(
serde_json::to_string(&Expiry::Infinity).unwrap(),
"\"infinity\""
);
let finite = Expiry::Finite(Timestamp(
NaiveDateTime::new(
NaiveDate::from_ymd_opt(2001, 1, 15).unwrap(),
NaiveTime::from_hms_opt(14, 56, 00).unwrap(),
)
.and_utc(),
));
assert_eq!(
serde_json::to_string(&finite).unwrap(),
"\"2001-01-15T14:56:00.000000Z\""
);
}
}