Skip to content

Commit

Permalink
Add IXDTF annotation handler and update critical flag handling (unico…
Browse files Browse the repository at this point in the history
  • Loading branch information
nekevss authored Apr 2, 2024
1 parent 612fb6f commit 8631983
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 100 deletions.
14 changes: 3 additions & 11 deletions utils/ixdtf/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions utils/ixdtf/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ pub enum ParserError {
// Duplicate calendar with critical.
#[displaydoc("Duplicate calendars cannot be provided when one is critical.")]
CriticalDuplicateCalendar,
#[displaydoc("Unrecognized annoation is marked as critical.")]
UnrecognizedCritical,

// Time Zone Errors
#[displaydoc("Invalid time zone leading character.")]
Expand Down
14 changes: 3 additions & 11 deletions utils/ixdtf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,13 @@
//! be provided to the user to handle the unknown key's critical flag as they see fit.
//!
//! ```rust
//! use ixdtf::parsers::IxdtfParser;
//! use ixdtf::{parsers::IxdtfParser, ParserError};
//!
//! let example_one = "2024-03-02T08:48:00-05:00[u-ca=iso8601][!answer-to-universe=fortytwo]";
//!
//! let mut ixdtf = IxdtfParser::new(example_one);
//!
//! let result = ixdtf.parse().unwrap();
//!
//! let annotation = &result.annotations[0];
//! let result = IxdtfParser::new(example_one).parse();
//!
//! // While an unknown annotation should not be critical, it is up to the user
//! // to act on that error.
//! assert!(annotation.critical);
//! assert_eq!(annotation.key, "answer-to-universe");
//! assert_eq!(annotation.value, "fortytwo");
//! assert_eq!(result, Err(ParserError::UnrecognizedCritical));
//! ```
//!
//! (4) belongs to group (b) and shows an ambiguous Time Zone caused by a misalignment
Expand Down
60 changes: 35 additions & 25 deletions utils/ixdtf/src/parsers/annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,66 +18,76 @@ use crate::{
ParserError, ParserResult,
};

use alloc::vec::Vec;

/// Strictly a parsing intermediary for the checking the common annotation backing.
pub(crate) struct AnnotationSet<'a> {
pub(crate) tz: Option<TimeZoneAnnotation<'a>>,
pub(crate) calendar: Option<&'a str>,
pub(crate) annotations: Vec<Annotation<'a>>,
}

/// Parse a `TimeZoneAnnotation` `Annotations` set
pub(crate) fn parse_annotation_set<'a>(cursor: &mut Cursor<'a>) -> ParserResult<AnnotationSet<'a>> {
pub(crate) fn parse_annotation_set<'a>(
cursor: &mut Cursor<'a>,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<AnnotationSet<'a>> {
// Parse the first annotation.
let tz_annotation = timezone::parse_ambiguous_tz_annotation(cursor)?;

// Parse any `Annotations`
let annotations = cursor.check_or(false, is_annotation_open);

if annotations {
let annotations = parse_annotations(cursor)?;
let calendar = parse_annotations(cursor, handler)?;
return Ok(AnnotationSet {
tz: tz_annotation,
calendar: annotations.0,
annotations: annotations.1,
calendar,
});
}

Ok(AnnotationSet {
tz: tz_annotation,
calendar: None,
annotations: Vec::default(),
})
}

