Add support for yaml datetimes (#2208)
* Add support for yaml datetimes * cargo fmt * Wire things up; Add more tests * cargo fmt
This commit is contained in:
parent
9c1b004b13
commit
ff9622f132
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4632,6 +4632,7 @@ dependencies = [
|
|||||||
"libs",
|
"libs",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -7,7 +7,7 @@ use time::macros::{format_description, time};
|
|||||||
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
||||||
|
|
||||||
use errors::{bail, Result};
|
use errors::{bail, Result};
|
||||||
use utils::de::{fix_toml_dates, from_toml_datetime};
|
use utils::de::{fix_toml_dates, from_unknown_datetime};
|
||||||
|
|
||||||
use crate::front_matter::split::RawFrontMatter;
|
use crate::front_matter::split::RawFrontMatter;
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ pub struct PageFrontMatter {
|
|||||||
/// Description in <meta> that appears when linked, e.g. on twitter
|
/// Description in <meta> that appears when linked, e.g. on twitter
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// Updated date
|
/// Updated date
|
||||||
#[serde(default, deserialize_with = "from_toml_datetime")]
|
#[serde(default, deserialize_with = "from_unknown_datetime")]
|
||||||
pub updated: Option<String>,
|
pub updated: Option<String>,
|
||||||
/// Datetime content was last updated
|
/// Datetime content was last updated
|
||||||
#[serde(default, skip_deserializing)]
|
#[serde(default, skip_deserializing)]
|
||||||
@ -29,7 +29,7 @@ pub struct PageFrontMatter {
|
|||||||
#[serde(default, skip_deserializing)]
|
#[serde(default, skip_deserializing)]
|
||||||
pub updated_datetime_tuple: Option<(i32, u8, u8)>,
|
pub updated_datetime_tuple: Option<(i32, u8, u8)>,
|
||||||
/// Date if we want to order pages (ie blog post)
|
/// Date if we want to order pages (ie blog post)
|
||||||
#[serde(default, deserialize_with = "from_toml_datetime")]
|
#[serde(default, deserialize_with = "from_unknown_datetime")]
|
||||||
pub date: Option<String>,
|
pub date: Option<String>,
|
||||||
/// Datetime content was created
|
/// Datetime content was created
|
||||||
#[serde(default, skip_deserializing)]
|
#[serde(default, skip_deserializing)]
|
||||||
@ -129,6 +129,7 @@ impl PageFrontMatter {
|
|||||||
/// Converts the TOML datetime to a time::OffsetDateTime
|
/// Converts the TOML datetime to a time::OffsetDateTime
|
||||||
/// Also grabs the year/month/day tuple that will be used in serialization
|
/// Also grabs the year/month/day tuple that will be used in serialization
|
||||||
pub fn date_to_datetime(&mut self) {
|
pub fn date_to_datetime(&mut self) {
|
||||||
|
println!("{:?}", self.date);
|
||||||
self.datetime = self.date.as_ref().map(|s| s.as_ref()).and_then(parse_datetime);
|
self.datetime = self.date.as_ref().map(|s| s.as_ref()).and_then(parse_datetime);
|
||||||
self.datetime_tuple = self.datetime.map(|dt| (dt.year(), dt.month().into(), dt.day()));
|
self.datetime_tuple = self.datetime.map(|dt| (dt.year(), dt.month().into(), dt.day()));
|
||||||
|
|
||||||
@ -333,6 +334,36 @@ date: 2002-10-02T15:00:00.123456Z
|
|||||||
assert_eq!(res.datetime.unwrap(), datetime!(2002 - 10 - 02 15:00:00.123456 UTC));
|
assert_eq!(res.datetime.unwrap(), datetime!(2002 - 10 - 02 15:00:00.123456 UTC));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(&RawFrontMatter::Yaml(r#"
|
||||||
|
title: Hello
|
||||||
|
description: hey there
|
||||||
|
date: 2001-12-15T02:59:43.1Z
|
||||||
|
"#); "canonical")]
|
||||||
|
#[test_case(&RawFrontMatter::Yaml(r#"
|
||||||
|
title: Hello
|
||||||
|
description: hey there
|
||||||
|
date: 2001-12-14t21:59:43.10-05:00
|
||||||
|
"#); "iso8601")]
|
||||||
|
#[test_case(&RawFrontMatter::Yaml(r#"
|
||||||
|
title: Hello
|
||||||
|
description: hey there
|
||||||
|
date: 2001-12-14 21:59:43.10 -5
|
||||||
|
"#); "space separated")]
|
||||||
|
#[test_case(&RawFrontMatter::Yaml(r#"
|
||||||
|
title: Hello
|
||||||
|
description: hey there
|
||||||
|
date: 2001-12-15 2:59:43.10
|
||||||
|
"#); "no time zone")]
|
||||||
|
#[test_case(&RawFrontMatter::Yaml(r#"
|
||||||
|
title: Hello
|
||||||
|
description: hey there
|
||||||
|
date: 2001-12-15
|
||||||
|
"#); "date only")]
|
||||||
|
fn can_parse_yaml_dates(content: &RawFrontMatter) {
|
||||||
|
let res = PageFrontMatter::parse(content).unwrap();
|
||||||
|
assert!(res.datetime.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test_case(&RawFrontMatter::Toml(r#"
|
#[test_case(&RawFrontMatter::Toml(r#"
|
||||||
title = "Hello"
|
title = "Hello"
|
||||||
description = "hey there"
|
description = "hey there"
|
||||||
|
@ -13,3 +13,4 @@ libs = { path = "../libs" }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
time = { version = "0.3", features = ["macros"] }
|
||||||
|
@ -1,12 +1,83 @@
|
|||||||
|
use core::convert::TryFrom;
|
||||||
|
use errors::{anyhow, Result};
|
||||||
|
use libs::regex::Regex;
|
||||||
use libs::tera::{Map, Value};
|
use libs::tera::{Map, Value};
|
||||||
|
use libs::time;
|
||||||
|
use libs::time::format_description::well_known::Rfc3339;
|
||||||
use libs::toml;
|
use libs::toml;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
pub fn parse_yaml_datetime(date_string: &str) -> Result<time::OffsetDateTime> {
|
||||||
|
// See https://github.com/getzola/zola/issues/2071#issuecomment-1530610650
|
||||||
|
let re = Regex::new(r#"^"?([0-9]{4})-([0-9][0-9]?)-([0-9][0-9]?)([Tt]|[ \t]+)([0-9][0-9]?):([0-9]{2}):([0-9]{2})\.([0-9]*)?Z?([ \t]([-+][0-9][0-9]?)(:([0-9][0-9]?))?Z?|([-+][0-9]{2})?:([0-9]{2})?)?|([0-9]{4})-([0-9]{2})-([0-9]{2})"?$"#).unwrap();
|
||||||
|
let captures = if let Some(captures_) = re.captures(date_string) {
|
||||||
|
Ok(captures_)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Error parsing YAML datetime"))
|
||||||
|
}?;
|
||||||
|
let year =
|
||||||
|
if let Some(cap) = captures.get(1) { cap } else { captures.get(15).unwrap() }.as_str();
|
||||||
|
let month =
|
||||||
|
if let Some(cap) = captures.get(2) { cap } else { captures.get(16).unwrap() }.as_str();
|
||||||
|
let day =
|
||||||
|
if let Some(cap) = captures.get(3) { cap } else { captures.get(17).unwrap() }.as_str();
|
||||||
|
let hours = if let Some(hours_) = captures.get(5) { hours_.as_str() } else { "0" };
|
||||||
|
let minutes = if let Some(minutes_) = captures.get(6) { minutes_.as_str() } else { "0" };
|
||||||
|
let seconds = if let Some(seconds_) = captures.get(7) { seconds_.as_str() } else { "0" };
|
||||||
|
let fractional_seconds_raw =
|
||||||
|
if let Some(fractionals) = captures.get(8) { fractionals.as_str() } else { "" };
|
||||||
|
let fractional_seconds_intermediate = fractional_seconds_raw.trim_end_matches("0");
|
||||||
|
//
|
||||||
|
// Prepare for eventual conversion into nanoseconds
|
||||||
|
let fractional_seconds = if fractional_seconds_intermediate.len() > 0
|
||||||
|
&& fractional_seconds_intermediate.len() <= 9
|
||||||
|
{
|
||||||
|
fractional_seconds_intermediate
|
||||||
|
} else {
|
||||||
|
"0"
|
||||||
|
};
|
||||||
|
let maybe_timezone_hour_1 = captures.get(10);
|
||||||
|
let maybe_timezone_minute_1 = captures.get(12);
|
||||||
|
let maybe_timezone_hour_2 = captures.get(13);
|
||||||
|
let maybe_timezone_minute_2 = captures.get(14);
|
||||||
|
let maybe_timezone_hour;
|
||||||
|
let maybe_timezone_minute;
|
||||||
|
if maybe_timezone_hour_2.is_some() {
|
||||||
|
maybe_timezone_hour = maybe_timezone_hour_2;
|
||||||
|
maybe_timezone_minute = maybe_timezone_minute_2;
|
||||||
|
} else {
|
||||||
|
maybe_timezone_hour = maybe_timezone_hour_1;
|
||||||
|
maybe_timezone_minute = maybe_timezone_minute_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset_datetime = time::OffsetDateTime::UNIX_EPOCH;
|
||||||
|
|
||||||
|
if let Some(hour) = maybe_timezone_hour {
|
||||||
|
let minute_str =
|
||||||
|
if let Some(minute_) = maybe_timezone_minute { minute_.as_str() } else { "0" };
|
||||||
|
offset_datetime = offset_datetime.to_offset(time::UtcOffset::from_hms(
|
||||||
|
hour.as_str().parse()?,
|
||||||
|
minute_str.parse()?,
|
||||||
|
0,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free parse unwraps since we know everything is a digit courtesy of prior regex.
|
||||||
|
Ok(offset_datetime
|
||||||
|
.replace_year(year.parse().unwrap())?
|
||||||
|
.replace_month(time::Month::try_from(month.parse::<u8>().unwrap())?)?
|
||||||
|
.replace_day(day.parse().unwrap())?
|
||||||
|
.replace_hour(hours.parse().unwrap())?
|
||||||
|
.replace_minute(minutes.parse().unwrap())?
|
||||||
|
.replace_second(seconds.parse().unwrap())?
|
||||||
|
.replace_nanosecond(fractional_seconds.parse::<u32>().unwrap() * 100_000_000)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Used as an attribute when we want to convert from TOML to a string date
|
/// Used as an attribute when we want to convert from TOML to a string date
|
||||||
/// If a TOML datetime isn't present, it will accept a string and push it through
|
/// If a TOML datetime isn't present, it will accept a string and push it through
|
||||||
/// TOML's date time parser to ensure only valid dates are accepted.
|
/// TOML's date time parser to ensure only valid dates are accepted.
|
||||||
/// Inspired by this proposal: <https://github.com/alexcrichton/toml-rs/issues/269>
|
/// Inspired by this proposal: <https://github.com/alexcrichton/toml-rs/issues/269>
|
||||||
pub fn from_toml_datetime<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
pub fn from_unknown_datetime<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
@ -22,10 +93,19 @@ where
|
|||||||
|
|
||||||
match MaybeDatetime::deserialize(deserializer)? {
|
match MaybeDatetime::deserialize(deserializer)? {
|
||||||
MaybeDatetime::Datetime(d) => Ok(Some(d.to_string())),
|
MaybeDatetime::Datetime(d) => Ok(Some(d.to_string())),
|
||||||
MaybeDatetime::String(s) => match toml::value::Datetime::from_str(&s) {
|
MaybeDatetime::String(s) => {
|
||||||
Ok(d) => Ok(Some(d.to_string())),
|
if let Ok(d) = toml::value::Datetime::from_str(&s) {
|
||||||
Err(e) => Err(D::Error::custom(e)),
|
Ok(Some(d.to_string()))
|
||||||
},
|
} else if let Ok(d) = parse_yaml_datetime(&s) {
|
||||||
|
// Ensure that the resulting string is easily reparseable down the line.
|
||||||
|
// In content::front_matter::page.rs where these strings are currently used,
|
||||||
|
// Rfc3339 works with the explicit demands in that code but not always with the result of
|
||||||
|
// _to_string.
|
||||||
|
Ok(Some(d.format(&Rfc3339).unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(D::Error::custom("Unable to parse datetime"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,3 +160,62 @@ pub fn fix_toml_dates(table: Map<String, Value>) -> Value {
|
|||||||
|
|
||||||
Value::Object(new)
|
Value::Object(new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::parse_yaml_datetime;
|
||||||
|
use time::macros::datetime;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yaml_spec_examples_pass() {
|
||||||
|
let canonical = "2001-12-15T02:59:43.1Z";
|
||||||
|
let valid_iso8601 = "2001-12-14t21:59:43.10-05:00";
|
||||||
|
let space_separated = "2001-12-14 21:59:43.10 -5";
|
||||||
|
let no_time_zone = "2001-12-15 2:59:43.10";
|
||||||
|
let date = "2002-12-14";
|
||||||
|
assert_eq!(parse_yaml_datetime(canonical).unwrap(), datetime!(2001-12-15 2:59:43.1 +0));
|
||||||
|
assert_eq!(
|
||||||
|
parse_yaml_datetime(valid_iso8601).unwrap(),
|
||||||
|
datetime!(2001-12-14 21:59:43.1 -5)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_yaml_datetime(space_separated).unwrap(),
|
||||||
|
datetime!(2001-12-14 21:59:43.1 -5)
|
||||||
|
);
|
||||||
|
assert_eq!(parse_yaml_datetime(no_time_zone).unwrap(), datetime!(2001-12-15 2:59:43.1 +0));
|
||||||
|
assert_eq!(parse_yaml_datetime(date).unwrap(), datetime!(2002-12-14 0:00:00 +0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yaml_spec_invalid_dates_fail() {
|
||||||
|
let invalid_month = "2001-13-15";
|
||||||
|
assert!(parse_yaml_datetime(invalid_month).is_err());
|
||||||
|
|
||||||
|
let invalid_month = "2001-13-15T02:59:43.1Z";
|
||||||
|
assert!(parse_yaml_datetime(invalid_month).is_err());
|
||||||
|
|
||||||
|
let no_digits_in_year = "xxxx-12-15";
|
||||||
|
assert!(parse_yaml_datetime(no_digits_in_year).is_err());
|
||||||
|
|
||||||
|
let no_digits_in_year = "xxxx-12-15T02:59:43.1Z";
|
||||||
|
assert!(parse_yaml_datetime(no_digits_in_year).is_err());
|
||||||
|
|
||||||
|
let no_digits_in_month = "2001-xx-15";
|
||||||
|
assert!(parse_yaml_datetime(no_digits_in_month).is_err());
|
||||||
|
|
||||||
|
let no_digits_in_month = "2001-xx-15T02:59:43.1Z";
|
||||||
|
assert!(parse_yaml_datetime(no_digits_in_month).is_err());
|
||||||
|
|
||||||
|
let no_digits_in_day = "2001-12-xx";
|
||||||
|
assert!(parse_yaml_datetime(no_digits_in_day).is_err());
|
||||||
|
|
||||||
|
let no_digits_in_day = "2001-12-xx:59:43.1Z";
|
||||||
|
assert!(parse_yaml_datetime(no_digits_in_day).is_err());
|
||||||
|
|
||||||
|
let unparseable_time = "2001-12-15:69:43.1Z";
|
||||||
|
assert!(parse_yaml_datetime(unparseable_time).is_err());
|
||||||
|
|
||||||
|
let unparseable_time = "2001-12-15:59:4x.1Z";
|
||||||
|
assert!(parse_yaml_datetime(unparseable_time).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user