vast4_rs/
extension.rs

1/// XML node for custom extensions, as defined by the ad server. When used, a custom element should
2/// be nested under `<Extensions>` to help separa te custom XML elements from VAST elements.
3///
4/// ```text
5/// <xs:element name="Extensions" minOccurs="0" maxOccurs="1">
6///   <xs:complexType>
7///     <xs:sequence>
8///       <xs:element name="Extension" minOccurs="0" maxOccurs="unbounded">
9///     </xs:sequence>
10///   </xs:complexType>
11/// </xs:element>
12/// ```
13#[derive(hard_xml::XmlWrite, hard_xml::XmlRead, Default, PartialEq, Clone, Debug)]
14#[xml(tag = "Extensions")]
15pub struct Extensions {
16    /// The container for zero or more [`<Extension>`](Extension) elements.
17    #[xml(child = "Extension", default)]
18    pub extensions: Vec<Extension>,
19}
20
21// TODO: custom xml object
22/// One instance of `<Extension>` should be used for each custom extension. The type attribute
23/// is a custom value which identifies the extension.
24///
25/// ```text
26/// <xs:element name="Extension" minOccurs="0" maxOccurs="unbounded">
27///   <xs:complexType>
28///     <xs:attribute name="type" type="xs:string" use="optional">
29///     <xs:sequence>
30///       <xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip" />
31///     </xs:sequence>
32///   </xs:complexType>
33/// </xs:element>
34/// ```
35#[derive(Default, PartialEq, Clone, Debug)]
36pub struct Extension {
37    /// The MIME type of any code that might be included in the extension.
38    pub mime_type: Option<String>,
39
40    /// Custom XML object.
41    pub xml: String,
42}
43
44impl Extension {
45    pub fn from_struct<T: hard_xml::XmlWrite>(
46        mime_type: &str,
47        value: &T,
48    ) -> hard_xml::XmlResult<Self> {
49        Ok(Self {
50            mime_type: Some(mime_type.to_owned()),
51            xml: hard_xml::XmlWrite::to_string(value)?,
52        })
53    }
54
55    pub fn as_struct<'a, 'b, T>(&'a self) -> hard_xml::XmlResult<T>
56    where
57        T: hard_xml::XmlRead<'b>,
58        'a: 'b,
59    {
60        <T as hard_xml::XmlRead<'b>>::from_str(&self.xml)
61    }
62}
63
64impl hard_xml::XmlWrite for Extension {
65    fn to_writer<W: std::io::Write>(
66        &self,
67        writer: &mut hard_xml::XmlWriter<W>,
68    ) -> hard_xml::XmlResult<()> {
69        writer.write_element_start("Extension")?;
70        if let Some(ref attr) = self.mime_type {
71            writer.write_attribute("type", attr)?;
72        }
73        writer.write_element_end_open()?;
74        write!(writer.inner, "{}", self.xml)?;
75        writer.write_element_end_close("Extension")?;
76        Ok(())
77    }
78}
79
80impl<'a> hard_xml::XmlRead<'a> for Extension {
81    fn from_reader(reader: &mut hard_xml::XmlReader<'a>) -> hard_xml::XmlResult<Self> {
82        use hard_xml::xmlparser::{ElementEnd, Token};
83
84        reader.read_till_element_start("Extension")?;
85
86        let mut mime_type = None;
87        while let Some((name, value)) = reader.find_attribute()? {
88            if name == "type" {
89                mime_type = Some(match value {
90                    std::borrow::Cow::Borrowed(s) => s.to_owned(),
91                    std::borrow::Cow::Owned(s) => s,
92                });
93            }
94        }
95
96        if let Some(t) = reader.next() {
97            let t = t?;
98            if !matches!(
99                t,
100                Token::ElementEnd {
101                    end: ElementEnd::Open,
102                    ..
103                }
104            ) {
105                return Err(hard_xml::XmlError::UnexpectedToken {
106                    token: format!("{t:?}"),
107                });
108            }
109        }
110
111        let mut depth = 0;
112        let mut xml = String::new();
113
114        while let Some(t) = reader.next() {
115            match t? {
116                Token::Declaration { span, .. }
117                | Token::ProcessingInstruction { span, .. }
118                | Token::Comment { span, .. }
119                | Token::DtdStart { span, .. }
120                | Token::EmptyDtd { span, .. }
121                | Token::EntityDeclaration { span, .. }
122                | Token::DtdEnd { span }
123                | Token::ElementStart { span, .. }
124                | Token::Cdata { span, .. } => xml.push_str(span.as_str()),
125                Token::Attribute { span, .. } => {
126                    xml.push(' ');
127                    xml.push_str(span.as_str())
128                }
129                Token::Text { text } => xml.push_str(text.as_str()),
130                Token::ElementEnd { end, span } => {
131                    match end {
132                        ElementEnd::Open => {
133                            depth += 1;
134                        }
135                        ElementEnd::Close(_, name) => {
136                            if depth == 0 && name.as_str() == "Extension" {
137                                break;
138                            } else {
139                                depth -= 1;
140                            }
141                        }
142                        ElementEnd::Empty => {}
143                    };
144                    xml.push_str(span.as_str())
145                }
146            }
147        }
148
149        Ok(Self { mime_type, xml })
150    }
151}
152
153#[cfg(test)]
154#[test]
155fn test_extension_attribute_flexible() {
156    let xml = r#"<Extension type="application/xml" anyAttribute1="value1">
157             <CreativeExtension type="text/javascript">
158                 <![CDATA[
159                     <Ext>
160                         <Some id="hoge">fuga</Some>
161                     </Ext>
162                 ]]>
163             </CreativeExtension>
164             <CreativeExtension type="text/javascript">
165                 <Ext>
166                    <Some id="hoge">fuga</Some>
167                 </Ext>
168                 <Extension>hoge</Extension>
169             </CreativeExtension>
170         </Extension>"#;
171    assert!(crate::from_str::<Extension>(xml).is_ok());
172}
173
174crate::declare_test!(
175    test_extension,
176    Extension,
177    r#"<Extension>
178             <CreativeExtension type="text/javascript">
179                 <![CDATA[
180                     <Ext>
181                         <Some id="hoge">fuga</Some>
182                     </Ext>
183                 ]]>
184             </CreativeExtension>
185             <CreativeExtension type="text/javascript">
186                 <Ext>
187                    <Some id="hoge">fuga</Some>
188                 </Ext>
189                 <Extension>hoge</Extension>
190             </CreativeExtension>
191         </Extension>"#,
192    Extension {
193        mime_type: None,
194        xml: r#"
195             <CreativeExtension type="text/javascript">
196                 <![CDATA[
197                     <Ext>
198                         <Some id="hoge">fuga</Some>
199                     </Ext>
200                 ]]>
201             </CreativeExtension>
202             <CreativeExtension type="text/javascript">
203                 <Ext>
204                    <Some id="hoge">fuga</Some>
205                 </Ext>
206                 <Extension>hoge</Extension>
207             </CreativeExtension>
208         "#
209        .into()
210    }
211);