/// Parse any number of `KeyValueAnnotation`s
pub(crate) fn parse_annotations<'a>(
cursor: &mut Cursor<'a>,
) -> ParserResult<(Option<&'a str>, Vec<Annotation<'a>>)> {
let mut annotations = Vec::default();
let mut calendar = None;
let mut calendar_crit = false;
mut handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<Option<&'a str>> {
let mut calendar: Option<Annotation<'a>> = None;

while cursor.check_or(false, is_annotation_open) {
let kv = parse_kv_annotation(cursor)?;

if kv.key == "u-ca" {
if calendar.is_none() {
calendar = Some(kv.value);
calendar_crit = kv.critical;
continue;
let annotation = handler(parse_kv_annotation(cursor)?);

match annotation {
// Check if the key is the registered key "u-ca".
Some(kv) if kv.key == "u-ca" => {
// Check the calendar
match calendar {
Some(calendar)
// if calendars do not match and one of them is critical
if calendar.value != kv.value && (calendar.critical || kv.critical) =>
{
return Err(ParserError::CriticalDuplicateCalendar)
}
// If there is not yet a calendar, save it.
None => {
calendar = Some(kv);
}
_ => {}
}
}

if calendar_crit || kv.critical {
return Err(ParserError::CriticalDuplicateCalendar);
Some(unknown_kv) => {
// Throw an error on any unrecognized annotations that are marked as critical.
if unknown_kv.critical {
return Err(ParserError::UnrecognizedCritical);
}
}
None => {}
}

annotations.push(kv);
}

Ok((calendar, annotations))
Ok(calendar.map(|a| a.value))
}

/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair.
Expand Down
19 changes: 7 additions & 12 deletions utils/ixdtf/src/parsers/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ use crate::{
ParserError, ParserResult,
};

use alloc::vec::Vec;

use super::records::UTCOffsetRecord;
use super::records::{Annotation, UTCOffsetRecord};

#[derive(Debug, Default, Clone)]
/// A `DateTime` Parse Node that contains the date, time, and offset info.
Expand All @@ -43,6 +41,7 @@ pub(crate) struct DateTimeRecord {
/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
pub(crate) fn parse_annotated_date_time<'a>(
cursor: &mut Cursor<'a>,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<IxdtfParseRecord<'a>> {
let date_time = parse_date_time(cursor)?;

Expand All @@ -57,11 +56,10 @@ pub(crate) fn parse_annotated_date_time<'a>(
offset: date_time.time_zone,
tz: None,
calendar: None,
annotations: Vec::default(),
});
}

let annotation_set = annotations::parse_annotation_set(cursor)?;
let annotation_set = annotations::parse_annotation_set(cursor, handler)?;

cursor.close()?;

Expand All @@ -71,13 +69,13 @@ pub(crate) fn parse_annotated_date_time<'a>(
offset: date_time.time_zone,
tz: annotation_set.tz,
calendar: annotation_set.calendar,
annotations: annotation_set.annotations,
})
}

/// Parses an AnnotatedMonthDay.
pub(crate) fn parse_annotated_month_day<'a>(
cursor: &mut Cursor<'a>,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<IxdtfParseRecord<'a>> {
let date = parse_month_day(cursor)?;

Expand All @@ -90,25 +88,24 @@ pub(crate) fn parse_annotated_month_day<'a>(
offset: None,
tz: None,
calendar: None,
annotations: Vec::default(),
});
}

let annotation_set = annotations::parse_annotation_set(cursor)?;
let annotation_set = annotations::parse_annotation_set(cursor, handler)?;

Ok(IxdtfParseRecord {
date: Some(date),
time: None,
offset: None,
tz: annotation_set.tz,
calendar: annotation_set.calendar,
annotations: annotation_set.annotations,
})
}

/// Parse an annotated YearMonth
pub(crate) fn parse_annotated_year_month<'a>(
cursor: &mut Cursor<'a>,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<IxdtfParseRecord<'a>> {
let year = parse_date_year(cursor)?;
cursor.advance_if(cursor.check_or(false, is_hyphen));
Expand All @@ -129,19 +126,17 @@ pub(crate) fn parse_annotated_year_month<'a>(
offset: None,
tz: None,
calendar: None,
annotations: Vec::default(),
});
}

let annotation_set = annotations::parse_annotation_set(cursor)?;
let annotation_set = annotations::parse_annotation_set(cursor, handler)?;

Ok(IxdtfParseRecord {
date: Some(date),
time: None,
offset: None,
tz: annotation_set.tz,
calendar: annotation_set.calendar,
annotations: annotation_set.annotations,
})
}

Expand Down
84 changes: 78 additions & 6 deletions utils/ixdtf/src/parsers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

use crate::{ParserError, ParserResult};

extern crate alloc;

#[cfg(feature = "duration")]
use records::DurationParseRecord;
use records::IxdtfParseRecord;

use self::records::Annotation;

pub mod records;

mod annotations;
Expand Down Expand Up @@ -62,22 +62,94 @@ impl<'a> IxdtfParser<'a> {
///
/// [temporal-dt]: https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString
pub fn parse(&mut self) -> ParserResult<IxdtfParseRecord<'a>> {
datetime::parse_annotated_date_time(&mut self.cursor)
self.parse_with_annotation_handler(Some)
}

