diff --git a/Cargo.toml b/Cargo.toml index 2d8fae9..f69f6d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ derive_builder = "0.7.2" chrono = "0.4.9" strum = { version = "0.16.0", features = ["derive"] } derive_more = "0.15.0" +hex = "0.4.0" [dev-dependencies] clap = "2" diff --git a/src/error.rs b/src/error.rs index eaf3645..c64e031 100644 --- a/src/error.rs +++ b/src/error.rs @@ -209,3 +209,9 @@ impl From<::core::convert::Infallible> for Error { Self::custom("An Infallible error has been returned! (this should never happen!)") } } + +impl From<::hex::FromHexError> for Error { + fn from(value: ::hex::FromHexError) -> Self { + Self::custom(value) // TODO! + } +} diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 12f15db..e984a48 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -3,63 +3,119 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, SecondsFormat}; +use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::ProtocolVersion; +use crate::types::{ProtocolVersion, Value}; use crate::utils::{quote, tag, unquote}; use crate::{Error, RequiredVersion}; /// # [4.3.2.7. EXT-X-DATERANGE] /// /// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 -/// -/// TODO: Implement properly -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Builder, Debug, Clone, PartialEq)] +#[builder(setter(into))] pub struct ExtXDateRange { - /// A string that uniquely identifies a [`ExtXDateRange`] in the Playlist. + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Note /// This attribute is required. id: String, + #[builder(setter(strip_option), default)] /// A client-defined string that specifies some set of attributes and their - /// associated value semantics. All Date Ranges with the same CLASS - /// attribute value MUST adhere to these semantics. This attribute is - /// OPTIONAL. + /// associated value semantics. All [`ExtXDateRange`]s with the same class + /// attribute value must adhere to these semantics. + /// + /// # Note + /// This attribute is optional. class: Option, - /// The date at which the Date Range begins. This attribute is REQUIRED. + /// The date at which the [`ExtXDateRange`] begins. + /// + /// # Note + /// This attribute is required. start_date: DateTime, - /// The date at which the Date Range ends. It MUST be equal to or later than - /// the value of the START-DATE attribute. This attribute is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Note + /// This attribute is optional. + /// + /// [`start-date`]: #method.start_date end_date: Option>, - /// The duration of the Date Range. It MUST NOT be negative. A single - /// instant in time (e.g., crossing a finish line) SHOULD be - /// represented with a duration of 0. This attribute is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., + /// crossing a finish line) should be represented with a duration of 0. + /// + /// # Note + /// This attribute is optional. duration: Option, - /// The expected duration of the Date Range. It MUST NOT be negative. This - /// attribute SHOULD be used to indicate the expected duration of a - /// Date Range whose actual duration is not yet known. - /// It is OPTIONAL. + #[builder(setter(strip_option), default)] + /// The expected duration of the [`ExtXDateRange`]. + /// This attribute should be used to indicate the expected duration of a + /// [`ExtXDateRange`] whose actual duration is not yet known. + /// + /// # Note + /// This attribute is optional. planned_duration: Option, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_cmd: Option, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_out: Option, + #[builder(setter(strip_option), default)] + /// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1 /// + /// # Note + /// This attribute is optional. scte35_in: Option, + #[builder(default)] /// This attribute indicates that the end of the range containing it is - /// equal to the START-DATE of its Following Range. The Following Range - /// is the Date Range of the same CLASS, that has the earliest - /// START-DATE after the START-DATE of the range in question. This - /// attribute is OPTIONAL. + /// equal to the [`start-date`] of its following range. The following range + /// is the [`ExtXDateRange`] of the same class, that has the earliest + /// [`start-date`] after the [`start-date`] of the range in question. + /// + /// # Note + /// This attribute is optional. end_on_next: bool, + #[builder(default)] /// The "X-" prefix defines a namespace reserved for client-defined - /// attributes. The client-attribute MUST be a legal AttributeName. - /// Clients SHOULD use a reverse-DNS syntax when defining their own - /// attribute names to avoid collisions. The attribute value MUST be - /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- - /// point. An example of a client-defined attribute is X-COM-EXAMPLE- - /// AD-ID="XYZ123". These attributes are OPTIONAL. - client_attributes: BTreeMap, + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Note + /// This attribute is optional. + client_attributes: BTreeMap, +} + +impl ExtXDateRangeBuilder { + /// Inserts a key value pair. + pub fn insert_client_attribute>( + &mut self, + key: K, + value: V, + ) -> &mut Self { + if self.client_attributes.is_none() { + self.client_attributes = Some(BTreeMap::new()); + } + + if let Some(client_attributes) = &mut self.client_attributes { + client_attributes.insert(key.to_string(), value.into()); + } else { + unreachable!(); + } + self + } } impl ExtXDateRange { @@ -97,68 +153,542 @@ impl ExtXDateRange { client_attributes: BTreeMap::new(), } } + + /// Returns a builder for [`ExtXDateRange`]. + pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() } + + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// assert_eq!(date_range.id(), &"id".to_string()); + /// ``` + pub const fn id(&self) -> &String { &self.id } + + /// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// date_range.set_id("new_id"); + /// assert_eq!(date_range.id(), &"new_id".to_string()); + /// ``` + pub fn set_id(&mut self, value: T) -> &mut Self { + self.id = value.to_string(); + self + } + + /// A client-defined string that specifies some set of attributes and their + /// associated value semantics. All [`ExtXDateRange`]s with the same class + /// attribute value must adhere to these semantics. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.class(), &None); + /// + /// date_range.set_class(Some("example_class")); + /// assert_eq!(date_range.class(), &Some("example_class".to_string())); + /// ``` + pub const fn class(&self) -> &Option { &self.class } + + /// A client-defined string that specifies some set of attributes and their + /// associated value semantics. All [`ExtXDateRange`]s with the same class + /// attribute value must adhere to these semantics. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.class(), &None); + /// + /// date_range.set_class(Some("example_class")); + /// assert_eq!(date_range.class(), &Some("example_class".to_string())); + /// ``` + pub fn set_class(&mut self, value: Option) -> &mut Self { + self.class = value.map(|v| v.to_string()); + self + } + + /// The date at which the [`ExtXDateRange`] begins. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// assert_eq!( + /// date_range.start_date(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31) + /// ); + /// ``` + pub const fn start_date(&self) -> DateTime { self.start_date } + + /// The date at which the [`ExtXDateRange`] begins. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// + /// date_range.set_start_date( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// ); + /// assert_eq!( + /// date_range.start_date(), + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ); + /// ``` + pub fn set_start_date(&mut self, value: T) -> &mut Self + where + T: Into>, + { + self.start_date = value.into(); + self + } + + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_date(), None); + /// + /// date_range.set_end_date(Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// )); + /// assert_eq!( + /// date_range.end_date(), + /// Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ) + /// ); + /// ``` + pub const fn end_date(&self) -> Option> { self.end_date } + + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_date(), None); + /// + /// date_range.set_end_date(Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10), + /// )); + /// assert_eq!( + /// date_range.end_date(), + /// Some( + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 10, 10) + /// .and_hms_milli(10, 10, 10, 10) + /// ) + /// ); + /// ``` + pub fn set_end_date(&mut self, value: Option) -> &mut Self + where + T: Into>, + { + self.end_date = value.map(Into::into); + self + } + + /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., + /// crossing a finish line) should be represented with a duration of 0. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.duration(), None); + /// + /// date_range.set_duration(Some(Duration::from_secs_f64(1.234))); + /// assert_eq!(date_range.duration(), Some(Duration::from_secs_f64(1.234))); + /// ``` + pub const fn duration(&self) -> Option { self.duration } + + /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., + /// crossing a finish line) should be represented with a duration of 0. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.duration(), None); + /// + /// date_range.set_duration(Some(Duration::from_secs_f64(1.234))); + /// assert_eq!(date_range.duration(), Some(Duration::from_secs_f64(1.234))); + /// ``` + pub fn set_duration(&mut self, value: Option) -> &mut Self { + self.duration = value; + self + } + + /// The expected duration of the [`ExtXDateRange`]. + /// This attribute should be used to indicate the expected duration of a + /// [`ExtXDateRange`] whose actual duration is not yet known. + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.planned_duration(), None); + /// + /// date_range.set_planned_duration(Some(Duration::from_secs_f64(1.2345))); + /// assert_eq!( + /// date_range.planned_duration(), + /// Some(Duration::from_secs_f64(1.2345)) + /// ); + /// ``` + pub const fn planned_duration(&self) -> Option { self.planned_duration } + + /// The expected duration of the [`ExtXDateRange`]. + /// This attribute should be used to indicate the expected duration of a + /// [`ExtXDateRange`] whose actual duration is not yet known. + /// The date at which the [`ExtXDateRange`] ends. It must be equal to or + /// later than the value of the [`start-date`] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.planned_duration(), None); + /// + /// date_range.set_planned_duration(Some(Duration::from_secs_f64(1.2345))); + /// assert_eq!( + /// date_range.planned_duration(), + /// Some(Duration::from_secs_f64(1.2345)) + /// ); + /// ``` + pub fn set_planned_duration(&mut self, value: Option) -> &mut Self { + self.planned_duration = value; + self + } + + pub const fn scte35_cmd(&self) -> &Option { &self.scte35_cmd } + + pub const fn scte35_in(&self) -> &Option { &self.scte35_in } + + pub const fn scte35_out(&self) -> &Option { &self.scte35_out } + + /// This attribute indicates that the end of the range containing it is + /// equal to the [`start-date`] of its following range. The following range + /// is the [`ExtXDateRange`] of the same class, that has the earliest + /// [`start-date`] after the [`start-date`] of the range in question. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_on_next(), false); + /// + /// date_range.set_end_on_next(true); + /// assert_eq!(date_range.end_on_next(), true); + /// ``` + pub const fn end_on_next(&self) -> bool { self.end_on_next } + + /// This attribute indicates that the end of the range containing it is + /// equal to the [`start-date`] of its following range. The following range + /// is the [`ExtXDateRange`] of the same class, that has the earliest + /// [`start-date`] after the [`start-date`] of the range in question. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use std::time::Duration; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.end_on_next(), false); + /// + /// date_range.set_end_on_next(true); + /// assert_eq!(date_range.end_on_next(), true); + /// ``` + pub fn set_end_on_next(&mut self, value: bool) -> &mut Self { + self.end_on_next = value; + self + } + + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use std::collections::BTreeMap; + /// + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use hls_m3u8::types::Value; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.client_attributes(), &BTreeMap::new()); + /// + /// let mut attributes = BTreeMap::new(); + /// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// date_range.set_client_attributes(attributes.clone()); + /// assert_eq!(date_range.client_attributes(), &attributes); + /// ``` + pub const fn client_attributes(&self) -> &BTreeMap { &self.client_attributes } + + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use std::collections::BTreeMap; + /// + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use hls_m3u8::types::Value; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.client_attributes(), &BTreeMap::new()); + /// + /// let mut attributes = BTreeMap::new(); + /// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// date_range + /// .client_attributes_mut() + /// .insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// assert_eq!(date_range.client_attributes(), &attributes); + /// ``` + pub fn client_attributes_mut(&mut self) -> &mut BTreeMap { + &mut self.client_attributes + } + + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute must be a uppercase characters. + /// Clients should use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. An example of a client-defined + /// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use std::collections::BTreeMap; + /// + /// use chrono::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// use hls_m3u8::types::Value; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let mut date_range = ExtXDateRange::new( + /// "id", + /// FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31), + /// ); + /// # assert_eq!(date_range.client_attributes(), &BTreeMap::new()); + /// + /// let mut attributes = BTreeMap::new(); + /// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1)); + /// + /// date_range.set_client_attributes(attributes.clone()); + /// assert_eq!(date_range.client_attributes(), &attributes); + /// ``` + pub fn set_client_attributes(&mut self, value: BTreeMap) -> &mut Self { + self.client_attributes = value; + self + } } /// This tag requires [`ProtocolVersion::V1`]. -/// -/// # Example -/// ``` -/// # use hls_m3u8::tags::ExtXDateRange; -/// use chrono::offset::TimeZone; -/// use chrono::{DateTime, FixedOffset}; -/// use hls_m3u8::types::ProtocolVersion; -/// use hls_m3u8::RequiredVersion; -/// -/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds -/// -/// let date_range = ExtXDateRange::new( -/// "id", -/// FixedOffset::east(8 * HOURS_IN_SECS) -/// .ymd(2010, 2, 19) -/// .and_hms_milli(14, 54, 23, 31), -/// ); -/// assert_eq!(date_range.required_version(), ProtocolVersion::V1); -/// ``` impl RequiredVersion for ExtXDateRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } -impl fmt::Display for ExtXDateRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - write!(f, "ID={}", quote(&self.id))?; - if let Some(value) = &self.class { - write!(f, ",CLASS={}", quote(value))?; - } - write!(f, ",START-DATE={}", quote(&self.start_date))?; - if let Some(value) = &self.end_date { - write!(f, ",END-DATE={}", quote(value))?; - } - if let Some(value) = &self.duration { - write!(f, ",DURATION={}", value.as_secs_f64())?; - } - if let Some(value) = &self.planned_duration { - write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; - } - if let Some(value) = &self.scte35_cmd { - write!(f, ",SCTE35-CMD={}", quote(value))?; - } - if let Some(value) = &self.scte35_out { - write!(f, ",SCTE35-OUT={}", quote(value))?; - } - if let Some(value) = &self.scte35_in { - write!(f, ",SCTE35-IN={}", quote(value))?; - } - if self.end_on_next { - write!(f, ",END-ON-NEXT=YES",)?; - } - for (k, v) in &self.client_attributes { - write!(f, ",{}={}", k, v)?; - } - Ok(()) - } -} - impl FromStr for ExtXDateRange { type Err = Error; @@ -195,13 +725,13 @@ impl FromStr for ExtXDateRange { "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { if value != "YES" { - return Err(Error::invalid_input()); + return Err(Error::custom("The value of `END-ON-NEXT` has to be `YES`!")); } end_on_next = true; } _ => { if key.starts_with("X-") { - client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); + client_attributes.insert(key.to_ascii_uppercase(), value.parse()?); } else { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an @@ -235,6 +765,60 @@ impl FromStr for ExtXDateRange { } } +impl fmt::Display for ExtXDateRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "ID={}", quote(&self.id))?; + if let Some(value) = &self.class { + write!(f, ",CLASS={}", quote(value))?; + } + + write!( + f, + ",START-DATE={}", + quote(&self.start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + + if let Some(value) = &self.end_date { + write!( + f, + ",END-DATE={}", + quote(value.to_rfc3339_opts(SecondsFormat::AutoSi, true)) + )?; + } + + if let Some(value) = &self.duration { + write!(f, ",DURATION={}", value.as_secs_f64())?; + } + + if let Some(value) = &self.planned_duration { + write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; + } + + if let Some(value) = &self.scte35_cmd { + write!(f, ",SCTE35-CMD={}", value)?; + } + + if let Some(value) = &self.scte35_out { + write!(f, ",SCTE35-OUT={}", value)?; + } + + if let Some(value) = &self.scte35_in { + write!(f, ",SCTE35-IN={}", value)?; + } + + for (k, v) in &self.client_attributes { + write!(f, ",{}={}", k, v)?; + } + + if self.end_on_next { + write!(f, ",END-ON-NEXT=YES",)?; + } + + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; @@ -242,6 +826,112 @@ mod test { const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + #[test] + fn test_parser() { + assert_eq!( + "#EXT-X-DATERANGE:\ + ID=\"splice-6FFFFFF0\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + PLANNED-DURATION=59.993,\ + SCTE35-OUT=0xFC002F0000000000FF000014056F\ + FFFFF000E011622DCAFF000052636200000000000\ + A0008029896F50000008700000000" + .parse::() + .unwrap(), + ExtXDateRange::builder() + .id("splice-6FFFFFF0") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .planned_duration(Duration::from_secs_f64(59.993)) + .scte35_out( + "0xFC002F0000000000FF00001\ + 4056FFFFFF000E011622DCAFF0\ + 00052636200000000000A00080\ + 29896F50000008700000000" + ) + .build() + .unwrap() + ); + + assert_eq!( + "#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + CLASS=\"test_class\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-DATE=\"2014-03-05T11:16:00Z\",\ + DURATION=60.1,\ + PLANNED-DURATION=59.993,\ + X-CUSTOM=45.3,\ + SCTE35-CMD=0xFC002F0000000000FF2,\ + SCTE35-OUT=0xFC002F0000000000FF0,\ + SCTE35-IN=0xFC002F0000000000FF1,\ + END-ON-NEXT=YES,\ + UNKNOWN=PHANTOM" + .parse::() + .unwrap(), + ExtXDateRange::builder() + .id("test_id") + .class("test_class") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0)) + .duration(Duration::from_secs_f64(60.1)) + .planned_duration(Duration::from_secs_f64(59.993)) + .insert_client_attribute("X-CUSTOM", 45.3) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build() + .unwrap() + ); + + assert!("#EXT-X-DATERANGE:END-ON-NEXT=NO" + .parse::() + .is_err()); + + assert!("garbage".parse::().is_err()); + assert!("".parse::().is_err()); + + assert!("#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-ON-NEXT=YES" + .parse::() + .is_err()); + } + + #[test] + fn test_display() { + assert_eq!( + ExtXDateRange::builder() + .id("test_id") + .class("test_class") + .start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0)) + .end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0)) + .duration(Duration::from_secs_f64(60.1)) + .planned_duration(Duration::from_secs_f64(59.993)) + .insert_client_attribute("X-CUSTOM", 45.3) + .scte35_cmd("0xFC002F0000000000FF2") + .scte35_out("0xFC002F0000000000FF0") + .scte35_in("0xFC002F0000000000FF1") + .end_on_next(true) + .build() + .unwrap() + .to_string(), + "#EXT-X-DATERANGE:\ + ID=\"test_id\",\ + CLASS=\"test_class\",\ + START-DATE=\"2014-03-05T11:15:00Z\",\ + END-DATE=\"2014-03-05T11:16:00Z\",\ + DURATION=60.1,\ + PLANNED-DURATION=59.993,\ + SCTE35-CMD=0xFC002F0000000000FF2,\ + SCTE35-OUT=0xFC002F0000000000FF0,\ + SCTE35-IN=0xFC002F0000000000FF1,\ + X-CUSTOM=45.3,\ + END-ON-NEXT=YES" + ) + } + #[test] fn test_required_version() { assert_eq!( diff --git a/src/types/mod.rs b/src/types/mod.rs index 64aa441..a1df2bd 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,6 +15,7 @@ mod media_type; mod protocol_version; mod signed_decimal_floating_point; mod stream_inf; +mod value; pub use byte_range::*; pub use channels::*; @@ -32,3 +33,4 @@ pub use media_type::*; pub use protocol_version::*; pub(crate) use signed_decimal_floating_point::*; pub use stream_inf::*; +pub use value::*; diff --git a/src/types/value.rs b/src/types/value.rs new file mode 100644 index 0000000..90c1783 --- /dev/null +++ b/src/types/value.rs @@ -0,0 +1,106 @@ +use std::fmt; +use std::str::FromStr; + +use hex; + +use crate::utils::{quote, unquote}; +use crate::Error; + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +/// A [`Value`]. +pub enum Value { + /// A [`String`]. + String(String), + /// A sequence of bytes. + Hex(Vec), + /// A floating point number, that's neither NaN nor infinite! + Float(f64), +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Self::String(value) => write!(f, "{}", quote(value)), + Self::Hex(value) => write!(f, "0x{}", hex::encode_upper(value)), + Self::Float(value) => write!(f, "{}", value), + } + } +} + +impl FromStr for Value { + type Err = Error; + + fn from_str(input: &str) -> Result { + if input.starts_with("0x") || input.starts_with("0X") { + Ok(Self::Hex(hex::decode( + input.trim_start_matches("0x").trim_start_matches("0X"), + )?)) + } else { + match input.parse() { + Ok(value) => Ok(Self::Float(value)), + Err(_) => Ok(Self::String(unquote(input))), + } + } + } +} + +impl From for Value { + fn from(value: f64) -> Self { Self::Float(value) } +} + +impl From> for Value { + fn from(value: Vec) -> Self { Self::Hex(value) } +} + +impl From for Value { + fn from(value: String) -> Self { Self::String(unquote(value)) } +} + +impl From<&str> for Value { + fn from(value: &str) -> Self { Self::String(unquote(value)) } +} + +// impl> From for Value { +// fn from(value: T) -> Self { Self::Hex(value.as_ref().into()) } +// } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!(Value::Float(1.1).to_string(), "1.1".to_string()); + assert_eq!( + Value::String("&str".to_string()).to_string(), + "\"&str\"".to_string() + ); + assert_eq!( + Value::Hex(vec![1, 2, 3]).to_string(), + "0x010203".to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!(Value::Float(1.1), "1.1".parse().unwrap()); + assert_eq!( + Value::String("&str".to_string()), + "\"&str\"".parse().unwrap() + ); + assert_eq!(Value::Hex(vec![1, 2, 3]), "0x010203".parse().unwrap()); + assert_eq!(Value::Hex(vec![1, 2, 3]), "0X010203".parse().unwrap()); + assert!("0x010203Z".parse::().is_err()); + } + + #[test] + fn test_from() { + assert_eq!(Value::from(1.0_f64), Value::Float(1.0)); + assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string())); + assert_eq!( + Value::from("&str".to_string()), + Value::String("&str".to_string()) + ); + assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3])); + } +}