545 lines
17 KiB
Rust
545 lines
17 KiB
Rust
|
use std::error;
|
||
|
use std::fmt;
|
||
|
use std::str::{self, FromStr};
|
||
|
|
||
|
use serde::{de, ser};
|
||
|
|
||
|
/// A parsed TOML datetime value
|
||
|
///
|
||
|
/// This structure is intended to represent the datetime primitive type that can
|
||
|
/// be encoded into TOML documents. This type is a parsed version that contains
|
||
|
/// all metadata internally.
|
||
|
///
|
||
|
/// Currently this type is intentionally conservative and only supports
|
||
|
/// `to_string` as an accessor. Over time though it's intended that it'll grow
|
||
|
/// more support!
|
||
|
///
|
||
|
/// Note that if you're using `Deserialize` to deserialize a TOML document, you
|
||
|
/// can use this as a placeholder for where you're expecting a datetime to be
|
||
|
/// specified.
|
||
|
///
|
||
|
/// Also note though that while this type implements `Serialize` and
|
||
|
/// `Deserialize` it's only recommended to use this type with the TOML format,
|
||
|
/// otherwise encoded in other formats it may look a little odd.
|
||
|
///
|
||
|
/// Depending on how the option values are used, this struct will correspond
|
||
|
/// with one of the following four datetimes from the [TOML v1.0.0 spec]:
|
||
|
///
|
||
|
/// | `date` | `time` | `offset` | TOML type |
|
||
|
/// | --------- | --------- | --------- | ------------------ |
|
||
|
/// | `Some(_)` | `Some(_)` | `Some(_)` | [Offset Date-Time] |
|
||
|
/// | `Some(_)` | `Some(_)` | `None` | [Local Date-Time] |
|
||
|
/// | `Some(_)` | `None` | `None` | [Local Date] |
|
||
|
/// | `None` | `Some(_)` | `None` | [Local Time] |
|
||
|
///
|
||
|
/// **1. Offset Date-Time**: If all the optional values are used, `Datetime`
|
||
|
/// corresponds to an [Offset Date-Time]. From the TOML v1.0.0 spec:
|
||
|
///
|
||
|
/// > To unambiguously represent a specific instant in time, you may use an
|
||
|
/// > RFC 3339 formatted date-time with offset.
|
||
|
/// >
|
||
|
/// > ```toml
|
||
|
/// > odt1 = 1979-05-27T07:32:00Z
|
||
|
/// > odt2 = 1979-05-27T00:32:00-07:00
|
||
|
/// > odt3 = 1979-05-27T00:32:00.999999-07:00
|
||
|
/// > ```
|
||
|
/// >
|
||
|
/// > For the sake of readability, you may replace the T delimiter between date
|
||
|
/// > and time with a space character (as permitted by RFC 3339 section 5.6).
|
||
|
/// >
|
||
|
/// > ```toml
|
||
|
/// > odt4 = 1979-05-27 07:32:00Z
|
||
|
/// > ```
|
||
|
///
|
||
|
/// **2. Local Date-Time**: If `date` and `time` are given but `offset` is
|
||
|
/// `None`, `Datetime` corresponds to a [Local Date-Time]. From the spec:
|
||
|
///
|
||
|
/// > If you omit the offset from an RFC 3339 formatted date-time, it will
|
||
|
/// > represent the given date-time without any relation to an offset or
|
||
|
/// > timezone. It cannot be converted to an instant in time without additional
|
||
|
/// > information. Conversion to an instant, if required, is implementation-
|
||
|
/// > specific.
|
||
|
/// >
|
||
|
/// > ```toml
|
||
|
/// > ldt1 = 1979-05-27T07:32:00
|
||
|
/// > ldt2 = 1979-05-27T00:32:00.999999
|
||
|
/// > ```
|
||
|
///
|
||
|
/// **3. Local Date**: If only `date` is given, `Datetime` corresponds to a
|
||
|
/// [Local Date]; see the docs for [`Date`].
|
||
|
///
|
||
|
/// **4. Local Time**: If only `time` is given, `Datetime` corresponds to a
|
||
|
/// [Local Time]; see the docs for [`Time`].
|
||
|
///
|
||
|
/// [TOML v1.0.0 spec]: https://toml.io/en/v1.0.0
|
||
|
/// [Offset Date-Time]: https://toml.io/en/v1.0.0#offset-date-time
|
||
|
/// [Local Date-Time]: https://toml.io/en/v1.0.0#local-date-time
|
||
|
/// [Local Date]: https://toml.io/en/v1.0.0#local-date
|
||
|
/// [Local Time]: https://toml.io/en/v1.0.0#local-time
|
||
|
#[derive(PartialEq, Clone)]
|
||
|
pub struct Datetime {
|
||
|
/// Optional date.
|
||
|
/// Required for: *Offset Date-Time*, *Local Date-Time*, *Local Date*.
|
||
|
pub date: Option<Date>,
|
||
|
|
||
|
/// Optional time.
|
||
|
/// Required for: *Offset Date-Time*, *Local Date-Time*, *Local Time*.
|
||
|
pub time: Option<Time>,
|
||
|
|
||
|
/// Optional offset.
|
||
|
/// Required for: *Offset Date-Time*.
|
||
|
pub offset: Option<Offset>,
|
||
|
}
|
||
|
|
||
|
/// Error returned from parsing a `Datetime` in the `FromStr` implementation.
|
||
|
#[derive(Debug, Clone)]
|
||
|
pub struct DatetimeParseError {
|
||
|
_private: (),
|
||
|
}
|
||
|
|
||
|
// Currently serde itself doesn't have a datetime type, so we map our `Datetime`
|
||
|
// to a special valid in the serde data model. Namely one with these special
|
||
|
// fields/struct names.
|
||
|
//
|
||
|
// In general the TOML encoder/decoder will catch this and not literally emit
|
||
|
// these strings but rather emit datetimes as they're intended.
|
||
|
pub const FIELD: &str = "$__toml_private_datetime";
|
||
|
pub const NAME: &str = "$__toml_private_Datetime";
|
||
|
|
||
|
/// A parsed TOML date value
|
||
|
///
|
||
|
/// May be part of a [`Datetime`]. Alone, `Date` corresponds to a [Local Date].
|
||
|
/// From the TOML v1.0.0 spec:
|
||
|
///
|
||
|
/// > If you include only the date portion of an RFC 3339 formatted date-time,
|
||
|
/// > it will represent that entire day without any relation to an offset or
|
||
|
/// > timezone.
|
||
|
/// >
|
||
|
/// > ```toml
|
||
|
/// > ld1 = 1979-05-27
|
||
|
/// > ```
|
||
|
///
|
||
|
/// [Local Date]: https://toml.io/en/v1.0.0#local-date
|
||
|
#[derive(PartialEq, Clone)]
|
||
|
pub struct Date {
|
||
|
/// Year: four digits
|
||
|
pub year: u16,
|
||
|
/// Month: 1 to 12
|
||
|
pub month: u8,
|
||
|
/// Day: 1 to {28, 29, 30, 31} (based on month/year)
|
||
|
pub day: u8,
|
||
|
}
|
||
|
|
||
|
/// A parsed TOML time value
|
||
|
///
|
||
|
/// May be part of a [`Datetime`]. Alone, `Time` corresponds to a [Local Time].
|
||
|
/// From the TOML v1.0.0 spec:
|
||
|
///
|
||
|
/// > If you include only the time portion of an RFC 3339 formatted date-time,
|
||
|
/// > it will represent that time of day without any relation to a specific
|
||
|
/// > day or any offset or timezone.
|
||
|
/// >
|
||
|
/// > ```toml
|
||
|
/// > lt1 = 07:32:00
|
||
|
/// > lt2 = 00:32:00.999999
|
||
|
/// > ```
|
||
|
/// >
|
||
|
/// > Millisecond precision is required. Further precision of fractional
|
||
|
/// > seconds is implementation-specific. If the value contains greater
|
||
|
/// > precision than the implementation can support, the additional precision
|
||
|
/// > must be truncated, not rounded.
|
||
|
///
|
||
|
/// [Local Time]: https://toml.io/en/v1.0.0#local-time
|
||
|
#[derive(PartialEq, Clone)]
|
||
|
pub struct Time {
|
||
|
/// Hour: 0 to 23
|
||
|
pub hour: u8,
|
||
|
/// Minute: 0 to 59
|
||
|
pub minute: u8,
|
||
|
/// Second: 0 to {58, 59, 60} (based on leap second rules)
|
||
|
pub second: u8,
|
||
|
/// Nanosecond: 0 to 999_999_999
|
||
|
pub nanosecond: u32,
|
||
|
}
|
||
|
|
||
|
/// A parsed TOML time offset
|
||
|
///
|
||
|
#[derive(PartialEq, Clone)]
|
||
|
pub enum Offset {
|
||
|
/// > A suffix which, when applied to a time, denotes a UTC offset of 00:00;
|
||
|
/// > often spoken "Zulu" from the ICAO phonetic alphabet representation of
|
||
|
/// > the letter "Z". --- [RFC 3339 section 2]
|
||
|
///
|
||
|
/// [RFC 3339 section 2]: https://datatracker.ietf.org/doc/html/rfc3339#section-2
|
||
|
Z,
|
||
|
|
||
|
/// Offset between local time and UTC
|
||
|
Custom {
|
||
|
/// Hours: -12 to +12
|
||
|
hours: i8,
|
||
|
|
||
|
/// Minutes: 0 to 59
|
||
|
minutes: u8,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
impl fmt::Debug for Datetime {
|
||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
fmt::Display::fmt(self, f)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl fmt::Display for Datetime {
|
||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
if let Some(ref date) = self.date {
|
||
|
write!(f, "{}", date)?;
|
||
|
}
|
||
|
if let Some(ref time) = self.time {
|
||
|
if self.date.is_some() {
|
||
|
write!(f, "T")?;
|
||
|
}
|
||
|
write!(f, "{}", time)?;
|
||
|
}
|
||
|
if let Some(ref offset) = self.offset {
|
||
|
write!(f, "{}", offset)?;
|
||
|
}
|
||
|
Ok(())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl fmt::Display for Date {
|
||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl fmt::Display for Time {
|
||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
|
||
|
if self.nanosecond != 0 {
|
||
|
let s = format!("{:09}", self.nanosecond);
|
||
|
write!(f, ".{}", s.trim_end_matches('0'))?;
|
||
|
}
|
||
|
Ok(())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl fmt::Display for Offset {
|
||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
match *self {
|
||
|
Offset::Z => write!(f, "Z"),
|
||
|
Offset::Custom { hours, minutes } => write!(f, "{:+03}:{:02}", hours, minutes),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl FromStr for Datetime {
|
||
|
type Err = DatetimeParseError;
|
||
|
|
||
|
fn from_str(date: &str) -> Result<Datetime, DatetimeParseError> {
|
||
|
// Accepted formats:
|
||
|
//
|
||
|
// 0000-00-00T00:00:00.00Z
|
||
|
// 0000-00-00T00:00:00.00
|
||
|
// 0000-00-00
|
||
|
// 00:00:00.00
|
||
|
if date.len() < 3 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
let mut offset_allowed = true;
|
||
|
let mut chars = date.chars();
|
||
|
|
||
|
// First up, parse the full date if we can
|
||
|
let full_date = if chars.clone().nth(2) == Some(':') {
|
||
|
offset_allowed = false;
|
||
|
None
|
||
|
} else {
|
||
|
let y1 = u16::from(digit(&mut chars)?);
|
||
|
let y2 = u16::from(digit(&mut chars)?);
|
||
|
let y3 = u16::from(digit(&mut chars)?);
|
||
|
let y4 = u16::from(digit(&mut chars)?);
|
||
|
|
||
|
match chars.next() {
|
||
|
Some('-') => {}
|
||
|
_ => return Err(DatetimeParseError { _private: () }),
|
||
|
}
|
||
|
|
||
|
let m1 = digit(&mut chars)?;
|
||
|
let m2 = digit(&mut chars)?;
|
||
|
|
||
|
match chars.next() {
|
||
|
Some('-') => {}
|
||
|
_ => return Err(DatetimeParseError { _private: () }),
|
||
|
}
|
||
|
|
||
|
let d1 = digit(&mut chars)?;
|
||
|
let d2 = digit(&mut chars)?;
|
||
|
|
||
|
let date = Date {
|
||
|
year: y1 * 1000 + y2 * 100 + y3 * 10 + y4,
|
||
|
month: m1 * 10 + m2,
|
||
|
day: d1 * 10 + d2,
|
||
|
};
|
||
|
|
||
|
if date.month < 1 || date.month > 12 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
if date.day < 1 || date.day > 31 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
|
||
|
Some(date)
|
||
|
};
|
||
|
|
||
|
// Next parse the "partial-time" if available
|
||
|
let next = chars.clone().next();
|
||
|
let partial_time = if full_date.is_some()
|
||
|
&& (next == Some('T') || next == Some('t') || next == Some(' '))
|
||
|
{
|
||
|
chars.next();
|
||
|
true
|
||
|
} else {
|
||
|
full_date.is_none()
|
||
|
};
|
||
|
|
||
|
let time = if partial_time {
|
||
|
let h1 = digit(&mut chars)?;
|
||
|
let h2 = digit(&mut chars)?;
|
||
|
match chars.next() {
|
||
|
Some(':') => {}
|
||
|
_ => return Err(DatetimeParseError { _private: () }),
|
||
|
}
|
||
|
let m1 = digit(&mut chars)?;
|
||
|
let m2 = digit(&mut chars)?;
|
||
|
match chars.next() {
|
||
|
Some(':') => {}
|
||
|
_ => return Err(DatetimeParseError { _private: () }),
|
||
|
}
|
||
|
let s1 = digit(&mut chars)?;
|
||
|
let s2 = digit(&mut chars)?;
|
||
|
|
||
|
let mut nanosecond = 0;
|
||
|
if chars.clone().next() == Some('.') {
|
||
|
chars.next();
|
||
|
let whole = chars.as_str();
|
||
|
|
||
|
let mut end = whole.len();
|
||
|
for (i, byte) in whole.bytes().enumerate() {
|
||
|
match byte {
|
||
|
b'0'..=b'9' => {
|
||
|
if i < 9 {
|
||
|
let p = 10_u32.pow(8 - i as u32);
|
||
|
nanosecond += p * u32::from(byte - b'0');
|
||
|
}
|
||
|
}
|
||
|
_ => {
|
||
|
end = i;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if end == 0 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
chars = whole[end..].chars();
|
||
|
}
|
||
|
|
||
|
let time = Time {
|
||
|
hour: h1 * 10 + h2,
|
||
|
minute: m1 * 10 + m2,
|
||
|
second: s1 * 10 + s2,
|
||
|
nanosecond,
|
||
|
};
|
||
|
|
||
|
if time.hour > 24 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
if time.minute > 59 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
if time.second > 59 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
if time.nanosecond > 999_999_999 {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
|
||
|
Some(time)
|
||
|
} else {
|
||
|
offset_allowed = false;
|
||
|
None
|
||
|
};
|
||
|
|
||
|
// And finally, parse the offset
|
||
|
let offset = if offset_allowed {
|
||
|
let next = chars.clone().next();
|
||
|
if next == Some('Z') || next == Some('z') {
|
||
|
chars.next();
|
||
|
Some(Offset::Z)
|
||
|
} else if next.is_none() {
|
||
|
None
|
||
|
} else {
|
||
|
let sign = match next {
|
||
|
Some('+') => 1,
|
||
|
Some('-') => -1,
|
||
|
_ => return Err(DatetimeParseError { _private: () }),
|
||
|
};
|
||
|
chars.next();
|
||
|
let h1 = digit(&mut chars)? as i8;
|
||
|
let h2 = digit(&mut chars)? as i8;
|
||
|
match chars.next() {
|
||
|
Some(':') => {}
|
||
|
_ => return Err(DatetimeParseError { _private: () }),
|
||
|
}
|
||
|
let m1 = digit(&mut chars)?;
|
||
|
let m2 = digit(&mut chars)?;
|
||
|
|
||
|
Some(Offset::Custom {
|
||
|
hours: sign * (h1 * 10 + h2),
|
||
|
minutes: m1 * 10 + m2,
|
||
|
})
|
||
|
}
|
||
|
} else {
|
||
|
None
|
||
|
};
|
||
|
|
||
|
// Return an error if we didn't hit eof, otherwise return our parsed
|
||
|
// date
|
||
|
if chars.next().is_some() {
|
||
|
return Err(DatetimeParseError { _private: () });
|
||
|
}
|
||
|
|
||
|
Ok(Datetime {
|
||
|
date: full_date,
|
||
|
time,
|
||
|
offset,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn digit(chars: &mut str::Chars<'_>) -> Result<u8, DatetimeParseError> {
|
||
|
match chars.next() {
|
||
|
Some(c) if '0' <= c && c <= '9' => Ok(c as u8 - b'0'),
|
||
|
_ => Err(DatetimeParseError { _private: () }),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl ser::Serialize for Datetime {
|
||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||
|
where
|
||
|
S: ser::Serializer,
|
||
|
{
|
||
|
use serde::ser::SerializeStruct;
|
||
|
|
||
|
let mut s = serializer.serialize_struct(NAME, 1)?;
|
||
|
s.serialize_field(FIELD, &self.to_string())?;
|
||
|
s.end()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl<'de> de::Deserialize<'de> for Datetime {
|
||
|
fn deserialize<D>(deserializer: D) -> Result<Datetime, D::Error>
|
||
|
where
|
||
|
D: de::Deserializer<'de>,
|
||
|
{
|
||
|
struct DatetimeVisitor;
|
||
|
|
||
|
impl<'de> de::Visitor<'de> for DatetimeVisitor {
|
||
|
type Value = Datetime;
|
||
|
|
||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
formatter.write_str("a TOML datetime")
|
||
|
}
|
||
|
|
||
|
fn visit_map<V>(self, mut visitor: V) -> Result<Datetime, V::Error>
|
||
|
where
|
||
|
V: de::MapAccess<'de>,
|
||
|
{
|
||
|
let value = visitor.next_key::<DatetimeKey>()?;
|
||
|
if value.is_none() {
|
||
|
return Err(de::Error::custom("datetime key not found"));
|
||
|
}
|
||
|
let v: DatetimeFromString = visitor.next_value()?;
|
||
|
Ok(v.value)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static FIELDS: [&str; 1] = [FIELD];
|
||
|
deserializer.deserialize_struct(NAME, &FIELDS, DatetimeVisitor)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct DatetimeKey;
|
||
|
|
||
|
impl<'de> de::Deserialize<'de> for DatetimeKey {
|
||
|
fn deserialize<D>(deserializer: D) -> Result<DatetimeKey, D::Error>
|
||
|
where
|
||
|
D: de::Deserializer<'de>,
|
||
|
{
|
||
|
struct FieldVisitor;
|
||
|
|
||
|
impl<'de> de::Visitor<'de> for FieldVisitor {
|
||
|
type Value = ();
|
||
|
|
||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
formatter.write_str("a valid datetime field")
|
||
|
}
|
||
|
|
||
|
fn visit_str<E>(self, s: &str) -> Result<(), E>
|
||
|
where
|
||
|
E: de::Error,
|
||
|
{
|
||
|
if s == FIELD {
|
||
|
Ok(())
|
||
|
} else {
|
||
|
Err(de::Error::custom("expected field with custom name"))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
deserializer.deserialize_identifier(FieldVisitor)?;
|
||
|
Ok(DatetimeKey)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub struct DatetimeFromString {
|
||
|
pub value: Datetime,
|
||
|
}
|
||
|
|
||
|
impl<'de> de::Deserialize<'de> for DatetimeFromString {
|
||
|
fn deserialize<D>(deserializer: D) -> Result<DatetimeFromString, D::Error>
|
||
|
where
|
||
|
D: de::Deserializer<'de>,
|
||
|
{
|
||
|
struct Visitor;
|
||
|
|
||
|
impl<'de> de::Visitor<'de> for Visitor {
|
||
|
type Value = DatetimeFromString;
|
||
|
|
||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
formatter.write_str("string containing a datetime")
|
||
|
}
|
||
|
|
||
|
fn visit_str<E>(self, s: &str) -> Result<DatetimeFromString, E>
|
||
|
where
|
||
|
E: de::Error,
|
||
|
{
|
||
|
match s.parse() {
|
||
|
Ok(date) => Ok(DatetimeFromString { value: date }),
|
||
|
Err(e) => Err(de::Error::custom(e)),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
deserializer.deserialize_str(Visitor)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl fmt::Display for DatetimeParseError {
|
||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
"failed to parse datetime".fmt(f)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl error::Error for DatetimeParseError {}
|