e256931e9b
This commit completely rewrites this crate from the ground up, supporting serde at the lowest levels as I believe serde support was intended to do. This is a major change from the previous versions of this crate, with a summary of changes being: * Serialization directly to TOML is now supported without going through a `Value` first. * Deserialization directly from TOML is now supported without going through a `Value`. Note that due to the TOML format some values still are buffered in intermediate memory, but overall this should be at a minimum now. * The API of `Value` was overhauled to match the API of `serde_json::Value`. The changes here were to: * Add `is_*` accessors * Add `get` and `get_mut` for one-field lookups. * Implement panicking lookups through `Index` The old `index` methods are now gone in favor of `get` and `Index` implementations. * A `Datetime` type has been added to represent a TOML datetime in a first-class fashion. Currently this type provides no accessors other than a `Display` implementation, but the idea is that this will grow support over time for decomposing the date. * Support for the `rustc-serialize` crate has been dropped, that'll stay on the 0.2 and 0.1 release trains. * This crate no longer supports the detection of unused fields, for that though you can use the `serde_ignored` crate on crates.io
425 lines
12 KiB
Rust
425 lines
12 KiB
Rust
use std::fmt;
|
|
use std::str::{self, FromStr};
|
|
use std::error;
|
|
|
|
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.
|
|
#[derive(PartialEq, Clone)]
|
|
pub struct Datetime {
|
|
date: Option<Date>,
|
|
time: Option<Time>,
|
|
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 thiese 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 SERDE_STRUCT_FIELD_NAME: &'static str = "$__toml_private_datetime";
|
|
pub const SERDE_STRUCT_NAME: &'static str = "$__toml_private_Datetime";
|
|
|
|
#[derive(PartialEq, Clone)]
|
|
struct Date {
|
|
year: u16,
|
|
month: u8,
|
|
day: u8,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone)]
|
|
struct Time {
|
|
hour: u8,
|
|
minute: u8,
|
|
second: u8,
|
|
secfract: Option<f64>,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone)]
|
|
enum Offset {
|
|
Z,
|
|
Custom { hours: i8, 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 let Some(i) = self.secfract {
|
|
let s = format!("{}", i);
|
|
write!(f, "{}", s.trim_left_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 = digit(&mut chars)? as u16;
|
|
let y2 = digit(&mut chars)? as u16;
|
|
let y3 = digit(&mut chars)? as u16;
|
|
let y4 = digit(&mut chars)? as u16;
|
|
|
|
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 partial_time = if full_date.is_some() &&
|
|
chars.clone().next() == Some('T') {
|
|
chars.next();
|
|
true
|
|
} else if full_date.is_none() {
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
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 secfract = if chars.clone().next() == Some('.') {
|
|
chars.next();
|
|
let mut first = true;
|
|
let whole = chars.as_str();
|
|
let mut end = whole.len();
|
|
for (i, c) in whole.char_indices() {
|
|
match c {
|
|
'0' ... '9' => {}
|
|
_ => {
|
|
end = i;
|
|
break
|
|
}
|
|
}
|
|
first = false;
|
|
}
|
|
if first {
|
|
return Err(DatetimeParseError { _private: () })
|
|
}
|
|
chars = whole[end..].chars();
|
|
match format!("0.{}", &whole[..end]).parse() {
|
|
Ok(f) => Some(f),
|
|
Err(_) => return Err(DatetimeParseError { _private: () }),
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let time = Time {
|
|
hour: h1 * 10 + h2,
|
|
minute: m1 * 10 + m2,
|
|
second: s1 * 10 + s2,
|
|
secfract: secfract,
|
|
};
|
|
|
|
if time.hour > 24 {
|
|
return Err(DatetimeParseError { _private: () })
|
|
}
|
|
if time.minute > 59 {
|
|
return Err(DatetimeParseError { _private: () })
|
|
}
|
|
if time.second > 60 {
|
|
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') {
|
|
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: time,
|
|
offset: offset,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn digit(chars: &mut str::Chars) -> Result<u8, DatetimeParseError> {
|
|
match chars.next() {
|
|
Some(c) if '0' <= c && c <= '9' => Ok(c as u8 - '0' as u8),
|
|
_ => 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(SERDE_STRUCT_NAME, 1)?;
|
|
s.serialize_field(SERDE_STRUCT_FIELD_NAME, &self.to_string())?;
|
|
s.end()
|
|
}
|
|
}
|
|
|
|
impl de::Deserialize for Datetime {
|
|
fn deserialize<D>(deserializer: D) -> Result<Datetime, D::Error>
|
|
where D: de::Deserializer
|
|
{
|
|
struct DatetimeVisitor;
|
|
|
|
impl de::Visitor 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::MapVisitor
|
|
{
|
|
let value = visitor.visit_key::<DatetimeKey>()?;
|
|
if value.is_none() {
|
|
return Err(de::Error::custom("datetime key not found"))
|
|
}
|
|
let v: DatetimeFromString = visitor.visit_value()?;
|
|
Ok(v.value)
|
|
|
|
}
|
|
}
|
|
|
|
static FIELDS: [&'static str; 1] = [SERDE_STRUCT_FIELD_NAME];
|
|
deserializer.deserialize_struct(SERDE_STRUCT_NAME,
|
|
&FIELDS,
|
|
DatetimeVisitor)
|
|
}
|
|
}
|
|
|
|
struct DatetimeKey;
|
|
|
|
impl de::Deserialize for DatetimeKey {
|
|
fn deserialize<D>(deserializer: D) -> Result<DatetimeKey, D::Error>
|
|
where D: de::Deserializer
|
|
{
|
|
struct FieldVisitor;
|
|
|
|
impl de::Visitor 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 == SERDE_STRUCT_FIELD_NAME {
|
|
Ok(())
|
|
} else {
|
|
Err(de::Error::custom("expected field with custom name"))
|
|
}
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_struct_field(FieldVisitor)?;
|
|
Ok(DatetimeKey)
|
|
}
|
|
}
|
|
|
|
pub struct DatetimeFromString {
|
|
pub value: Datetime,
|
|
}
|
|
|
|
impl de::Deserialize for DatetimeFromString {
|
|
fn deserialize<D>(deserializer: D) -> Result<DatetimeFromString, D::Error>
|
|
where D: de::Deserializer
|
|
{
|
|
struct Visitor;
|
|
|
|
impl de::Visitor 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 {
|
|
fn description(&self) -> &str {
|
|
"failed to parse datetime"
|
|
}
|
|
}
|