/// Parses the source as an annotated DateTime string with an Annotation handler.
///
/// # Annotation Handling
///
/// The annotation handler provides a parsed annotation to the callback and expects a return
/// of an annotation or None. `ixdtf` performs baseline annotation checks once the handler
/// returns. Returning None will ignore the standard checks for that annotation.
///
/// Unless the user's application has a specific reason to bypass action on an annotation, such
/// as, not throwing an error on an unknown key's criticality or superceding a calendar based on
/// it's critical flag, it is recommended to return the annotation value.
pub fn parse_with_annotation_handler(
&mut self,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<IxdtfParseRecord<'a>> {
datetime::parse_annotated_date_time(&mut self.cursor, handler)
}

/// Parses the source as an annotated YearMonth string.
pub fn parse_year_month(&mut self) -> ParserResult<IxdtfParseRecord<'a>> {
datetime::parse_annotated_year_month(&mut self.cursor)
self.parse_year_month_with_annotation_handler(Some)
}

/// Parses the source as an annotated YearMonth string with an Annotation handler.
///
/// # Annotation Handling
///
/// The annotation handler provides a parsed annotation to the callback and expects a return
/// of an annotation or None. `ixdtf` performs baseline annotation checks once the handler
/// returns. Returning None will ignore the standard checks for that annotation.
///
/// Unless the user's application has a specific use case to bypass action on an annotation, such
/// as, not throwing an error on an unknown key's criticality or superceding a calendar based on
/// it's critical flag, it is recommended to return the annotation value.
pub fn parse_year_month_with_annotation_handler(
&mut self,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<IxdtfParseRecord<'a>> {
datetime::parse_annotated_year_month(&mut self.cursor, handler)
}

/// Parses the source as an annotated MonthDay string.
pub fn parse_month_day(&mut self) -> ParserResult<IxdtfParseRecord<'a>> {
datetime::parse_annotated_month_day(&mut self.cursor)
self.parse_month_day_with_annotation_handler(Some)
}

/// Parses the source as an annotated MonthDay string with an Annotation handler.
///
/// # Annotation Handling
///
/// The annotation handler provides a parsed annotation to the callback and expects a return
/// of an annotation or None. `ixdtf` performs baseline annotation checks once the handler
/// returns. Returning None will ignore the standard checks for that annotation.
///
/// Unless the user's application has a specific reason to bypass action on an annotation, such
/// as, not throwing an error on an unknown key's criticality or superceding a calendar based on
/// it's critical flag, it is recommended to return the annotation value.
pub fn parse_month_day_with_annotation_handler(
&mut self,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<IxdtfParseRecord<'a>> {
datetime::parse_annotated_month_day(&mut self.cursor, handler)
}

/// Parses the source as an annotated Time string.
pub fn parse_time(&mut self) -> ParserResult<IxdtfParseRecord<'a>> {
time::parse_annotated_time_record(&mut self.cursor)
self.parse_time_with_annotation_handler(Some)
}

/// Parses the source as an annotated Time string with an Annotation handler.
///
/// # Annotation Handling
///
/// The annotation handler provides a parsed annotation to the callback and expects a return
/// of an annotation or None. `ixdtf` performs baseline annotation checks once the handler
/// returns. Returning None will ignore the standard checks for that annotation.
///
/// Unless the user's application has a specific reason to bypass action on an annotation, such
/// as, not throwing an error on an unknown key's criticality or superceding a calendar based on
/// it's critical flag, it is recommended to return the annotation value.
pub fn parse_time_with_annotation_handler(
&mut self,
handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>,
) -> ParserResult<IxdtfParseRecord<'a>> {
time::parse_annotated_time_record(&mut self.cursor, handler)
}
}

Expand Down
4 changes: 0 additions & 4 deletions utils/ixdtf/src/parsers/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

//! The records that `ixdtf`'s contain the resulting values of parsing.

use alloc::vec::Vec;

/// An `IxdtfParseRecord` is an intermediary record returned by `IxdtfParser`.
#[non_exhaustive]
#[derive(Default, Debug, PartialEq)]
Expand All @@ -20,8 +18,6 @@ pub struct IxdtfParseRecord<'a> {
pub tz: Option<TimeZoneAnnotation<'a>>,
/// The parsed calendar value.
pub calendar: Option<&'a str>,
/// A collection of annotations provided on an IXDTF string.
pub annotations: Vec<Annotation<'a>>,
}

#[non_exhaustive]
Expand Down
Loading

0 comments on commit 8631983

Please sign in to comment.