rss-2.0.11/.cargo_vcs_info.json0000644000000001360000000000100117240ustar { "git": { "sha1": "44eece93b15248efa37356665d4400da0c1de9ec" }, "path_in_vcs": "" }rss-2.0.11/Cargo.toml0000644000000040720000000000100077250ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "rss" version = "2.0.11" authors = [ "James Hurst ", "Corey Farwell ", "Chris Palmer ", ] build = false include = [ "src/*", "Cargo.toml", "LICENSE-MIT", "LICENSE-APACHE", "README.md", ] autobins = false autoexamples = false autotests = false autobenches = false description = "Library for serializing the RSS web content syndication format" documentation = "https://docs.rs/rss/" readme = "README.md" keywords = [ "rss", "feed", "parser", "parsing", ] license = "MIT/Apache-2.0" repository = "https://github.com/rust-syndication/rss" [package.metadata.docs.rs] all-features = false [lib] name = "rss" path = "src/lib.rs" [dependencies.atom_syndication] version = "0.12" optional = true [dependencies.chrono] version = "0.4.31" features = ["alloc"] optional = true default-features = false [dependencies.derive_builder] version = "0.20" optional = true [dependencies.mime] version = "0.3" optional = true [dependencies.never] version = "0.1" optional = true [dependencies.quick-xml] version = "0.37.1" features = ["encoding"] [dependencies.serde] version = "1.0" features = ["derive"] optional = true [dependencies.url] version = "2.1" optional = true [dev-dependencies.bencher] version = "0.1" [features] atom = ["atom_syndication"] builders = [ "derive_builder", "never", "atom_syndication/builders", ] default = ["builders"] validation = [ "chrono", "chrono/std", "url", "mime", ] with-serde = [ "serde", "atom_syndication/with-serde", ] rss-2.0.11/Cargo.toml.orig000064400000000000000000000027101046102023000134030ustar 00000000000000[package] name = "rss" version = "2.0.11" authors = ["James Hurst ", "Corey Farwell ", "Chris Palmer "] description = "Library for serializing the RSS web content syndication format" repository = "https://github.com/rust-syndication/rss" documentation = "https://docs.rs/rss/" license = "MIT/Apache-2.0" readme = "README.md" keywords = ["rss", "feed", "parser", "parsing"] include = ["src/*", "Cargo.toml", "LICENSE-MIT", "LICENSE-APACHE", "README.md"] edition = "2021" [package.metadata.docs.rs] all-features = false [features] default = ["builders"] atom = ["atom_syndication"] builders = ["derive_builder", "never", "atom_syndication/builders"] validation = ["chrono", "chrono/std", "url", "mime"] with-serde = ["serde", "atom_syndication/with-serde"] [dependencies] quick-xml = { version = "0.37.1", features = ["encoding"] } atom_syndication = { version = "0.12", optional = true } chrono = { version = "0.4.31", optional = true, default-features = false, features = ["alloc"] } derive_builder = { version = "0.20", optional = true } mime = { version = "0.3", optional = true } never = { version = "0.1", optional = true } serde = { version = "1.0", optional = true, features = ["derive"] } url = { version = "2.1", optional = true } [dev-dependencies] bencher = "0.1" [[bench]] name = "read" path = "benches/read.rs" harness = false [[bench]] name = "write" path = "benches/write.rs" harness = false rss-2.0.11/LICENSE-APACHE000064400000000000000000000261461046102023000124510ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2015-2021 The rust-syndication Developers Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. rss-2.0.11/LICENSE-MIT000064400000000000000000000021041046102023000121450ustar 00000000000000MIT License Copyright © 2015-2021 The rust-syndication Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rss-2.0.11/README.md000064400000000000000000000066531046102023000120050ustar 00000000000000# rss [![Build status](https://github.com/rust-syndication/rss/workflows/Build/badge.svg)](https://github.com/rust-syndication/rss/actions?query=branch%3Amaster) [![Codecov](https://codecov.io/gh/rust-syndication/rss/branch/master/graph/badge.svg)](https://codecov.io/gh/rust-syndication/rss) [![crates.io Status](https://img.shields.io/crates/v/rss.svg)](https://crates.io/crates/rss) [![Docs](https://docs.rs/rss/badge.svg)](https://docs.rs/rss) Library for deserializing and serializing the RSS web content syndication format. ### Supported Versions Reading from the following RSS versions is supported: * RSS 0.90 * RSS 0.91 * RSS 0.92 * RSS 1.0 * RSS 2.0 Writing support is limited to RSS 2.0. ### Documentation - [Released](https://docs.rs/rss/) - [Master](https://rust-syndication.github.io/rss/rss/) ## Usage Add the dependency to your `Cargo.toml`. ```toml [dependencies] rss = "2.0" ``` ## Reading A channel can be read from any object that implements the `BufRead` trait. ### From a file ```rust use std::fs::File; use std::io::BufReader; use rss::Channel; let file = File::open("example.xml").unwrap(); let channel = Channel::read_from(BufReader::new(file)).unwrap(); ``` ### From a buffer **Note**: This example requires [reqwest](https://crates.io/crates/reqwest) crate. ```rust use std::error::Error; use rss::Channel; async fn example_feed() -> Result> { let content = reqwest::get("http://example.com/feed.xml") .await? .bytes() .await?; let channel = Channel::read_from(&content[..])?; Ok(channel) } ``` ## Writing A channel can be written to any object that implements the `Write` trait or converted to an XML string using the `ToString` trait. ```rust use rss::Channel; let channel = Channel::default(); channel.write_to(::std::io::sink()).unwrap(); // // write to the channel to a writer let string = channel.to_string(); // convert the channel to a string ``` ## Creation Builder methods are provided to assist in the creation of channels. **Note**: This requires the `builders` feature, which is enabled by default. ```rust use rss::ChannelBuilder; let channel = ChannelBuilder::default() .title("Channel Title") .link("http://example.com") .description("An RSS feed.") .build() .unwrap(); ``` ## Validation Validation methods are provided to validate the contents of a channel against the RSS specification. **Note**: This requires enabling the `validation` feature. ```rust use rss::Channel; use rss::validation::Validate; let channel = Channel::default(); channel.validate().unwrap(); ``` ## Extensions Elements which have non-default namespaces will be considered extensions. Extensions are stored in `Channel.extensions` and `Item.extensions`. For convenience, [Dublin Core](http://dublincore.org/documents/dces/), [Syndication](http://web.resource.org/rss/1.0/modules/syndication/) and [iTunes](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) extensions are extracted to structs and stored in as properties on channels and items. ## Invalid Feeds As a best effort to parse invalid feeds `rss` will default elements declared as "required" by the RSS 2.0 specification to an empty string. ## License Licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. rss-2.0.11/src/category.rs000064400000000000000000000100211046102023000134600ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::{BufRead, Write}; use quick_xml::events::attributes::Attributes; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::error::Error; use crate::toxml::ToXml; use crate::util::{attr_value, decode, element_text}; /// Represents a category in an RSS feed. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Category { /// The name of the category. pub name: String, /// The domain for the category. pub domain: Option, } impl Category { /// Return the name of this category. /// /// # Examples /// /// ``` /// use rss::Category; /// /// let mut category = Category::default(); /// category.set_name("Technology"); /// assert_eq!(category.name(), "Technology"); /// ``` pub fn name(&self) -> &str { self.name.as_str() } /// Set the name of this category. /// /// # Examples /// /// ``` /// use rss::Category; /// /// let mut category = Category::default(); /// category.set_name("Technology"); /// ``` pub fn set_name(&mut self, name: V) where V: Into, { self.name = name.into(); } /// Return the domain of this category. /// /// # Examples /// /// ``` /// use rss::Category; /// /// let mut category = Category::default(); /// category.set_domain("http://example.com".to_string()); /// assert_eq!(category.domain(), Some("http://example.com")); /// ``` pub fn domain(&self) -> Option<&str> { self.domain.as_deref() } /// Set the domain of this category. /// /// # Examples /// /// ``` /// use rss::Category; /// /// let mut category = Category::default(); /// category.set_domain("http://example.com".to_string()); /// ``` pub fn set_domain(&mut self, domain: V) where V: Into>, { self.domain = domain.into(); } } impl Category { /// Builds a Category from source XML pub fn from_xml( reader: &mut Reader, mut atts: Attributes, ) -> Result { let mut category = Category::default(); for attr in atts.with_checks(false).flatten() { if decode(attr.key.as_ref(), reader)?.as_ref() == "domain" { category.domain = Some(attr_value(&attr, reader)?.to_string()); break; } } category.name = element_text(reader)?.unwrap_or_default(); Ok(category) } } impl ToXml for Category { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "category"; let mut element = BytesStart::new(name); if let Some(ref domain) = self.domain { element.push_attribute(("domain", &**domain)); } writer.write_event(Event::Start(element))?; writer.write_event(Event::Text(BytesText::new(&self.name)))?; writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } } impl From for Category { fn from(name: String) -> Self { Self { name, domain: None } } } impl From<&str> for Category { fn from(name: &str) -> Self { Self { name: name.to_string(), domain: None, } } } #[cfg(feature = "builders")] impl CategoryBuilder { /// Builds a new `Category`. pub fn build(&self) -> Category { self.build_impl().unwrap() } } rss-2.0.11/src/channel.rs000064400000000000000000001330361046102023000132670ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::fmt::Display; use std::io::{BufRead, Write}; use std::str::{self, FromStr}; use quick_xml::events::attributes::Attributes; use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::category::Category; use crate::cloud::Cloud; use crate::error::Error; #[cfg(feature = "atom")] use crate::extension::atom; use crate::extension::dublincore; use crate::extension::itunes::{self, is_itunes_namespace}; use crate::extension::syndication; use crate::extension::util::{ extension_entry, extension_name, parse_extension_element, read_namespace_declarations, }; use crate::extension::ExtensionMap; use crate::image::Image; use crate::item::Item; use crate::textinput::TextInput; use crate::toxml::{ToXml, WriterExt}; use crate::util::{decode, element_text, skip}; /// Represents the channel of an RSS feed. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Channel { /// The name of the channel. pub title: String, /// The URL for the website corresponding to the channel. pub link: String, /// A description of the channel. pub description: String, /// The language of the channel. pub language: Option, /// The copyright notice for the channel. pub copyright: Option, /// The email address for the managing editor. pub managing_editor: Option, /// The email address for the webmaster. pub webmaster: Option, /// The publication date for the content of the channel as an RFC822 timestamp. pub pub_date: Option, /// The date that the contents of the channel last changed as an RFC822 timestamp. pub last_build_date: Option, /// The categories the channel belongs to. #[cfg_attr(feature = "builders", builder(setter(each = "category")))] pub categories: Vec, /// A string indicating the program used to generate the channel. pub generator: Option, /// A URL that points to the documentation for the RSS format. pub docs: Option, /// The cloud to register with to be notified of updates to the channel. pub cloud: Option, /// The PICS rating for the channel. pub rating: Option, /// The number of minutes the channel can be cached before refreshing. pub ttl: Option, /// An image that can be displayed with the channel. pub image: Option, /// A text input box that can be displayed with the channel. pub text_input: Option, /// A hint to tell the aggregator which hours it can skip. #[cfg_attr(feature = "builders", builder(setter(each = "skip_hour")))] pub skip_hours: Vec, /// A hint to tell the aggregator which days it can skip. #[cfg_attr(feature = "builders", builder(setter(each = "skip_day")))] pub skip_days: Vec, /// The items in the channel. #[cfg_attr(feature = "builders", builder(setter(each = "item")))] pub items: Vec, /// The extensions for the channel. #[cfg_attr(feature = "builders", builder(setter(each = "extension")))] pub extensions: ExtensionMap, /// The Atom extension for the channel. #[cfg(feature = "atom")] pub atom_ext: Option, /// The iTunes extension for the channel. pub itunes_ext: Option, /// The Dublin Core extension for the channel. pub dublin_core_ext: Option, /// The Syndication extension for the channel. pub syndication_ext: Option, /// The namespaces present in the RSS tag. #[cfg_attr(feature = "builders", builder(setter(each = "namespace")))] pub namespaces: BTreeMap, } impl Channel { /// Return the title of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_title("Channel Title"); /// assert_eq!(channel.title(), "Channel Title"); /// ``` pub fn title(&self) -> &str { self.title.as_str() } /// Set the title of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_title("Channel Title"); /// ``` pub fn set_title(&mut self, title: V) where V: Into, { self.title = title.into(); } /// Return the URL for the website corresponding to this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_link("http://example.com"); /// assert_eq!(channel.link(), "http://example.com"); /// ``` pub fn link(&self) -> &str { self.link.as_str() } /// Set the URL for the website corresponding to this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_link("http://example.com"); /// ``` pub fn set_link(&mut self, link: V) where V: Into, { self.link = link.into(); } /// Return the description of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_description("Channel description"); /// assert_eq!(channel.description(), "Channel description"); /// ``` pub fn description(&self) -> &str { self.description.as_str() } /// Set the description of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_description("Channel description"); /// ``` pub fn set_description(&mut self, description: V) where V: Into, { self.description = description.into(); } /// Return the language of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_language("en-US".to_string()); /// assert_eq!(channel.language(), Some("en-US")); /// ``` pub fn language(&self) -> Option<&str> { self.language.as_ref().map(String::as_ref) } /// Set the language of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_language("en-US".to_string()); /// ``` pub fn set_language(&mut self, language: V) where V: Into>, { self.language = language.into(); } /// Return the copyright notice for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_copyright("© 2017 John Doe".to_string()); /// assert_eq!(channel.copyright(), Some("© 2017 John Doe")); /// ``` pub fn copyright(&self) -> Option<&str> { self.copyright.as_deref() } /// Set the copyright notice for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_copyright("© 2017 John Doe".to_string()); /// ``` pub fn set_copyright(&mut self, copyright: V) where V: Into>, { self.copyright = copyright.into(); } /// Return the email address for the managing editor of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_managing_editor("johndoe@example.com".to_string()); /// assert_eq!(channel.managing_editor(), Some("johndoe@example.com")); /// ``` pub fn managing_editor(&self) -> Option<&str> { self.managing_editor.as_deref() } /// Set the email address for the managing editor of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_managing_editor("johndoe@example.com".to_string()); /// assert_eq!(channel.managing_editor(), Some("johndoe@example.com")); /// ``` pub fn set_managing_editor(&mut self, managing_editor: V) where V: Into>, { self.managing_editor = managing_editor.into(); } /// Return the email address for webmaster of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_webmaster("johndoe@example.com".to_string()); /// assert_eq!(channel.webmaster(), Some("johndoe@example.com")); /// ``` pub fn webmaster(&self) -> Option<&str> { self.webmaster.as_deref() } /// Set the email address for webmaster of this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_webmaster("johndoe@example.com".to_string()); /// ``` pub fn set_webmaster(&mut self, webmaster: V) where V: Into>, { self.webmaster = webmaster.into(); } /// Return the publication date for the content of this channel as an RFC822 timestamp. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_pub_date("Sun, 1 Jan 2017 12:00:00 GMT".to_string()); /// assert_eq!(channel.pub_date(), Some("Sun, 1 Jan 2017 12:00:00 GMT")); /// ``` pub fn pub_date(&self) -> Option<&str> { self.pub_date.as_deref() } /// Set the publication date for the content of this channel as an RFC822 timestamp. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_pub_date("Sun, 1 Jan 2017 12:00:00 GMT".to_string()); /// assert_eq!(channel.pub_date(), Some("Sun, 1 Jan 2017 12:00:00 GMT")); /// ``` /// /// ## Using chrono::DateTime /// ``` /// # #[cfg(feature = "validation")] /// # { /// use rss::Channel; /// use chrono::{TimeZone, Utc}; /// /// let mut channel = Channel::default(); /// channel.set_pub_date(Utc.with_ymd_and_hms(2017, 1, 1, 12, 0, 0).unwrap().to_rfc2822()); /// assert_eq!(channel.pub_date(), Some("Sun, 1 Jan 2017 12:00:00 +0000")); /// # } /// ``` pub fn set_pub_date(&mut self, pub_date: V) where V: Into>, { self.pub_date = pub_date.into(); } /// Return the time that the content of this channel was last changed as an RFC822 timestamp. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_last_build_date("Sun, 1 Jan 2017 12:00:00 GMT".to_string()); /// assert_eq!(channel.last_build_date(), Some("Sun, 1 Jan 2017 12:00:00 GMT")); /// ``` pub fn last_build_date(&self) -> Option<&str> { self.last_build_date.as_deref() } /// Set the time that the content of this channel was last changed as an RFC822 timestamp. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_last_build_date("Sun, 1 Jan 2017 12:00:00 GMT".to_string()); /// assert_eq!(channel.last_build_date(), Some("Sun, 1 Jan 2017 12:00:00 GMT")); /// ``` /// /// ## Using chrono::DateTime /// ``` /// # #[cfg(feature = "validation")] /// # { /// use rss::Channel; /// use chrono::{TimeZone, Utc}; /// /// let mut channel = Channel::default(); /// channel.set_last_build_date(Utc.with_ymd_and_hms(2017, 1, 1, 12, 0, 0).unwrap().to_rfc2822()); /// assert_eq!(channel.last_build_date(), Some("Sun, 1 Jan 2017 12:00:00 +0000")); /// # } /// ``` pub fn set_last_build_date(&mut self, last_build_date: V) where V: Into>, { self.last_build_date = last_build_date.into(); } /// Return the categories that this channel belongs to. /// /// # Examples /// /// ``` /// use rss::{Channel, Category}; /// /// let mut channel = Channel::default(); /// channel.set_categories(vec![Category::default()]); /// assert_eq!(channel.categories().len(), 1); /// ``` pub fn categories(&self) -> &[Category] { &self.categories } /// Return a mutable slice of the categories that this channel belongs to. pub fn categories_mut(&mut self) -> &mut [Category] { &mut self.categories } /// Set the categories that this channel belongs to. /// /// # Examples /// /// ``` /// use rss::{Channel, Category}; /// /// let mut channel = Channel::default(); /// channel.set_categories(vec![Category::default()]); /// ``` pub fn set_categories(&mut self, categories: V) where V: Into>, { self.categories = categories.into(); } /// Return a string indicating the program used to generate the channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_generator("Program Name".to_string()); /// assert_eq!(channel.generator(), Some("Program Name")); /// ``` pub fn generator(&self) -> Option<&str> { self.generator.as_deref() } /// Set a string indicating the program used to generate the channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_generator("Program Name".to_string()); /// ``` pub fn set_generator(&mut self, generator: V) where V: Into>, { self.generator = generator.into(); } /// Return a URL that points to the documentation of the RSS format used in this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_docs("https://cyber.harvard.edu/rss/rss.html".to_string()); /// assert_eq!(channel.docs(), Some("https://cyber.harvard.edu/rss/rss.html")); /// ``` pub fn docs(&self) -> Option<&str> { self.docs.as_deref() } /// Set a URL that points to the documentation of the RSS format used in this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_docs("https://cyber.harvard.edu/rss/rss.html".to_string()); /// ``` pub fn set_docs(&mut self, docs: V) where V: Into>, { self.docs = docs.into(); } /// Return the information used to register with a cloud for notifications of updates to the /// channel. /// /// # Examples /// /// ``` /// use rss::{Channel, Cloud}; /// /// let mut channel = Channel::default(); /// channel.set_cloud(Cloud::default()); /// assert!(channel.cloud().is_some()); /// ``` pub fn cloud(&self) -> Option<&Cloud> { self.cloud.as_ref() } /// Set the information used to register with a cloud for notifications of updates to the /// channel. /// /// # Examples /// /// ``` /// use rss::{Channel, Cloud}; /// /// let mut channel = Channel::default(); /// channel.set_cloud(Cloud::default()); /// ``` pub fn set_cloud(&mut self, cloud: V) where V: Into>, { self.cloud = cloud.into(); } /// Return the time to live of this channel. This indicates the number of minutes the /// channel can be cached before needing to be refreshed. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_ttl("60".to_string()); /// assert_eq!(channel.ttl(), Some("60")); /// ``` pub fn ttl(&self) -> Option<&str> { self.ttl.as_deref() } /// Set the time to live of this channel. This indicates the number of minutes the /// channel can be cached before needing to be refreshed. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_ttl("60".to_string()); /// ``` pub fn set_ttl(&mut self, ttl: V) where V: Into>, { self.ttl = ttl.into(); } /// Return the image to be displayed with this channel. /// /// # Examples /// /// ``` /// use rss::{Channel, Image}; /// /// let mut channel = Channel::default(); /// channel.set_image(Image::default()); /// assert!(channel.image().is_some()); /// ``` pub fn image(&self) -> Option<&Image> { self.image.as_ref() } /// Set the image to be displayed with this channel. /// /// # Examples /// /// ``` /// use rss::{Channel, Image}; /// /// let mut channel = Channel::default(); /// channel.set_image(Image::default()); /// ``` pub fn set_image(&mut self, image: V) where V: Into>, { self.image = image.into(); } /// Return the [PICS](https://www.w3.org/PICS/) rating for this channel. pub fn rating(&self) -> Option<&str> { self.rating.as_deref() } /// Set the [PICS](https://www.w3.org/PICS/) rating for this channel. pub fn set_rating(&mut self, rating: V) where V: Into>, { self.rating = rating.into(); } /// Return the information for a text box to be displayed with this channel. /// /// # Examples /// /// ``` /// use rss::{Channel, TextInput}; /// /// let mut channel = Channel::default(); /// channel.set_text_input(TextInput::default()); /// assert!(channel.text_input().is_some()); /// ``` pub fn text_input(&self) -> Option<&TextInput> { self.text_input.as_ref() } /// Set the information for a text box to be displayed with this channel. /// /// # Examples /// /// ``` /// use rss::{Channel, TextInput}; /// /// let mut channel = Channel::default(); /// channel.set_text_input(TextInput::default()); /// ``` pub fn set_text_input(&mut self, text_input: V) where V: Into>, { self.text_input = text_input.into(); } /// Return the hours that aggregators can skip for refreshing content. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let skip_hours = vec![6, 7, 8, 14, 22]; /// /// let mut channel = Channel::default(); /// channel.set_skip_hours(vec!["1".to_string()]); /// assert_eq!(channel.skip_hours().len(), 1); /// ``` pub fn skip_hours(&self) -> &[String] { &self.skip_hours } /// Return a mutable slice of the hours that aggregators can skip for refreshing content. pub fn skip_hours_mut(&mut self) -> &mut [String] { &mut self.skip_hours } /// Set the hours that aggregators can skip for refreshing content. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_skip_hours(vec!["1".to_string()]); /// ``` pub fn set_skip_hours(&mut self, skip_hours: V) where V: Into>, { self.skip_hours = skip_hours.into(); } /// Return the days that aggregators can skip for refreshing content. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_skip_days(vec!["Monday".to_string()]); /// assert_eq!(channel.skip_days().len(), 1); /// ``` pub fn skip_days(&self) -> &[String] { &self.skip_days } /// Return a mutable slice of the days that aggregators can skip for refreshing content. pub fn skip_days_mut(&mut self) -> &mut [String] { &mut self.skip_days } /// Set the days that aggregators can skip for refreshing content. /// /// # Examples /// /// ``` /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_skip_days(vec!["Monday".to_string()]); /// ``` pub fn set_skip_days(&mut self, skip_days: V) where V: Into>, { self.skip_days = skip_days.into(); } /// Return the items in this channel. /// /// # Examples /// /// ``` /// use rss::{Channel, Item}; /// /// let mut channel = Channel::default(); /// channel.set_items(vec![Item::default()]); /// assert_eq!(channel.items().len(), 1); /// ``` pub fn items(&self) -> &[Item] { &self.items } /// Return a mutable slice of the items in this channel. pub fn items_mut(&mut self) -> &mut [Item] { &mut self.items } /// Consume the `Channel` and return a vector of `Item`s. /// /// # Examples /// /// ``` /// use rss::{Channel, Item}; /// /// let mut channel = Channel::default(); /// channel.set_items(vec![Item::default()]); /// assert_eq!(channel.into_items().len(), 1); /// ``` pub fn into_items(self) -> Vec { self.items } /// Set the items in this channel. /// /// # Examples /// /// ``` /// use rss::{Channel, Item}; /// /// let mut channel = Channel::default(); /// channel.set_items(vec![Item::default()]); /// ``` pub fn set_items(&mut self, items: V) where V: Into>, { self.items = items.into(); } /// Return the Atom extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::atom::AtomExtension; /// /// let mut channel = Channel::default(); /// channel.set_atom_ext(AtomExtension::default()); /// assert!(channel.atom_ext().is_some()); /// ``` #[cfg(feature = "atom")] pub fn atom_ext(&self) -> Option<&atom::AtomExtension> { self.atom_ext.as_ref() } /// Set the Atom extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::atom::AtomExtension; /// /// let mut channel = Channel::default(); /// channel.set_atom_ext(AtomExtension::default()); /// ``` #[cfg(feature = "atom")] pub fn set_atom_ext(&mut self, atom_ext: V) where V: Into>, { self.atom_ext = atom_ext.into(); } /// Return the iTunes extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut channel = Channel::default(); /// channel.set_itunes_ext(ITunesChannelExtension::default()); /// assert!(channel.itunes_ext().is_some()); /// ``` pub fn itunes_ext(&self) -> Option<&itunes::ITunesChannelExtension> { self.itunes_ext.as_ref() } /// Set the iTunes extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut channel = Channel::default(); /// channel.set_itunes_ext(ITunesChannelExtension::default()); /// ``` pub fn set_itunes_ext(&mut self, itunes_ext: V) where V: Into>, { self.itunes_ext = itunes_ext.into(); } /// Return the Dublin Core extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::dublincore::DublinCoreExtension; /// /// let mut channel = Channel::default(); /// channel.set_dublin_core_ext(DublinCoreExtension::default()); /// assert!(channel.dublin_core_ext().is_some()); /// ``` pub fn dublin_core_ext(&self) -> Option<&dublincore::DublinCoreExtension> { self.dublin_core_ext.as_ref() } /// Set the Dublin Core extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::dublincore::DublinCoreExtension; /// /// let mut channel = Channel::default(); /// channel.set_dublin_core_ext(DublinCoreExtension::default()); /// ``` pub fn set_dublin_core_ext(&mut self, dublin_core_ext: V) where V: Into>, { self.dublin_core_ext = dublin_core_ext.into(); } /// Return the Syndication extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::syndication::SyndicationExtension; /// /// let mut channel = Channel::default(); /// channel.set_syndication_ext(SyndicationExtension::default()); /// assert!(channel.syndication_ext().is_some()); /// ``` pub fn syndication_ext(&self) -> Option<&syndication::SyndicationExtension> { self.syndication_ext.as_ref() } /// Set the Syndication extension for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::syndication::SyndicationExtension; /// /// let mut channel = Channel::default(); /// channel.set_syndication_ext(SyndicationExtension::default()); /// ``` pub fn set_syndication_ext(&mut self, syndication_ext: V) where V: Into>, { self.syndication_ext = syndication_ext.into(); } /// Return the extensions for this channel. /// /// # Examples /// /// ``` /// use std::collections::BTreeMap; /// use rss::Channel; /// use rss::extension::{ExtensionMap, Extension}; /// /// let extension = Extension::default(); /// /// let mut item_map = BTreeMap::>::new(); /// item_map.insert("ext:name".to_string(), vec![extension]); /// /// let mut extension_map = ExtensionMap::default(); /// extension_map.insert("ext".to_string(), item_map); /// /// let mut channel = Channel::default(); /// channel.set_extensions(extension_map); /// assert_eq!(channel.extensions() /// .get("ext") /// .and_then(|m| m.get("ext:name")) /// .map(|v| v.len()), /// Some(1)); /// ``` pub fn extensions(&self) -> &ExtensionMap { &self.extensions } /// Set the extensions for this channel. /// /// # Examples /// /// ``` /// use rss::Channel; /// use rss::extension::ExtensionMap; /// /// let mut channel = Channel::default(); /// channel.set_extensions(ExtensionMap::default()); /// ``` pub fn set_extensions(&mut self, extensions: V) where V: Into, { self.extensions = extensions.into() } /// Return the namespaces for this channel. /// /// # Examples /// /// ``` /// use std::collections::BTreeMap; /// use rss::Channel; /// /// let mut namespaces = BTreeMap::new(); /// namespaces.insert("ext".to_string(), "http://example.com".to_string()); /// /// let mut channel = Channel::default(); /// channel.set_namespaces(namespaces); /// assert_eq!(channel.namespaces().get("ext").map(String::as_str), Some("http://example.com")); /// ``` pub fn namespaces(&self) -> &BTreeMap { &self.namespaces } /// Set the namespaces for this channel. /// /// # Examples /// /// ``` /// use std::collections::BTreeMap; /// use rss::Channel; /// /// let mut channel = Channel::default(); /// channel.set_namespaces(BTreeMap::new()); /// ``` pub fn set_namespaces(&mut self, namespaces: V) where V: Into>, { self.namespaces = namespaces.into() } } impl Channel { /// Attempt to read an RSS channel from a reader. /// /// # Example /// /// ```rust,ignore /// let reader: BufRead = ...; /// let channel = Channel::read_from(reader).unwrap(); /// ``` pub fn read_from(reader: R) -> Result { let mut reader = Reader::from_reader(reader); reader.config_mut().expand_empty_elements = true; let namespaces; let mut buf = Vec::new(); let mut channel: Option = None; // for parsing RSS 0.9, 1.0 feeds let mut items: Option> = None; let mut image: Option = None; let mut text_input: Option = None; // find opening element loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => match decode(element.name().as_ref(), &reader)?.as_ref() { "rss" | "rdf:RDF" => { namespaces = read_namespace_declarations( &mut reader, element.attributes(), &BTreeMap::new(), )? .into_owned(); break; } _ => { return Err(Error::InvalidStartTag); } }, Event::Eof => return Err(Error::Eof), _ => continue, } } loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => match decode(element.name().as_ref(), &reader)?.as_ref() { "channel" => { let inner = Channel::from_xml(&namespaces, &mut reader, element.attributes())?; channel = Some(inner); } "item" => { let item = Item::from_xml(&namespaces, &mut reader, element.attributes())?; if items.is_none() { items = Some(Vec::new()); } items.as_mut().unwrap().push(item); } "image" => { let inner = Image::from_xml(&mut reader, element.attributes())?; image = Some(inner); } "textinput" => { let inner = TextInput::from_xml(&mut reader, element.attributes())?; text_input = Some(inner); } _ => skip(element.name(), &mut reader)?, }, Event::End(_) | Event::Eof => break, _ => {} } buf.clear(); } if let Some(mut channel) = channel { if let Some(mut items) = items { channel.items.append(&mut items); } if image.is_some() { channel.image = image; } if text_input.is_some() { channel.text_input = text_input; } channel.namespaces = namespaces; Ok(channel) } else { Err(Error::Eof) } } fn write(&self, mut writer: Writer) -> Result { writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?; let name = "rss"; let mut element = BytesStart::new(name); element.push_attribute(("version", "2.0")); let used_namespaces = self.used_namespaces(); let mut namespaces: BTreeMap<&String, &String> = BTreeMap::new(); namespaces.extend(&used_namespaces); namespaces.extend(&self.namespaces); for (name, url) in namespaces { element.push_attribute((format!("xmlns:{}", name).as_bytes(), url.as_bytes())); } writer.write_event(Event::Start(element))?; self.to_xml(&mut writer)?; writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(writer.into_inner()) } /// Attempt to write the RSS channel as XML to a writer. /// /// # Example /// /// ```rust,ignore /// let channel: Channel = ...; /// let writer: Write = ...; /// channel.write_to(writer).unwrap(); /// ``` pub fn write_to(&self, writer: W) -> Result { self.write(::quick_xml::Writer::new(writer)) } /// Attempt to write the RSS channel as pretty XML to a writer. /// /// # Example /// /// ```rust,ignore /// let channel: Channel = ...; /// let writer: Write = ...; /// channel.pretty_write_to(writer, b' ', 2).unwrap(); /// ``` pub fn pretty_write_to( &self, writer: W, indent_char: u8, indent_size: usize, ) -> Result { self.write(::quick_xml::Writer::new_with_indent( writer, indent_char, indent_size, )) } } impl Display for Channel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let buf = self.write_to(Vec::new()).unwrap_or_default(); // this unwrap should be safe since the bytes written from the Channel are all valid utf8 f.write_str(String::from_utf8(buf).unwrap().as_str()) } } impl Channel { /// Builds a Channel from source XML pub fn from_xml( namespaces: &BTreeMap, reader: &mut Reader, atts: Attributes, ) -> Result { let mut channel = Channel::default(); let mut extensions = ExtensionMap::new(); let mut buf = Vec::new(); let mut skip_buf = Vec::new(); let namespaces = read_namespace_declarations(reader, atts, namespaces)?; loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => match decode(element.name().as_ref(), reader)?.as_ref() { "category" => { let category = Category::from_xml(reader, element.attributes())?; channel.categories.push(category); } "cloud" => { let cloud = Cloud::from_xml(reader, &element)?; channel.cloud = Some(cloud); } "image" => { let image = Image::from_xml(reader, element.attributes())?; channel.image = Some(image); } "textInput" => { let text_input = TextInput::from_xml(reader, element.attributes())?; channel.text_input = Some(text_input); } "item" => { let item = Item::from_xml(namespaces.as_ref(), reader, element.attributes())?; channel.items.push(item); } "title" => { if let Some(content) = element_text(reader)? { channel.title = content; } } "link" => { if let Some(content) = element_text(reader)? { channel.link = content; } } "description" => { if let Some(content) = element_text(reader)? { channel.description = content; } } "language" => channel.language = element_text(reader)?, "copyright" => channel.copyright = element_text(reader)?, "managingEditor" => { channel.managing_editor = element_text(reader)?; } "webMaster" => channel.webmaster = element_text(reader)?, "pubDate" => channel.pub_date = element_text(reader)?, "lastBuildDate" => { channel.last_build_date = element_text(reader)?; } "generator" => channel.generator = element_text(reader)?, "rating" => channel.rating = element_text(reader)?, "docs" => channel.docs = element_text(reader)?, "ttl" => channel.ttl = element_text(reader)?, "skipHours" => loop { skip_buf.clear(); match reader.read_event_into(&mut skip_buf)? { Event::Start(element) => { if decode(element.name().as_ref(), reader)?.as_ref() == "hour" { if let Some(content) = element_text(reader)? { channel.skip_hours.push(content); } } else { skip(element.name(), reader)?; } } Event::End(_) | Event::Eof => break, _ => {} } }, "skipDays" => loop { skip_buf.clear(); match reader.read_event_into(&mut skip_buf)? { Event::Start(element) => { if decode(element.name().as_ref(), reader)?.as_ref() == "day" { if let Some(content) = element_text(reader)? { channel.skip_days.push(content); } } else { skip(element.name(), reader)?; } } Event::End(_) | Event::Eof => break, _ => {} } }, n => { if let Some((prefix, name)) = extension_name(n) { let scope_namespases = read_namespace_declarations( reader, element.attributes(), namespaces.as_ref(), )?; let ext_ns = scope_namespases.get(prefix).map(|s| s.as_str()); let ext = parse_extension_element(reader, element.attributes())?; match ext_ns { #[cfg(feature = "atom")] Some(ns @ atom::NAMESPACE) => { extension_entry(&mut extensions, ns, name).push(ext); } Some(ns) if is_itunes_namespace(ns) => { extension_entry(&mut extensions, itunes::NAMESPACE, name) .push(ext); } Some(ns @ dublincore::NAMESPACE) | Some(ns @ syndication::NAMESPACE) => { extension_entry(&mut extensions, ns, name).push(ext); } _ => { extension_entry(&mut channel.extensions, prefix, name).push(ext) } } } else { skip(element.name(), reader)?; } } }, Event::End(_) => break, Event::Eof => return Err(Error::Eof), _ => {} } buf.clear(); } // Process each of the namespaces we know #[cfg(feature = "atom")] if let Some(v) = extensions.remove(atom::NAMESPACE) { channel.atom_ext = Some(atom::AtomExtension::from_map(v)); } if let Some(v) = extensions.remove(itunes::NAMESPACE) { channel.itunes_ext = Some(itunes::ITunesChannelExtension::from_map(v)); } if let Some(v) = extensions.remove(dublincore::NAMESPACE) { channel.dublin_core_ext = Some(dublincore::DublinCoreExtension::from_map(v)); } if let Some(v) = extensions.remove(syndication::NAMESPACE) { channel.syndication_ext = Some(syndication::SyndicationExtension::from_map(v)); } Ok(channel) } } impl ToXml for Channel { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "channel"; writer.write_event(Event::Start(BytesStart::new(name)))?; writer.write_text_element("title", &self.title)?; writer.write_text_element("link", &self.link)?; writer.write_text_element("description", &self.description)?; if let Some(language) = self.language.as_ref() { writer.write_text_element("language", language)?; } if let Some(copyright) = self.copyright.as_ref() { writer.write_text_element("copyright", copyright)?; } if let Some(managing_editor) = self.managing_editor.as_ref() { writer.write_text_element("managingEditor", managing_editor)?; } if let Some(webmaster) = self.webmaster.as_ref() { writer.write_text_element("webMaster", webmaster)?; } if let Some(pub_date) = self.pub_date.as_ref() { writer.write_text_element("pubDate", pub_date)?; } if let Some(last_build_date) = self.last_build_date.as_ref() { writer.write_text_element("lastBuildDate", last_build_date)?; } writer.write_objects(&self.categories)?; if let Some(generator) = self.generator.as_ref() { writer.write_text_element("generator", generator)?; } if let Some(rating) = self.rating.as_ref() { writer.write_text_element("rating", rating)?; } if let Some(docs) = self.docs.as_ref() { writer.write_text_element("docs", docs)?; } if let Some(cloud) = self.cloud.as_ref() { writer.write_object(cloud)?; } if let Some(ttl) = self.ttl.as_ref() { writer.write_text_element("ttl", ttl)?; } if let Some(image) = self.image.as_ref() { writer.write_object(image)?; } if let Some(text_input) = self.text_input.as_ref() { writer.write_object(text_input)?; } if !self.skip_hours.is_empty() { let name = "skipHours"; writer.write_event(Event::Start(BytesStart::new(name)))?; for hour in &self.skip_hours { writer.write_text_element("hour", hour)?; } writer.write_event(Event::End(BytesEnd::new(name)))?; } if !self.skip_days.is_empty() { let name = "skipDays"; writer.write_event(Event::Start(BytesStart::new(name)))?; for day in &self.skip_days { writer.write_text_element("day", day)?; } writer.write_event(Event::End(BytesEnd::new(name)))?; } for map in self.extensions.values() { for extensions in map.values() { for extension in extensions { extension.to_xml(writer)?; } } } #[cfg(feature = "atom")] if let Some(ext) = &self.atom_ext { ext.to_xml(writer)?; } if let Some(ext) = &self.itunes_ext { ext.to_xml(writer)?; } if let Some(ext) = &self.dublin_core_ext { ext.to_xml(writer)?; } if let Some(ext) = &self.syndication_ext { ext.to_xml(&self.namespaces, writer)?; } writer.write_objects(&self.items)?; writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } fn used_namespaces(&self) -> BTreeMap { let mut namespaces = BTreeMap::new(); for item in &self.items { namespaces.extend(item.used_namespaces()); } if let Some(ext) = self.itunes_ext() { namespaces.extend(ext.used_namespaces()); } if let Some(ext) = self.dublin_core_ext() { namespaces.extend(ext.used_namespaces()); } #[cfg(feature = "atom")] if let Some(ext) = self.atom_ext() { namespaces.extend(ext.used_namespaces()); } namespaces } } impl FromStr for Channel { type Err = Error; #[inline] fn from_str(s: &str) -> Result { Channel::read_from(s.as_bytes()) } } #[cfg(feature = "builders")] impl ChannelBuilder { /// Builds a new `Channel`. pub fn build(&self) -> Channel { self.build_impl().unwrap() } } rss-2.0.11/src/cloud.rs000064400000000000000000000143711046102023000127650ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::{BufRead, Write}; use quick_xml::events::{BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::error::Error; use crate::toxml::ToXml; use crate::util::{attr_value, decode, skip}; /// Represents a cloud in an RSS feed. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Cloud { /// The domain to register with. pub domain: String, /// The port to register with. pub port: String, /// The path to register with. pub path: String, /// The procedure to register with. pub register_procedure: String, /// The protocol to register with. pub protocol: String, } impl Cloud { /// Return the domain for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_domain("http://example.com"); /// assert_eq!(cloud.domain(), "http://example.com"); /// ``` pub fn domain(&self) -> &str { self.domain.as_str() } /// Set the domain for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_domain("http://example.com"); /// ``` pub fn set_domain(&mut self, domain: V) where V: Into, { self.domain = domain.into(); } /// Return the port for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_port("80"); /// assert_eq!(cloud.port(), "80"); /// ``` pub fn port(&self) -> &str { self.port.as_str() } /// Set the port for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_port("80"); /// ``` pub fn set_port(&mut self, port: V) where V: Into, { self.port = port.into(); } /// Return the path for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_port("/rpc"); /// assert_eq!(cloud.port(), "/rpc"); /// ``` pub fn path(&self) -> &str { self.path.as_str() } /// Set the path for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_path("/rpc"); /// ``` pub fn set_path(&mut self, path: V) where V: Into, { self.path = path.into(); } /// Return the register procedure for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_register_procedure("pingMe"); /// assert_eq!(cloud.register_procedure(), "pingMe"); /// ``` pub fn register_procedure(&self) -> &str { self.register_procedure.as_str() } /// Set the register procedure for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_register_procedure("pingMe"); /// ``` pub fn set_register_procedure(&mut self, register_procedure: V) where V: Into, { self.register_procedure = register_procedure.into(); } /// Return the protocol for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_protocol("xml-rpc"); /// assert_eq!(cloud.protocol(), "xml-rpc"); /// ``` pub fn protocol(&self) -> &str { self.protocol.as_str() } /// Set the protocol for this cloud. /// /// # Examples /// /// ``` /// use rss::Cloud; /// /// let mut cloud = Cloud::default(); /// cloud.set_protocol("xml-rpc"); /// ``` pub fn set_protocol(&mut self, protocol: V) where V: Into, { self.protocol = protocol.into(); } } impl Cloud { /// Builds a Cloud from source XML pub fn from_xml<'s, R: BufRead>( reader: &mut Reader, element: &'s BytesStart<'s>, ) -> Result { let mut cloud = Cloud::default(); for att in element.attributes().with_checks(false).flatten() { match decode(att.key.as_ref(), reader)?.as_ref() { "domain" => cloud.domain = attr_value(&att, reader)?.to_string(), "port" => cloud.port = attr_value(&att, reader)?.to_string(), "path" => cloud.path = attr_value(&att, reader)?.to_string(), "registerProcedure" => { cloud.register_procedure = attr_value(&att, reader)?.to_string() } "protocol" => cloud.protocol = attr_value(&att, reader)?.to_string(), _ => {} } } skip(element.name(), reader)?; Ok(cloud) } } impl ToXml for Cloud { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "cloud"; let mut element = BytesStart::new(name); element.push_attribute(("domain", self.domain.as_str())); element.push_attribute(("port", self.port.as_str())); element.push_attribute(("path", self.path.as_str())); element.push_attribute(("registerProcedure", self.register_procedure.as_str())); element.push_attribute(("protocol", self.protocol.as_str())); writer.write_event(Event::Empty(element))?; Ok(()) } } #[cfg(feature = "builders")] impl CloudBuilder { /// Builds a new `Cloud`. pub fn build(&self) -> Cloud { self.build_impl().unwrap() } } rss-2.0.11/src/enclosure.rs000064400000000000000000000111361046102023000136520ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::{BufRead, Write}; use quick_xml::events::{BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::error::Error; use crate::toxml::ToXml; use crate::util::{attr_value, decode, skip}; /// Represents an enclosure in an RSS item. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Enclosure { /// The URL of the enclosure. pub url: String, /// The length of the enclosure in bytes. pub length: String, /// The MIME type of the enclosure. pub mime_type: String, } impl Enclosure { /// Return the URL of this enclosure. /// /// # Examples /// /// ``` /// use rss::Enclosure; /// /// let mut enclosure = Enclosure::default(); /// enclosure.set_url("http://example.com/audio.mp3"); /// assert_eq!(enclosure.url(), "http://example.com/audio.mp3"); /// ``` pub fn url(&self) -> &str { self.url.as_str() } /// Set the URL of this enclosure. /// /// # Examples /// /// ``` /// use rss::Enclosure; /// /// let mut enclosure = Enclosure::default(); /// enclosure.set_url("http://example.com/audio.mp3"); /// ``` pub fn set_url(&mut self, url: V) where V: Into, { self.url = url.into(); } /// Return the content length of this enclosure. /// /// # Examples /// /// ``` /// use rss::Enclosure; /// /// let mut enclosure = Enclosure::default(); /// enclosure.set_length("1000"); /// assert_eq!(enclosure.length(), "1000"); /// ``` pub fn length(&self) -> &str { self.length.as_str() } /// Set the content length of this enclosure. /// /// # Examples /// /// ``` /// use rss::Enclosure; /// /// let mut enclosure = Enclosure::default(); /// enclosure.set_length("1000"); /// ``` pub fn set_length(&mut self, length: V) where V: Into, { self.length = length.into(); } /// Return the MIME type of this enclosure. /// /// # Examples /// /// ``` /// use rss::Enclosure; /// /// let mut enclosure = Enclosure::default(); /// enclosure.set_mime_type("audio/mpeg"); /// assert_eq!(enclosure.mime_type(), "audio/mpeg"); /// ``` pub fn mime_type(&self) -> &str { self.mime_type.as_str() } /// Set the MIME type of this enclosure. /// /// # Examples /// /// ``` /// use rss::Enclosure; /// /// let mut enclosure = Enclosure::default(); /// enclosure.set_mime_type("audio/mpeg"); /// ``` pub fn set_mime_type(&mut self, mime_type: V) where V: Into, { self.mime_type = mime_type.into(); } } impl Enclosure { /// Builds an Enclosure from source XML pub fn from_xml<'s, R: BufRead>( reader: &mut Reader, element: &'s BytesStart<'s>, ) -> Result { let mut enclosure = Enclosure::default(); for attr in element.attributes().with_checks(false).flatten() { match decode(attr.key.as_ref(), reader)?.as_ref() { "url" => enclosure.url = attr_value(&attr, reader)?.to_string(), "length" => enclosure.length = attr_value(&attr, reader)?.to_string(), "type" => enclosure.mime_type = attr_value(&attr, reader)?.to_string(), _ => {} } } skip(element.name(), reader)?; Ok(enclosure) } } impl ToXml for Enclosure { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "enclosure"; let mut element = BytesStart::new(name); element.push_attribute(("url", self.url.as_str())); element.push_attribute(("length", self.length.as_str())); element.push_attribute(("type", self.mime_type.as_str())); writer.write_event(Event::Empty(element))?; Ok(()) } } #[cfg(feature = "builders")] impl EnclosureBuilder { /// Builds a new `Enclosure`. pub fn build(&self) -> Enclosure { self.build_impl().unwrap() } } rss-2.0.11/src/error.rs000064400000000000000000000036721046102023000130120ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::error::Error as StdError; use std::fmt; use std::io; use std::str::Utf8Error; use std::sync::Arc; use quick_xml::Error as XmlError; #[derive(Debug)] /// Errors that occur during parsing. pub enum Error { /// An error while converting bytes to UTF8. Utf8(Utf8Error), /// An XML parsing error. Xml(XmlError), /// The input didn't begin with an opening `` tag. InvalidStartTag, /// The end of the input was reached without finding a complete channel element. Eof, } impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match *self { Error::Utf8(ref err) => Some(err), Error::Xml(ref err) => Some(err), Error::InvalidStartTag | Error::Eof => None, } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Error::Utf8(ref err) => fmt::Display::fmt(err, f), Error::Xml(ref err) => fmt::Display::fmt(err, f), Error::InvalidStartTag => write!(f, "the input did not begin with an rss tag"), Error::Eof => write!(f, "reached end of input without finding a complete channel"), } } } impl From for Error { fn from(err: XmlError) -> Error { Error::Xml(err) } } impl From for Error { fn from(err: quick_xml::encoding::EncodingError) -> Error { Error::Xml(XmlError::Encoding(err)) } } impl From for Error { fn from(err: io::Error) -> Error { Error::Xml(XmlError::Io(Arc::new(err))) } } impl From for Error { fn from(err: Utf8Error) -> Error { Error::Utf8(err) } } rss-2.0.11/src/extension/atom.rs000064400000000000000000000112651046102023000146320ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2020 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::io::Write; pub use atom_syndication::Link; use quick_xml::events::{BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Writer; use crate::extension::Extension; use crate::toxml::ToXml; /// The Atom XML namespace. pub const NAMESPACE: &str = "http://www.w3.org/2005/Atom"; /// An Atom element extension. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct AtomExtension { /// Links #[cfg_attr(feature = "builders", builder(setter(each = "link")))] pub links: Vec, } impl AtomExtension { /// Retrieve links pub fn links(&self) -> &[Link] { &self.links } /// Set links pub fn set_links(&mut self, links: V) where V: Into>, { self.links = links.into(); } } impl AtomExtension { /// Creates an `AtomExtension` using the specified `BTreeMap`. pub fn from_map(mut map: BTreeMap>) -> Self { let links = map .remove("link") .unwrap_or_default() .into_iter() .filter_map(|mut link_ext| { Some(Link { href: link_ext.attrs.remove("href")?, rel: link_ext .attrs .remove("rel") .unwrap_or_else(|| Link::default().rel), hreflang: link_ext.attrs.remove("hreflang"), mime_type: link_ext.attrs.remove("type"), title: link_ext.attrs.remove("title"), length: link_ext.attrs.remove("length"), }) }) .collect(); Self { links } } } impl ToXml for AtomExtension { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { for link in &self.links { let mut element = BytesStart::new("atom:link"); element.push_attribute(("href", &*link.href)); element.push_attribute(("rel", &*link.rel)); if let Some(ref hreflang) = link.hreflang { element.push_attribute(("hreflang", &**hreflang)); } if let Some(ref mime_type) = link.mime_type { element.push_attribute(("type", &**mime_type)); } if let Some(ref title) = link.title { element.push_attribute(("title", &**title)); } if let Some(ref length) = link.length { element.push_attribute(("length", &**length)); } writer.write_event(Event::Empty(element))?; } Ok(()) } fn used_namespaces(&self) -> BTreeMap { let mut namespaces = BTreeMap::new(); namespaces.insert("atom".to_owned(), NAMESPACE.to_owned()); namespaces } } #[cfg(feature = "builders")] impl AtomExtensionBuilder { /// Builds a new `AtomExtension`. pub fn build(&self) -> AtomExtension { self.build_impl().unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "builders")] #[cfg(feature = "atom")] fn test_builder() { use atom_syndication::LinkBuilder; assert_eq!( AtomExtensionBuilder::default() .link( LinkBuilder::default() .rel("self") .href("http://example.com/feed") .build(), ) .link( LinkBuilder::default() .rel("alternate") .href("http://example.com") .build(), ) .build(), AtomExtension { links: vec![ Link { rel: "self".to_string(), href: "http://example.com/feed".to_string(), ..Default::default() }, Link { rel: "alternate".to_string(), href: "http://example.com".to_string(), ..Default::default() } ] } ); } } rss-2.0.11/src/extension/dublincore.rs000064400000000000000000000333071046102023000160210ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::io::Write; use quick_xml::Error as XmlError; use quick_xml::Writer; use crate::extension::util::get_extension_values; use crate::extension::Extension; use crate::toxml::{ToXml, WriterExt}; /// The Dublin Core XML namespace. pub const NAMESPACE: &str = "http://purl.org/dc/elements/1.1/"; /// A Dublin Core element extension. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct DublinCoreExtension { /// An entity responsible for making contributions to the resource. #[cfg_attr(feature = "builders", builder(setter(each = "contributor")))] pub contributors: Vec, /// The spatial or temporal topic of the resource, the spatial applicability of the resource, /// or the jurisdiction under which the resource is relevant. #[cfg_attr(feature = "builders", builder(setter(each = "coverage")))] pub coverages: Vec, /// An entity primarily responsible for making the resource. #[cfg_attr(feature = "builders", builder(setter(each = "creator")))] pub creators: Vec, /// A point or period of time associated with an event in the lifecycle of the resource. #[cfg_attr(feature = "builders", builder(setter(each = "date")))] pub dates: Vec, /// An account of the resource. #[cfg_attr(feature = "builders", builder(setter(each = "description")))] pub descriptions: Vec, /// The file format, physical medium, or dimensions of the resource. #[cfg_attr(feature = "builders", builder(setter(each = "format")))] pub formats: Vec, /// An unambiguous reference to the resource within a given context. #[cfg_attr(feature = "builders", builder(setter(each = "identifier")))] pub identifiers: Vec, /// A language of the resource. #[cfg_attr(feature = "builders", builder(setter(each = "language")))] pub languages: Vec, /// An entity responsible for making the resource available. #[cfg_attr(feature = "builders", builder(setter(each = "publisher")))] pub publishers: Vec, /// A related resource. #[cfg_attr(feature = "builders", builder(setter(each = "relation")))] pub relations: Vec, /// Information about rights held in and over the resource. #[cfg_attr(feature = "builders", builder(setter(each = "right")))] pub rights: Vec, /// A related resource from which the described resource is derived. #[cfg_attr(feature = "builders", builder(setter(each = "source")))] pub sources: Vec, /// The topic of the resource. #[cfg_attr(feature = "builders", builder(setter(each = "subject")))] pub subjects: Vec, /// A name given to the resource. #[cfg_attr(feature = "builders", builder(setter(each = "title")))] pub titles: Vec, /// The nature or genre of the resource. #[cfg_attr(feature = "builders", builder(setter(each = "r#type")))] pub types: Vec, } impl DublinCoreExtension { /// Return the contributors to the resource. pub fn contributors(&self) -> &[String] { &self.contributors } /// Return a mutable slice of the contributors to the resource. pub fn contributors_mut(&mut self) -> &mut [String] { &mut self.contributors } /// Set the contributors to the resource. pub fn set_contributors(&mut self, contributors: V) where V: Into>, { self.contributors = contributors.into(); } /// Return the spatial or temporal topics of the resource, the spatial applicabilities of the /// resource, or the jurisdictions under which the resource is relevant. pub fn coverages(&self) -> &[String] { &self.coverages } /// Return a mutable slice of the spatial or temporal topics of the resource, the spatial /// applicabilities of the resource, or the jurisdictions under which the resource is relevant. pub fn coverages_mut(&mut self) -> &mut [String] { &mut self.coverages } /// Set the spatial or temporal topics of the resource, the spatial applicabilities of the /// resource, or the jurisdictions under which the resource is relevant. pub fn set_coverages(&mut self, coverages: V) where V: Into>, { self.coverages = coverages.into(); } /// Return the creators of the resource. pub fn creators(&self) -> &[String] { &self.creators } /// Return a mutable slice of the creators of the resource. pub fn creators_mut(&mut self) -> &mut [String] { &mut self.creators } /// Set the creators of the resource. pub fn set_creators(&mut self, creators: V) where V: Into>, { self.creators = creators.into(); } /// Return the times associated with the resource. pub fn dates(&self) -> &[String] { &self.dates } /// Return a mutable slice of the times associated with the resource. pub fn dates_mut(&mut self) -> &mut [String] { &mut self.dates } /// Set the times associated with the resource. pub fn set_dates(&mut self, dates: V) where V: Into>, { self.dates = dates.into(); } /// Return the descriptions of the resource. pub fn descriptions(&self) -> &[String] { &self.descriptions } /// Return a mutable slice of the descriptions of the resource. pub fn descriptions_mut(&mut self) -> &mut [String] { &mut self.descriptions } /// Set the descriptions of the resource. pub fn set_descriptions(&mut self, descriptions: V) where V: Into>, { self.descriptions = descriptions.into(); } /// Return the file formats, physical mediums, or dimensions of the resource. pub fn formats(&self) -> &[String] { &self.formats } /// Return a mutable slice of the file formats, physical mediums, or /// dimensions of the resource. pub fn formats_mut(&mut self) -> &mut [String] { &mut self.formats } /// Set the file formats, physical mediums, or dimensions of the resource. pub fn set_formats(&mut self, formats: V) where V: Into>, { self.formats = formats.into(); } /// Return the identifiers of the resource. pub fn identifiers(&self) -> &[String] { &self.identifiers } /// Return a mutable slice of the identifiers of the resource. pub fn identifiers_mut(&mut self) -> &mut [String] { &mut self.identifiers } /// Set the identifiers of the resource. pub fn set_identifiers(&mut self, identifiers: V) where V: Into>, { self.identifiers = identifiers.into(); } /// Return the languages of the resource. pub fn languages(&self) -> &[String] { &self.languages } /// Return a mutable slice of the languages of the resource. pub fn languages_mut(&mut self) -> &mut [String] { &mut self.languages } /// Set the languages of the resource. pub fn set_languages(&mut self, languages: V) where V: Into>, { self.languages = languages.into(); } /// Return the publishers of the resource. pub fn publishers(&self) -> &[String] { &self.publishers } /// Return a mutable slice of the publishers of the resource. pub fn publishers_mut(&mut self) -> &mut [String] { &mut self.publishers } /// Set the publishers of the resource. pub fn set_publishers(&mut self, publishers: V) where V: Into>, { self.publishers = publishers.into(); } /// Return the related resources. pub fn relations(&self) -> &[String] { &self.relations } /// Return a mutable slice of the related resources. pub fn relations_mut(&mut self) -> &mut [String] { &mut self.relations } /// Set the related resources. pub fn set_relations(&mut self, relations: V) where V: Into>, { self.relations = relations.into(); } /// Return the information about rights held in and over the resource. pub fn rights(&self) -> &[String] { &self.rights } /// Return a mutable slice of the information about rights held in and over /// the resource. pub fn rights_mut(&mut self) -> &mut [String] { &mut self.rights } /// Set the information about rights held in and over the resource. pub fn set_rights(&mut self, rights: V) where V: Into>, { self.rights = rights.into(); } /// Return the sources of the resource. pub fn sources(&self) -> &[String] { &self.sources } /// Return a mutable slice of the sources of the resource. pub fn sources_mut(&mut self) -> &mut [String] { &mut self.sources } /// Set the sources of the resource. pub fn set_sources(&mut self, sources: V) where V: Into>, { self.sources = sources.into(); } /// Return the topics of the resource. pub fn subjects(&self) -> &[String] { &self.subjects } /// Return a mutable slice of the subjects of the resource. pub fn subjects_mut(&mut self) -> &mut [String] { &mut self.subjects } /// Set the topics of the resource. pub fn set_subjects(&mut self, subjects: V) where V: Into>, { self.subjects = subjects.into(); } /// Return the titles of the resource. pub fn titles(&self) -> &[String] { &self.titles } /// Return a mutable slice of the titles of the resource. pub fn titles_mut(&mut self) -> &mut [String] { &mut self.titles } /// Set the titles of the resource. pub fn set_titles(&mut self, titles: V) where V: Into>, { self.titles = titles.into(); } /// Return the natures or genres of the resource. pub fn types(&self) -> &[String] { &self.types } /// Return a mutable slice of the natures or genres of the resource. pub fn types_mut(&mut self) -> &mut [String] { &mut self.types } /// set the natures or genres of the resource. pub fn set_types(&mut self, types: V) where V: Into>, { self.types = types.into(); } } impl DublinCoreExtension { /// Creates a `DublinCoreExtension` using the specified `BTreeMap`. pub fn from_map(map: BTreeMap>) -> Self { let mut ext = DublinCoreExtension::default(); for (key, v) in map { match key.as_str() { "contributor" => ext.contributors = get_extension_values(v), "coverage" => ext.coverages = get_extension_values(v), "creator" => ext.creators = get_extension_values(v), "date" => ext.dates = get_extension_values(v), "description" => ext.descriptions = get_extension_values(v), "format" => ext.formats = get_extension_values(v), "identifier" => ext.identifiers = get_extension_values(v), "language" => ext.languages = get_extension_values(v), "publisher" => ext.publishers = get_extension_values(v), "relation" => ext.relations = get_extension_values(v), "rights" => ext.rights = get_extension_values(v), "source" => ext.sources = get_extension_values(v), "subject" => ext.subjects = get_extension_values(v), "title" => ext.titles = get_extension_values(v), "type" => ext.types = get_extension_values(v), _ => {} } } ext } } impl ToXml for DublinCoreExtension { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { writer.write_text_elements("dc:contributor", &self.contributors)?; writer.write_text_elements("dc:coverage", &self.coverages)?; writer.write_text_elements("dc:creator", &self.creators)?; writer.write_text_elements("dc:date", &self.dates)?; writer.write_text_elements("dc:description", &self.descriptions)?; writer.write_text_elements("dc:format", &self.formats)?; writer.write_text_elements("dc:identifier", &self.identifiers)?; writer.write_text_elements("dc:language", &self.languages)?; writer.write_text_elements("dc:publisher", &self.publishers)?; writer.write_text_elements("dc:relation", &self.relations)?; writer.write_text_elements("dc:rights", &self.rights)?; writer.write_text_elements("dc:source", &self.sources)?; writer.write_text_elements("dc:subject", &self.subjects)?; writer.write_text_elements("dc:title", &self.titles)?; writer.write_text_elements("dc:type", &self.types)?; Ok(()) } fn used_namespaces(&self) -> BTreeMap { let mut namespaces = BTreeMap::new(); namespaces.insert("dc".to_owned(), NAMESPACE.to_owned()); namespaces } } #[cfg(feature = "builders")] impl DublinCoreExtensionBuilder { /// Builds a new `DublinCoreExtension`. pub fn build(&self) -> DublinCoreExtension { self.build_impl().unwrap() } } rss-2.0.11/src/extension/itunes/itunes_category.rs000064400000000000000000000103141046102023000203770ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::Write; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Writer; use crate::toxml::ToXml; /// A category for an iTunes podcast. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct ITunesCategory { /// The name of the category. pub text: String, // This is contained within a Box to ensure it gets allocated on the heap to prevent an // infinite size. /// An optional subcategory for the category. pub subcategory: Option>, } impl ITunesCategory { /// Return the name of this category. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesCategory; /// /// let mut category = ITunesCategory::default(); /// category.set_text("Technology"); /// assert_eq!(category.text(), "Technology") /// ``` pub fn text(&self) -> &str { self.text.as_str() } /// Set the name of this category. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesCategory; /// /// let mut category = ITunesCategory::default(); /// category.set_text("Technology"); /// ``` pub fn set_text(&mut self, text: V) where V: Into, { self.text = text.into(); } /// Return the subcategory for this category. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesCategory; /// /// let mut category = ITunesCategory::default(); /// category.set_subcategory(Box::new(ITunesCategory::default())); /// assert!(category.subcategory().is_some()); /// ``` pub fn subcategory(&self) -> Option<&ITunesCategory> { self.subcategory.as_deref() } /// Set the subcategory for this category. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesCategory; /// /// let mut category = ITunesCategory::default(); /// category.set_subcategory(Box::new(ITunesCategory::default())); /// ``` pub fn set_subcategory(&mut self, subcategory: V) where V: Into>>, { self.subcategory = subcategory.into(); } } impl ToXml for ITunesCategory { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "itunes:category"; let mut element = BytesStart::new(name); element.push_attribute(("text", &*self.text)); writer.write_event(Event::Start(element))?; if let Some(subcategory) = self.subcategory.as_ref() { subcategory.to_xml(writer)?; } writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } } #[cfg(feature = "builders")] impl ITunesCategoryBuilder { /// Builds a new `ITunesCategory`. pub fn build(&self) -> ITunesCategory { self.build_impl().unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "builders")] fn test_builder() { assert_eq!( ITunesCategoryBuilder::default().text("music").build(), ITunesCategory { text: "music".to_string(), subcategory: None, } ); assert_eq!( ITunesCategoryBuilder::default() .text("music") .subcategory(Some(Box::new( ITunesCategoryBuilder::default().text("pop").build() ))) .build(), ITunesCategory { text: "music".to_string(), subcategory: Some(Box::new(ITunesCategory { text: "pop".to_string(), subcategory: None, })), } ); } } rss-2.0.11/src/extension/itunes/itunes_channel_extension.rs000064400000000000000000000447711046102023000223040ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::io::Write; use quick_xml::events::{BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Writer; use super::{parse_categories, parse_image, parse_owner, NAMESPACE}; use crate::extension::itunes::{ITunesCategory, ITunesOwner}; use crate::extension::util::remove_extension_value; use crate::extension::Extension; use crate::toxml::{ToXml, WriterExt}; /// An iTunes channel element extension. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct ITunesChannelExtension { /// The author of the podcast. pub author: Option, /// Specifies if the podcast should be prevented from appearing in the iTunes Store. A value of /// `Yes` indicates that the podcast should not show up in the iTunes Store. All other values /// are ignored. pub block: Option, /// The iTunes categories the podcast belongs to. #[cfg_attr(feature = "builders", builder(setter(each = "category")))] pub categories: Vec, /// The artwork for the podcast. pub image: Option, /// Specifies whether the podcast contains explicit content. A value of `Yes`, `Explicit`, or /// `True` indicates that the podcast contains explicit content. A value of `Clean`, `No`, /// `False` indicates that none of the episodes contain explicit content. pub explicit: Option, /// Specifies whether the podcast is complete and no new episodes will be posted. A value of /// `Yes` indicates that the podcast is complete. pub complete: Option, /// The new URL where the podcast is located. pub new_feed_url: Option, /// The contact information for the owner of the podcast. pub owner: Option, /// A description of the podcast. pub subtitle: Option, /// A summary of the podcast. pub summary: Option, /// Keywords for the podcast. The string contains a comma separated list of keywords. pub keywords: Option, /// The type of the podcast. Usually `serial` or `episodic`. pub r#type: Option, } impl ITunesChannelExtension { /// Return the author of this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_author("John Doe".to_string()); /// assert_eq!(extension.author(), Some("John Doe")); /// ``` pub fn author(&self) -> Option<&str> { self.author.as_deref() } /// Set the author of this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_author("John Doe".to_string()); /// ``` pub fn set_author(&mut self, author: V) where V: Into>, { self.author = author.into(); } /// Return whether the podcast should be blocked from appearing in the iTunes Store. /// /// A value of `Yes` indicates that the podcast should not show up in the iTunes Store. All /// other values are ignored. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_block("Yes".to_string()); /// assert_eq!(extension.block(), Some("Yes")); /// ``` pub fn block(&self) -> Option<&str> { self.block.as_deref() } /// Set whether the podcast should be blocked from appearing in the iTunes Store. /// /// A value of `Yes` indicates that the podcast should not show up in the iTunes Store. All /// other values are ignored. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_block("Yes".to_string()); /// ``` pub fn set_block(&mut self, block: V) where V: Into>, { self.block = block.into(); } /// Return the iTunes categories that the podcast belongs to. /// /// # Examples /// /// ``` /// use rss::extension::itunes::{ITunesCategory, ITunesChannelExtension}; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_categories(vec![ITunesCategory::default()]); /// assert_eq!(extension.categories().len(), 1); /// ``` pub fn categories(&self) -> &[ITunesCategory] { &self.categories } /// Return a mutable slice of the iTunes categories that the podcast belongs to. pub fn categories_mut(&mut self) -> &mut [ITunesCategory] { &mut self.categories } /// Set the iTunes categories that the podcast belongs to. /// /// # Examples /// /// ``` /// use rss::extension::itunes::{ITunesCategory, ITunesChannelExtension}; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_categories(vec![ITunesCategory::default()]); /// ``` pub fn set_categories(&mut self, categories: V) where V: Into>, { self.categories = categories.into(); } /// Return the artwork URL for the podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_image("http://example.com/artwork.png".to_string()); /// assert_eq!(extension.image(), Some("http://example.com/artwork.png")); /// ``` pub fn image(&self) -> Option<&str> { self.image.as_deref() } /// Set the artwork URL for the podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_image("http://example.com/artwork.png".to_string()); /// ``` pub fn set_image(&mut self, image: V) where V: Into>, { self.image = image.into(); } /// Return whether the podcast contains explicit content. /// /// A value of `Yes`, `Explicit`, or `True` indicates that the podcast contains explicit /// content. A value of `Clean`, `No`, `False` indicates that none of the episodes contain /// explicit content. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_explicit("Yes".to_string()); /// assert_eq!(extension.explicit(), Some("Yes")); /// ``` pub fn explicit(&self) -> Option<&str> { self.explicit.as_deref() } /// Set whether the podcast contains explicit content. /// /// A value of `Yes`, `Explicit`, or `True` indicates that the podcast contains explicit /// content. A value of `Clean`, `No`, `False` indicates that none of the episodes contain /// explicit content. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_explicit("Yes".to_string()); /// ``` pub fn set_explicit(&mut self, explicit: V) where V: Into>, { self.explicit = explicit.into(); } /// Return whether the podcast is complete and no new episodes will be posted. /// /// A value of `Yes` indicates that the podcast is complete. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_complete("Yes".to_string()); /// assert_eq!(extension.complete(), Some("Yes")); /// ``` pub fn complete(&self) -> Option<&str> { self.complete.as_deref() } /// Set whether the podcast is complete and no new episodes will be posted. /// /// A value of `Yes` indicates that the podcast is complete. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_complete("Yes".to_string()); /// ``` pub fn set_complete(&mut self, complete: V) where V: Into>, { self.complete = complete.into(); } /// Return the new feed URL for this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_new_feed_url("http://example.com/feed".to_string()); /// assert_eq!(extension.new_feed_url(), Some("http://example.com/feed")); /// ``` pub fn new_feed_url(&self) -> Option<&str> { self.new_feed_url.as_deref() } /// Set the new feed URL for this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_new_feed_url("http://example.com/feed".to_string()); /// ``` pub fn set_new_feed_url(&mut self, new_feed_url: V) where V: Into>, { self.new_feed_url = new_feed_url.into(); } /// Return the contact information for the owner of this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::{ITunesChannelExtension, ITunesOwner}; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_owner(ITunesOwner::default()); /// assert!(extension.owner().is_some()); /// ``` pub fn owner(&self) -> Option<&ITunesOwner> { self.owner.as_ref() } /// Set the contact information for the owner of this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::{ITunesChannelExtension, ITunesOwner}; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_owner(ITunesOwner::default()); /// ``` pub fn set_owner(&mut self, owner: V) where V: Into>, { self.owner = owner.into(); } /// Return the description of this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_subtitle("A podcast".to_string()); /// assert_eq!(extension.subtitle(), Some("A podcast")); /// ``` pub fn subtitle(&self) -> Option<&str> { self.subtitle.as_deref() } /// Set the description of this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_subtitle("A podcast".to_string()); /// ``` pub fn set_subtitle(&mut self, subtitle: V) where V: Into>, { self.subtitle = subtitle.into(); } /// Return the summary for this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_summary("A podcast".to_string()); /// assert_eq!(extension.summary(), Some("A podcast")); /// ``` pub fn summary(&self) -> Option<&str> { self.summary.as_deref() } /// Set the summary for this podcast. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_summary("A podcast about technology".to_string()); /// ``` pub fn set_summary(&mut self, summary: V) where V: Into>, { self.summary = summary.into(); } /// Return the keywords for this podcast. /// /// A comma separated list of keywords. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_keywords("technology".to_string()); /// assert_eq!(extension.keywords(), Some("technology")); /// ``` pub fn keywords(&self) -> Option<&str> { self.keywords.as_deref() } /// Set the keywords for this podcast. /// /// A comma separated list of keywords. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_keywords("technology".to_string()); /// ``` pub fn set_keywords(&mut self, keywords: V) where V: Into>, { self.keywords = keywords.into(); } /// Return the type of this podcast. /// /// A string usually "serial" or "episodic" /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_type("episodic".to_string()); /// assert_eq!(extension.r#type(), Some("episodic")); /// ``` pub fn r#type(&self) -> Option<&str> { self.r#type.as_deref() } /// Set the type of this podcast. /// /// A string, usually "serial" or "episodic" /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesChannelExtension; /// /// let mut extension = ITunesChannelExtension::default(); /// extension.set_type("serial".to_string()); /// ``` pub fn set_type(&mut self, t: V) where V: Into>, { self.r#type = t.into(); } } impl ITunesChannelExtension { /// Create an `ITunesChannelExtension` from a `BTreeMap`. pub fn from_map(mut map: BTreeMap>) -> Self { Self { author: remove_extension_value(&mut map, "author"), block: remove_extension_value(&mut map, "block"), categories: parse_categories(&mut map), image: parse_image(&mut map), explicit: remove_extension_value(&mut map, "explicit"), complete: remove_extension_value(&mut map, "complete"), new_feed_url: remove_extension_value(&mut map, "new-feed-url"), owner: parse_owner(&mut map), subtitle: remove_extension_value(&mut map, "subtitle"), summary: remove_extension_value(&mut map, "summary"), keywords: remove_extension_value(&mut map, "keywords"), r#type: remove_extension_value(&mut map, "type"), } } } impl ToXml for ITunesChannelExtension { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { if let Some(author) = self.author.as_ref() { writer.write_text_element("itunes:author", author)?; } if let Some(block) = self.block.as_ref() { writer.write_text_element("itunes:block", block)?; } writer.write_objects(&self.categories)?; if let Some(image) = self.image.as_ref() { let name = "itunes:image"; let mut element = BytesStart::new(name); element.push_attribute(("href", &**image)); writer.write_event(Event::Empty(element))?; } if let Some(explicit) = self.explicit.as_ref() { writer.write_text_element("itunes:explicit", explicit)?; } if let Some(complete) = self.complete.as_ref() { writer.write_text_element("itunes:complete", complete)?; } if let Some(new_feed_url) = self.new_feed_url.as_ref() { writer.write_text_element("itunes:new-feed-url", new_feed_url)?; } if let Some(owner) = self.owner.as_ref() { writer.write_object(owner)?; } if let Some(subtitle) = self.subtitle.as_ref() { writer.write_text_element("itunes:subtitle", subtitle)?; } if let Some(summary) = self.summary.as_ref() { writer.write_text_element("itunes:summary", summary)?; } if let Some(keywords) = self.keywords.as_ref() { writer.write_text_element("itunes:keywords", keywords)?; } if let Some(r#type) = self.r#type.as_ref() { writer.write_text_element("itunes:type", r#type)?; } Ok(()) } fn used_namespaces(&self) -> BTreeMap { let mut namespaces = BTreeMap::new(); namespaces.insert("itunes".to_owned(), NAMESPACE.to_owned()); namespaces } } #[cfg(feature = "builders")] impl ITunesChannelExtensionBuilder { /// Builds a new `ITunesChannelExtension`. pub fn build(&self) -> ITunesChannelExtension { self.build_impl().unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "builders")] fn test_builder() { use crate::extension::itunes::ITunesCategoryBuilder; assert_eq!( ITunesChannelExtensionBuilder::default() .author("John Doe".to_string()) .category(ITunesCategoryBuilder::default().text("technology").build()) .category(ITunesCategoryBuilder::default().text("podcast").build()) .build(), ITunesChannelExtension { author: Some("John Doe".to_string()), categories: vec![ ITunesCategory { text: "technology".to_string(), subcategory: None, }, ITunesCategory { text: "podcast".to_string(), subcategory: None, }, ], ..Default::default() }, ); } } rss-2.0.11/src/extension/itunes/itunes_item_extension.rs000064400000000000000000000471571046102023000216330ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::io::Write; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Writer; use super::{parse_image, NAMESPACE}; use crate::extension::util::remove_extension_value; use crate::extension::Extension; use crate::toxml::{ToXml, WriterExt}; /// An iTunes item element extension. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct ITunesItemExtension { /// The author of the podcast episode. pub author: Option, /// Specifies if the podcast episode should be prevented from appearing in the iTunes Store. A /// value of `Yes` indicates that the episode should not show up in the iTunes Store. All other /// values are ignored. pub block: Option, /// The artwork for the podcast episode. pub image: Option, /// The podcast episode duration in one of the following formats: HH:MM:SS, H:MM:SS, MM:SS, /// M:SS. pub duration: Option, /// Specifies whether the podcast episode contains explicit content. A value of `Yes`, /// `Explicit`, or `True` indicates that the episode contains explicit content. A value of /// `Clean`, `No`, `False` indicates that episode does not contain explicit content. pub explicit: Option, /// Specifies whether the podcast episode contains embedded closed captioning. A value of `Yes` /// indicates that it does. Any other value indicates that it does not. pub closed_captioned: Option, /// A value used to override the default sorting order for episodes. pub order: Option, /// A description of the podcast episode. pub subtitle: Option, /// A summary of the podcast episode. pub summary: Option, /// Keywords for the podcast. The string contains a comma separated list of keywords. pub keywords: Option, /// Episode number for this episode. pub episode: Option, /// Season number for this episode. pub season: Option, /// Type of episode. Usually `full`, but potentially also `trailer` or `bonus` pub episode_type: Option, } impl ITunesItemExtension { /// Return the author of this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_author("John Doe".to_string()); /// assert_eq!(extension.author(), Some("John Doe")); /// ``` pub fn author(&self) -> Option<&str> { self.author.as_deref() } /// Set the author of this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_author("John Doe".to_string()); /// ``` pub fn set_author(&mut self, author: V) where V: Into>, { self.author = author.into(); } /// Return whether this podcast episode should be blocked from appearing in the iTunes Store. /// /// A value of `Yes` indicates that the podcast should not show up in the iTunes Store. All /// other values are ignored. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_block("Yes".to_string()); /// assert_eq!(extension.block(), Some("Yes")); /// ``` pub fn block(&self) -> Option<&str> { self.block.as_deref() } /// Set whether this podcast episode should be blocked from appearing in the iTunes Store. /// /// A value of `Yes` indicates that the podcast should not show up in the iTunes Store. All /// other values are ignored. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_block("Yes".to_string()); /// ``` pub fn set_block(&mut self, block: V) where V: Into>, { self.block = block.into(); } /// Return the artwork URL for this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_image("http://example.com/artwork.png".to_string()); /// assert_eq!(extension.image(), Some("http://example.com/artwork.png")); /// ``` pub fn image(&self) -> Option<&str> { self.image.as_deref() } /// Set the artwork URL for this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_image("http://example.com/artwork.png".to_string()); /// ``` pub fn set_image(&mut self, image: V) where V: Into>, { self.image = image.into(); } /// Return the duration of this podcast episode. /// /// The duration should be in one of the following formats: HH:MM:SS, H:MM:SS, MM:SS, M:SS. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_duration("1:00".to_string()); /// assert_eq!(extension.duration(), Some("1:00")); /// ``` pub fn duration(&self) -> Option<&str> { self.duration.as_deref() } /// Set the duration of this podcast episode. /// /// The duration should be in one of the following formats: HH:MM:SS, H:MM:SS, MM:SS, M:SS. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_duration("1:00".to_string()); /// ``` pub fn set_duration(&mut self, duration: V) where V: Into>, { self.duration = duration.into(); } /// Return whether this podcast episode contains explicit content. /// /// A value of `Yes`, `Explicit`, or `True` indicates that the episode contains explicit /// content. A value of `Clean`, `No`, `False` indicates that episode does not contain /// explicit content. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_explicit("Yes".to_string()); /// assert_eq!(extension.explicit(), Some("Yes")); /// ``` pub fn explicit(&self) -> Option<&str> { self.explicit.as_deref() } /// Set whether this podcast episode contains explicit content. /// /// A value of `Yes`, `Explicit`, or `True` indicates that the episode contains explicit /// content. A value of `Clean`, `No`, `False` indicates that episode does not contain /// explicit content. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_explicit("Yes".to_string()); /// ``` pub fn set_explicit(&mut self, explicit: V) where V: Into>, { self.explicit = explicit.into(); } /// Return whether this podcast episode contains embedded closed captioning. /// /// A value of `Yes` indicates that it does. Any other value indicates that it does not. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_closed_captioned("Yes".to_string()); /// assert_eq!(extension.closed_captioned(), Some("Yes")); /// ``` pub fn closed_captioned(&self) -> Option<&str> { self.closed_captioned.as_deref() } /// Set whether this podcast episode contains embedded closed captioning. /// /// A value of `Yes` indicates that it does. Any other value indicates that it does not. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_closed_captioned("Yes".to_string()); /// ``` pub fn set_closed_captioned(&mut self, closed_captioned: V) where V: Into>, { self.closed_captioned = closed_captioned.into(); } /// Return the value used to override the default sorting order for episodes. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_order("1".to_string()); /// assert_eq!(extension.order(), Some("1")); /// ``` pub fn order(&self) -> Option<&str> { self.order.as_deref() } /// Set the value used to override the default sorting order for episodes. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_order("1".to_string()); /// ``` pub fn set_order(&mut self, order: V) where V: Into>, { self.order = order.into(); } /// Return the description of this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_subtitle("An episode".to_string()); /// assert_eq!(extension.subtitle(), Some("An episode")); /// ``` pub fn subtitle(&self) -> Option<&str> { self.subtitle.as_deref() } /// Set the description of this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_subtitle("An episode".to_string()); /// ``` pub fn set_subtitle(&mut self, subtitle: V) where V: Into>, { self.subtitle = subtitle.into(); } /// Return the summary for this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_summary("An episode".to_string()); /// assert_eq!(extension.summary(), Some("An episode")); /// ``` pub fn summary(&self) -> Option<&str> { self.summary.as_deref() } /// Set the summary for this podcast episode. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_summary("An episode".to_string()); /// ``` pub fn set_summary(&mut self, summary: V) where V: Into>, { self.summary = summary.into(); } /// Return the keywords for this podcast episode. /// /// A comma separated list of keywords. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_keywords("technology".to_string()); /// assert_eq!(extension.keywords(), Some("technology")); /// ``` pub fn keywords(&self) -> Option<&str> { self.keywords.as_deref() } /// Set the keywords for this podcast episode. /// /// A comma separated list of keywords. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_keywords("technology".to_string()); /// ``` pub fn set_keywords(&mut self, keywords: V) where V: Into>, { self.keywords = keywords.into(); } /// Return the episode number of this podcast episode /// /// The episode number will be a string although it is typically a number in practice /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_episode("3".to_string()); /// assert_eq!(extension.episode(), Some("3")); /// ``` pub fn episode(&self) -> Option<&str> { self.episode.as_deref() } /// Set the the episode number for this episode. /// /// An string. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_episode("2".to_string()); /// assert_eq!(extension.episode(), Some("2")); /// ``` pub fn set_episode(&mut self, episode: V) where V: Into>, { self.episode = episode.into() } /// Return the season of this podcast episode /// /// The season will be a string although it is typically a number in practice /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_season("3".to_string()); /// assert_eq!(extension.season(), Some("3")); /// ``` pub fn season(&self) -> Option<&str> { self.season.as_deref() } /// Set the the season number for this episode. /// /// An integer. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_season("3".to_string()); /// assert_eq!(extension.season(), Some("3")); /// ``` pub fn set_season(&mut self, season: V) where V: Into>, { self.season = season.into() } /// Return the episode_type of this podcast episode /// /// The episode type will be a string usually "full" "trailer" or "bonus" /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_episode_type("trailer".to_string()); /// assert_eq!(extension.episode_type(), Some("trailer")); /// ``` pub fn episode_type(&self) -> Option<&str> { self.episode_type.as_deref() } /// Set the the episode type for this episode. /// /// A string, usually "full" but maybe "trailer" or "bonus" /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut extension = ITunesItemExtension::default(); /// extension.set_episode_type("full".to_string()); /// assert_eq!(extension.episode_type(), Some("full")); /// ``` pub fn set_episode_type(&mut self, episode_type: V) where V: Into>, { self.episode_type = episode_type.into() } } impl ITunesItemExtension { /// Create an `ITunesChannelExtension` from a `BTreeMap`. pub fn from_map(mut map: BTreeMap>) -> Self { Self { author: remove_extension_value(&mut map, "author"), block: remove_extension_value(&mut map, "block"), image: parse_image(&mut map), duration: remove_extension_value(&mut map, "duration"), explicit: remove_extension_value(&mut map, "explicit"), closed_captioned: remove_extension_value(&mut map, "isClosedCaptioned"), order: remove_extension_value(&mut map, "order"), subtitle: remove_extension_value(&mut map, "subtitle"), summary: remove_extension_value(&mut map, "summary"), keywords: remove_extension_value(&mut map, "keywords"), episode: remove_extension_value(&mut map, "episode"), season: remove_extension_value(&mut map, "season"), episode_type: remove_extension_value(&mut map, "episodeType"), } } } impl ToXml for ITunesItemExtension { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { if let Some(author) = self.author.as_ref() { writer.write_text_element("itunes:author", author)?; } if let Some(block) = self.block.as_ref() { writer.write_text_element("itunes:block", block)?; } if let Some(image) = self.image.as_ref() { let name = "itunes:image"; let mut element = BytesStart::new(name); element.push_attribute(("href", &**image)); writer.write_event(Event::Start(element))?; writer.write_event(Event::End(BytesEnd::new(name)))?; } if let Some(duration) = self.duration.as_ref() { writer.write_text_element("itunes:duration", duration)?; } if let Some(explicit) = self.explicit.as_ref() { writer.write_text_element("itunes:explicit", explicit)?; } if let Some(closed_captioned) = self.closed_captioned.as_ref() { writer.write_text_element("itunes:isClosedCaptioned", closed_captioned)?; } if let Some(order) = self.order.as_ref() { writer.write_text_element("itunes:order", order)?; } if let Some(subtitle) = self.subtitle.as_ref() { writer.write_text_element("itunes:subtitle", subtitle)?; } if let Some(summary) = self.summary.as_ref() { writer.write_text_element("itunes:summary", summary)?; } if let Some(keywords) = self.keywords.as_ref() { writer.write_text_element("itunes:keywords", keywords)?; } if let Some(episode) = self.episode.as_ref() { writer.write_text_element("itunes:episode", episode)?; } if let Some(season) = self.season.as_ref() { writer.write_text_element("itunes:season", season)?; } if let Some(episode_type) = self.episode_type.as_ref() { writer.write_text_element("itunes:episodeType", episode_type)?; } Ok(()) } fn used_namespaces(&self) -> BTreeMap { let mut namespaces = BTreeMap::new(); namespaces.insert("itunes".to_owned(), NAMESPACE.to_owned()); namespaces } } #[cfg(feature = "builders")] impl ITunesItemExtensionBuilder { /// Builds a new `ITunesItemExtension`. pub fn build(&self) -> ITunesItemExtension { self.build_impl().unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "builders")] fn test_builder() { assert_eq!( ITunesItemExtensionBuilder::default() .author("John Doe".to_string()) .build(), ITunesItemExtension { author: Some("John Doe".to_string()), ..Default::default() } ); } } rss-2.0.11/src/extension/itunes/itunes_owner.rs000064400000000000000000000076461046102023000177320ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::Write; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Writer; use crate::toxml::{ToXml, WriterExt}; /// The contact information for the owner of an iTunes podcast. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct ITunesOwner { /// The name of the owner. pub name: Option, /// The email of the owner. pub email: Option, } impl ITunesOwner { /// Return the name of this person. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesOwner; /// /// let mut owner = ITunesOwner::default(); /// owner.set_name("John Doe".to_string()); /// assert_eq!(owner.name(), Some("John Doe")); /// ``` pub fn name(&self) -> Option<&str> { self.name.as_deref() } /// Set the name of this person. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesOwner; /// /// let mut owner = ITunesOwner::default(); /// owner.set_name("John Doe".to_string()); /// ``` pub fn set_name(&mut self, name: V) where V: Into>, { self.name = name.into(); } /// Return the email of this person. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesOwner; /// /// let mut owner = ITunesOwner::default(); /// owner.set_email("johndoe@example.com".to_string()); /// assert_eq!(owner.email(), Some("johndoe@example.com")); /// ``` pub fn email(&self) -> Option<&str> { self.email.as_deref() } /// Set the email of this person. /// /// # Examples /// /// ``` /// use rss::extension::itunes::ITunesOwner; /// /// let mut owner = ITunesOwner::default(); /// owner.set_email("johndoe@example.com".to_string()); /// ``` pub fn set_email(&mut self, email: V) where V: Into>, { self.email = email.into(); } } impl ToXml for ITunesOwner { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "itunes:owner"; writer.write_event(Event::Start(BytesStart::new(name)))?; if let Some(name) = self.name.as_ref() { writer.write_text_element("itunes:name", name)?; } if let Some(email) = self.email.as_ref() { writer.write_text_element("itunes:email", email)?; } writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } } #[cfg(feature = "builders")] impl ITunesOwnerBuilder { /// Builds a new `ITunesOwner`. pub fn build(&self) -> ITunesOwner { self.build_impl().unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "builders")] fn test_builder() { assert_eq!( ITunesOwnerBuilder::default() .name("John Doe".to_string()) .build(), ITunesOwner { name: Some("John Doe".to_string()), email: None, } ); assert_eq!( ITunesOwnerBuilder::default() .name("John Doe".to_string()) .email("johndoe@example.com".to_string()) .build(), ITunesOwner { name: Some("John Doe".to_string()), email: Some("johndoe@example.com".to_string()), } ); } } rss-2.0.11/src/extension/itunes/mod.rs000064400000000000000000000054001046102023000157520ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use crate::extension::Extension; mod itunes_category; mod itunes_channel_extension; mod itunes_item_extension; mod itunes_owner; pub use self::itunes_category::*; pub use self::itunes_channel_extension::*; pub use self::itunes_item_extension::*; pub use self::itunes_owner::*; /// The iTunes XML namespace. pub const NAMESPACE: &str = "http://www.itunes.com/dtds/podcast-1.0.dtd"; /// Formally XML namespace is case sensitive and this should be just an equality check. /// But many podcast publishers ignore this and use different case variations of the namespace. /// Hence this check is relaxed and ignores a case. #[inline] pub(crate) fn is_itunes_namespace(ns: &str) -> bool { ns.eq_ignore_ascii_case(NAMESPACE) } fn parse_image(map: &mut BTreeMap>) -> Option { let mut element = match map.remove("image").map(|mut v| v.remove(0)) { Some(element) => element, None => return None, }; element.attrs.remove("href") } fn parse_categories(map: &mut BTreeMap>) -> Vec { let mut elements = match map.remove("category") { Some(elements) => elements, None => return Vec::new(), }; let mut categories = Vec::with_capacity(elements.len()); for elem in &mut elements { let text = elem.attrs.remove("text").unwrap_or_default(); let child = { if let Some(mut child) = elem.children.remove("category").map(|mut v| v.remove(0)) { let text = child.attrs.remove("text").unwrap_or_default(); let mut category = ITunesCategory::default(); category.set_text(text); Some(Box::new(category)) } else { None } }; let mut category = ITunesCategory::default(); category.set_text(text); category.set_subcategory(child); categories.push(category); } categories } fn parse_owner(map: &mut BTreeMap>) -> Option { if let Some(mut element) = map.remove("owner").map(|mut v| v.remove(0)) { let name = element .children .remove("name") .and_then(|mut v| v.remove(0).value); let email = element .children .remove("email") .and_then(|mut v| v.remove(0).value); let mut owner = ITunesOwner::default(); owner.set_name(name); owner.set_email(email); Some(owner) } else { None } } rss-2.0.11/src/extension/mod.rs000064400000000000000000000074361046102023000144560ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::io::Write; use std::str; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Error as XmlError; use quick_xml::Writer; use crate::toxml::ToXml; /// Types and methods for [Atom](https://www.rssboard.org/rss-profile#namespace-elements-atom) extensions. #[cfg(feature = "atom")] pub mod atom; /// Types and methods for /// [iTunes](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) extensions. pub mod itunes; /// Types and methods for [Dublin Core](http://dublincore.org/documents/dces/) extensions. pub mod dublincore; /// Types and methods for [Syndication](http://web.resource.org/rss/1.0/modules/syndication/) extensions. pub mod syndication; pub(crate) mod util; /// A map of extension namespace prefixes to local names to elements. pub type ExtensionMap = BTreeMap>>; /// A namespaced extension such as iTunes or Dublin Core. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Extension { /// The qualified name of the extension element. pub name: String, /// The content of the extension element. pub value: Option, /// The attributes for the extension element. #[cfg_attr(feature = "builders", builder(setter(each = "attr")))] pub attrs: BTreeMap, /// The children of the extension element. This is a map of local names to child /// elements. #[cfg_attr(feature = "builders", builder(setter(each = "child")))] pub children: BTreeMap>, } impl Extension { /// Return the qualified name of this extension. pub fn name(&self) -> &str { self.name.as_str() } /// Set the qualified name of this extension. pub fn set_name(&mut self, name: V) where V: Into, { self.name = name.into(); } /// Return the text content of this extension. pub fn value(&self) -> Option<&str> { self.value.as_deref() } /// Set the text content of this extension. pub fn set_value(&mut self, value: V) where V: Into>, { self.value = value.into(); } /// Return the attributes for the extension element. pub fn attrs(&self) -> &BTreeMap { &self.attrs } /// Return the children of the extension element. /// /// This is a map of local names to child elements. pub fn children(&self) -> &BTreeMap> { &self.children } } impl ToXml for Extension { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let mut element = BytesStart::new(&self.name); element.extend_attributes(self.attrs.iter().map(|a| (a.0.as_str(), a.1.as_str()))); writer.write_event(Event::Start(element))?; if let Some(ref value) = self.value { writer.write_event(Event::Text(BytesText::new(value)))?; } for extension in self.children.values().flatten() { extension.to_xml(writer)?; } writer.write_event(Event::End(BytesEnd::new(&self.name)))?; Ok(()) } } #[cfg(feature = "builders")] impl ExtensionBuilder { /// Builds a new `Extension`. pub fn build(&self) -> Extension { self.build_impl().unwrap() } } rss-2.0.11/src/extension/syndication.rs000064400000000000000000000140051046102023000162110ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::fmt; use std::io::Write; use std::str::FromStr; use quick_xml::Error as XmlError; use quick_xml::Writer; use crate::extension::Extension; use crate::toxml::WriterExt; /// The Syndication XML namespace. pub const NAMESPACE: &str = "http://purl.org/rss/1.0/modules/syndication/"; /// The unit of time between updates/refreshes #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] pub enum UpdatePeriod { /// refresh hourly Hourly, /// refresh daily Daily, /// refresh weekly Weekly, /// refresh monthly Monthly, /// refresh yearly Yearly, } impl FromStr for UpdatePeriod { type Err = (); fn from_str(s: &str) -> Result { match s { "hourly" => Ok(UpdatePeriod::Hourly), "daily" => Ok(UpdatePeriod::Daily), "weekly" => Ok(UpdatePeriod::Weekly), "monthly" => Ok(UpdatePeriod::Monthly), "yearly" => Ok(UpdatePeriod::Yearly), _ => Err(()), } } } impl fmt::Display for UpdatePeriod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { UpdatePeriod::Hourly => write!(f, "hourly"), UpdatePeriod::Daily => write!(f, "daily"), UpdatePeriod::Weekly => write!(f, "weekly"), UpdatePeriod::Monthly => write!(f, "monthly"), UpdatePeriod::Yearly => write!(f, "yearly"), } } } /// An RSS syndication element extension. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct SyndicationExtension { /// The refresh period for this channel pub period: UpdatePeriod, /// Number of periods between refreshes pub frequency: u32, /// Timestamp from which the refresh periods are calculated pub base: String, } impl SyndicationExtension { /// Retrieve the base timestamp from which the refresh periods are calculated pub fn base(&self) -> &str { &self.base } /// Set the base from which the refresh periods are calculated pub fn set_base(&mut self, base: &str) { base.clone_into(&mut self.base); } /// Retrieve the number of periods between refreshes pub fn frequency(&self) -> u32 { self.frequency } /// Set the number of periods between refreshes pub fn set_frequency(&mut self, frequency: u32) { self.frequency = frequency; } /// Retrieve the refresh period for this channel pub fn period(&self) -> &UpdatePeriod { &self.period } /// Set the refresh period for this channel pub fn set_period(&mut self, period: UpdatePeriod) { self.period = period; } /// Serializes this extension to the nominated writer pub fn to_xml( &self, namespaces: &BTreeMap, writer: &mut Writer, ) -> Result<(), XmlError> { for (prefix, namespace) in namespaces { if NAMESPACE == namespace { writer.write_text_element( format!("{}:updatePeriod", prefix), &self.period.to_string(), )?; writer.write_text_element( format!("{}:updateFrequency", prefix), &format!("{}", self.frequency), )?; writer.write_text_element(format!("{}:updateBase", prefix), &self.base)?; } } Ok(()) } } impl Default for SyndicationExtension { fn default() -> Self { SyndicationExtension { period: UpdatePeriod::Daily, frequency: 1, base: String::from("1970-01-01T00:00+00:00"), } } } /// Retrieves the extensions for the nominated field and runs the callback if there is at least 1 extension value fn with_first_ext_value<'a, F>(map: &'a BTreeMap>, field: &str, f: F) where F: FnOnce(&'a str), { if let Some(extensions) = map.get(field) { if !extensions.is_empty() { if let Some(v) = extensions[0].value.as_ref() { f(v); } } } } impl SyndicationExtension { /// Creates a `SyndicationExtension` using the specified `BTreeMap`. pub fn from_map(map: BTreeMap>) -> Self { let mut syn = SyndicationExtension::default(); with_first_ext_value(&map, "updatePeriod", |value| { if let Ok(update_period) = value.parse() { syn.period = update_period } }); with_first_ext_value(&map, "updateFrequency", |value| { if let Ok(frequency) = value.parse() { syn.frequency = frequency } }); with_first_ext_value(&map, "updateBase", |value| value.clone_into(&mut syn.base)); syn } } #[cfg(feature = "builders")] impl SyndicationExtensionBuilder { /// Builds a new `SyndicationExtension`. pub fn build(&self) -> SyndicationExtension { self.build_impl().unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "builders")] fn test_builder() { assert_eq!( SyndicationExtensionBuilder::default() .period(UpdatePeriod::Weekly) .frequency(2_u32) .base("2021-01-01T00:00+00:00") .build(), SyndicationExtension { period: UpdatePeriod::Weekly, frequency: 2, base: "2021-01-01T00:00+00:00".to_string(), } ); } } rss-2.0.11/src/extension/util.rs000064400000000000000000000070411046102023000146440ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::borrow::Cow; use std::collections::BTreeMap; use std::io::BufRead; use std::str; use quick_xml::events::attributes::Attributes; use quick_xml::events::Event; use quick_xml::Reader; use crate::error::Error; use crate::extension::{Extension, ExtensionMap}; use crate::util::{attr_value, decode}; pub(crate) fn read_namespace_declarations<'m, R>( reader: &mut Reader, mut atts: Attributes, base: &'m BTreeMap, ) -> Result>, Error> where R: BufRead, { let mut namespaces = Cow::Borrowed(base); for attr in atts.with_checks(false).flatten() { let key = decode(attr.key.as_ref(), reader)?; if let Some(ns) = key.strip_prefix("xmlns:") { namespaces .to_mut() .insert(ns.to_string(), attr_value(&attr, reader)?.to_string()); } } Ok(namespaces) } pub(crate) fn extension_name(element_name: &str) -> Option<(&str, &str)> { let mut split = element_name.splitn(2, ':'); let ns = split.next().filter(|ns| !ns.is_empty())?; let name = split.next()?; Some((ns, name)) } pub(crate) fn extension_entry<'e>( extensions: &'e mut ExtensionMap, ns: &str, name: &str, ) -> &'e mut Vec { let map = extensions.entry(ns.to_string()).or_default(); map.entry(name.to_string()).or_default() } pub(crate) fn parse_extension_element( reader: &mut Reader, mut atts: Attributes, ) -> Result { let mut extension = Extension::default(); let mut buf = Vec::new(); for attr in atts.with_checks(false).flatten() { let key = decode(attr.key.as_ref(), reader)?.to_string(); let value = attr_value(&attr, reader)?.to_string(); extension.attrs.insert(key.to_string(), value); } let mut text = String::new(); loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => { let ext = parse_extension_element(reader, element.attributes())?; let element_local_name = element.local_name(); let name = decode(element_local_name.as_ref(), reader)?; let items = extension .children .entry(name.to_string()) .or_insert_with(Vec::new); items.push(ext); } Event::CData(element) => { text.push_str(decode(&element, reader)?.as_ref()); } Event::Text(element) => { text.push_str(element.unescape()?.as_ref()); } Event::End(element) => { extension.name = decode(element.name().as_ref(), reader)?.into(); break; } Event::Eof => return Err(Error::Eof), _ => {} } buf.clear(); } extension.value = Some(text.trim()) .filter(|t| !t.is_empty()) .map(ToString::to_string); Ok(extension) } pub fn get_extension_values(v: Vec) -> Vec { v.into_iter() .filter_map(|ext| ext.value) .collect::>() } pub fn remove_extension_value( map: &mut BTreeMap>, key: &str, ) -> Option { map.remove(key) .map(|mut v| v.remove(0)) .and_then(|ext| ext.value) } rss-2.0.11/src/guid.rs000064400000000000000000000073701046102023000126100ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::{BufRead, Write}; use quick_xml::events::attributes::Attributes; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::error::Error; use crate::toxml::ToXml; use crate::util::{decode, element_text}; /// Represents the GUID of an RSS item. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Guid { /// The value of the GUID. pub value: String, /// Indicates if the GUID is a permalink. pub permalink: bool, } impl Guid { /// Return whether this GUID is a permalink. /// /// # Examples /// /// ``` /// use rss::Guid; /// /// let mut guid = Guid::default(); /// guid.set_permalink(true); /// assert!(guid.is_permalink()); /// ``` pub fn is_permalink(&self) -> bool { self.permalink } /// Set whether this GUID is a permalink. /// /// # Examples /// /// ``` /// use rss::Guid; /// /// let mut guid = Guid::default(); /// guid.set_permalink(true); /// ``` pub fn set_permalink(&mut self, permalink: V) where V: Into, { self.permalink = permalink.into() } /// Return the value of this GUID. /// /// # Examples /// /// ``` /// use rss::Guid; /// /// let mut guid = Guid::default(); /// guid.set_value("00000000-0000-0000-0000-00000000000"); /// assert_eq!(guid.value(), "00000000-0000-0000-0000-00000000000"); /// ``` pub fn value(&self) -> &str { self.value.as_str() } /// Set the value of this GUID. /// /// # Examples /// /// ``` /// use rss::Guid; /// /// let mut guid = Guid::default(); /// guid.set_value("00000000-0000-0000-0000-00000000000"); /// ``` pub fn set_value(&mut self, value: V) where V: Into, { self.value = value.into(); } } impl Default for Guid { #[inline] fn default() -> Self { Guid { value: Default::default(), permalink: true, } } } impl Guid { /// Builds a Guid from source XML pub fn from_xml( reader: &mut Reader, mut atts: Attributes, ) -> Result { let mut guid = Guid::default(); for attr in atts.with_checks(false).flatten() { if decode(attr.key.as_ref(), reader)?.as_ref() == "isPermaLink" { guid.permalink = &*attr.value != b"false"; break; } } guid.value = element_text(reader)?.unwrap_or_default(); Ok(guid) } } impl ToXml for Guid { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "guid"; let mut element = BytesStart::new(name); if !self.permalink { element.push_attribute(("isPermaLink", "false")); } writer.write_event(Event::Start(element))?; writer.write_event(Event::Text(BytesText::new(&self.value)))?; writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } } #[cfg(feature = "builders")] impl GuidBuilder { /// Builds a new `Guid`. pub fn build(&self) -> Guid { self.build_impl().unwrap() } } rss-2.0.11/src/image.rs000064400000000000000000000200221046102023000127270ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::{BufRead, Write}; use quick_xml::events::attributes::Attributes; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::error::Error; use crate::toxml::{ToXml, WriterExt}; use crate::util::{decode, element_text, skip}; /// Represents an image in an RSS feed. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Image { /// The URL of the image. pub url: String, /// A description of the image. This is used in the HTML `alt` attribute. pub title: String, /// The URL that the image links to. pub link: String, /// The width of the image. pub width: Option, /// The height of the image. pub height: Option, /// The text for the HTML `title` attribute of the link formed around the image. pub description: Option, } impl Image { /// Return the URL of this image. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_url("http://example.com/image.png"); /// assert_eq!(image.url(), "http://example.com/image.png"); /// ``` pub fn url(&self) -> &str { self.url.as_str() } /// Set the URL of this image. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_url("http://example.com/image.png"); /// ``` pub fn set_url(&mut self, url: V) where V: Into, { self.url = url.into(); } /// Return the description of this image. /// /// This is used in the HTML `alt` attribute. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_title("Example image"); /// assert_eq!(image.title(), "Example image"); /// ``` pub fn title(&self) -> &str { self.title.as_str() } /// Set the description of this image. /// /// This is used in the HTML `alt` attribute. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_title("Example image"); /// ``` pub fn set_title(&mut self, title: V) where V: Into, { self.title = title.into(); } /// Return the URL that this image links to. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_link("http://example.com"); /// assert_eq!(image.link(), "http://example.com"); pub fn link(&self) -> &str { self.link.as_str() } /// Set the URL that this image links to. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_link("http://example.com"); pub fn set_link(&mut self, link: V) where V: Into, { self.link = link.into(); } /// Return the width of this image. /// /// If the width is `None` the default value should be considered to be `80`. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_width("80".to_string()); /// assert_eq!(image.width(), Some("80")); pub fn width(&self) -> Option<&str> { self.width.as_deref() } /// Set the width of this image. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_width("80".to_string()); pub fn set_width(&mut self, width: V) where V: Into>, { self.width = width.into(); } /// Return the height of this image. /// /// If the height is `None` the default value should be considered to be `31`. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_height("31".to_string()); /// assert_eq!(image.height(), Some("31")); /// ``` pub fn height(&self) -> Option<&str> { self.height.as_deref() } /// Set the height of this image. /// /// If the height is `None` the default value should be considered to be `31`. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_height("31".to_string()); /// ``` pub fn set_height(&mut self, height: V) where V: Into>, { self.height = height.into(); } /// Return the title for the link formed around this image. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_description("Example Title".to_string()); /// assert_eq!(image.description(), Some("Example Title")); /// ``` pub fn description(&self) -> Option<&str> { self.description.as_deref() } /// Set the title for the link formed around this image. /// /// # Examples /// /// ``` /// use rss::Image; /// /// let mut image = Image::default(); /// image.set_description("Example Title".to_string()); /// ``` pub fn set_description(&mut self, description: V) where V: Into>, { self.description = description.into(); } } impl Image { /// Builds an Image from source XML pub fn from_xml(reader: &mut Reader, _: Attributes) -> Result { let mut image = Image::default(); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => match decode(element.name().as_ref(), reader)?.as_ref() { "url" => image.url = element_text(reader)?.unwrap_or_default(), "title" => image.title = element_text(reader)?.unwrap_or_default(), "link" => image.link = element_text(reader)?.unwrap_or_default(), "width" => image.width = element_text(reader)?, "height" => image.height = element_text(reader)?, "description" => image.description = element_text(reader)?, _ => skip(element.name(), reader)?, }, Event::End(_) => break, Event::Eof => return Err(Error::Eof), _ => {} } buf.clear(); } Ok(image) } } impl ToXml for Image { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "image"; writer.write_event(Event::Start(BytesStart::new(name)))?; writer.write_text_element("url", &self.url)?; writer.write_text_element("title", &self.title)?; writer.write_text_element("link", &self.link)?; if let Some(width) = self.width.as_ref() { writer.write_text_element("width", width)?; } if let Some(height) = self.height.as_ref() { writer.write_text_element("height", height)?; } if let Some(description) = self.description.as_ref() { writer.write_text_element("description", description)?; } writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } } #[cfg(feature = "builders")] impl ImageBuilder { /// Builds a new `Image`. pub fn build(&self) -> Image { self.build_impl().unwrap() } } rss-2.0.11/src/item.rs000064400000000000000000000555711046102023000126240ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::io::{BufRead, Write}; use quick_xml::events::attributes::Attributes; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::category::Category; use crate::enclosure::Enclosure; use crate::error::Error; #[cfg(feature = "atom")] use crate::extension::atom; use crate::extension::dublincore; use crate::extension::itunes::{self, is_itunes_namespace}; use crate::extension::util::{ extension_entry, extension_name, parse_extension_element, read_namespace_declarations, }; use crate::extension::ExtensionMap; use crate::guid::Guid; use crate::source::Source; use crate::toxml::{ToXml, WriterExt}; use crate::util::{decode, element_text, skip}; /// Represents an item in an RSS feed. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Item { /// The title of the item. pub title: Option, /// The URL of the item. pub link: Option, /// The item synopsis. pub description: Option, /// The email address of author of the item. pub author: Option, /// The categories the item belongs to. #[cfg_attr(feature = "builders", builder(setter(each = "category")))] pub categories: Vec, /// The URL for the comments page of the item. pub comments: Option, /// The description of a media object that is attached to the item. pub enclosure: Option, /// A unique identifier for the item. pub guid: Option, /// The date the item was published as an RFC 2822 timestamp. pub pub_date: Option, /// The RSS channel the item came from. pub source: Option, /// The HTML contents of the item. pub content: Option, /// The extensions for the item. #[cfg_attr(feature = "builders", builder(setter(each = "extension")))] pub extensions: ExtensionMap, /// The Atom extension for the channel. #[cfg(feature = "atom")] pub atom_ext: Option, /// The iTunes extension for the item. pub itunes_ext: Option, /// The Dublin Core extension for the item. pub dublin_core_ext: Option, } impl Item { /// Return the title of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_title("Item Title".to_string()); /// assert_eq!(item.title(), Some("Item Title")); /// ``` pub fn title(&self) -> Option<&str> { self.title.as_deref() } /// Set the title of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_title("Item Title".to_string()); /// ``` pub fn set_title(&mut self, title: V) where V: Into>, { self.title = title.into(); } /// Return the URL of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_link("http://example.com".to_string()); /// assert_eq!(item.link(), Some("http://example.com")); /// ``` pub fn link(&self) -> Option<&str> { self.link.as_deref() } /// Set the URL of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_link("http://example.com".to_string()); /// ``` pub fn set_link(&mut self, link: V) where V: Into>, { self.link = link.into(); } /// Return the description of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_description("Item description".to_string()); /// assert_eq!(item.description(), Some("Item description")); /// ``` pub fn description(&self) -> Option<&str> { self.description.as_deref() } /// Return the description of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_description("Item description".to_string()); /// ``` pub fn set_description(&mut self, description: V) where V: Into>, { self.description = description.into(); } /// Return the email address for the author of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_author("John Doe".to_string()); /// assert_eq!(item.author(), Some("John Doe")); /// ``` pub fn author(&self) -> Option<&str> { self.author.as_deref() } /// Set the email address for the author of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_author("John Doe".to_string()); /// ``` pub fn set_author(&mut self, author: V) where V: Into>, { self.author = author.into(); } /// Return the categories that this item belongs to. /// /// # Examples /// /// ``` /// use rss::{Category, Item}; /// /// let mut item = Item::default(); /// item.set_categories(vec![Category::default()]); /// assert_eq!(item.categories().len(), 1); /// ``` pub fn categories(&self) -> &[Category] { &self.categories } /// Return a mutable slice of the categories that this item belongs to. pub fn categories_mut(&mut self) -> &mut [Category] { &mut self.categories } /// Set the categories that this item belongs to. /// /// # Examples /// /// ``` /// use rss::{Category, Item}; /// /// let mut item = Item::default(); /// item.set_categories(vec![Category::default()]); /// ``` pub fn set_categories(&mut self, categories: V) where V: Into>, { self.categories = categories.into(); } /// Return the URL for comments about this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_comments("http://example.com".to_string()); /// assert_eq!(item.comments(), Some("http://example.com")); /// ``` pub fn comments(&self) -> Option<&str> { self.comments.as_deref() } /// Set the URL for comments about this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_comments("http://example.com".to_string()); /// ``` pub fn set_comments(&mut self, comments: V) where V: Into>, { self.comments = comments.into(); } /// Return the enclosure information for this item. /// /// # Examples /// /// ``` /// use rss::{Enclosure, Item}; /// /// let mut item = Item::default(); /// item.set_enclosure(Enclosure::default()); /// assert!(item.enclosure().is_some()); /// ``` pub fn enclosure(&self) -> Option<&Enclosure> { self.enclosure.as_ref() } /// Set the enclosure information for this item. /// /// # Examples /// /// ``` /// use rss::{Enclosure, Item}; /// /// let mut item = Item::default(); /// item.set_enclosure(Enclosure::default()); /// ``` pub fn set_enclosure(&mut self, enclosure: V) where V: Into>, { self.enclosure = enclosure.into(); } /// Return the GUID for this item. /// /// # Examples /// /// ``` /// use rss::{Guid, Item}; /// /// let mut item = Item::default(); /// item.set_guid(Guid::default()); /// assert!(item.guid().is_some()) /// ``` pub fn guid(&self) -> Option<&Guid> { self.guid.as_ref() } /// Set the GUID for this item. /// /// # Examples /// /// ``` /// use rss::{Guid, Item}; /// /// let mut item = Item::default(); /// item.set_guid(Guid::default()); /// ``` pub fn set_guid(&mut self, guid: V) where V: Into>, { self.guid = guid.into(); } /// Return the publication date of this item as an RFC 2822 timestamp. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_pub_date("Sun, 01 Jan 2017 12:00:00 GMT".to_string()); /// assert_eq!(item.pub_date(), Some("Sun, 01 Jan 2017 12:00:00 GMT")); /// ``` pub fn pub_date(&self) -> Option<&str> { self.pub_date.as_deref() } /// Set the publication date of this item as an RFC 2822 timestamp. /// /// # Examples /// /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_pub_date("Sun, 01 Jan 2017 12:00:00 GMT".to_string()); /// assert_eq!(item.pub_date(), Some("Sun, 01 Jan 2017 12:00:00 GMT")); /// ``` /// /// ## Using chrono::DateTime /// ``` /// # #[cfg(feature = "validation")] /// # { /// use rss::Item; /// use chrono::{FixedOffset, TimeZone, Utc}; /// /// let mut item = Item::default(); /// item.set_pub_date(Utc.with_ymd_and_hms(2017, 1, 1, 12, 0, 0).unwrap().to_rfc2822()); /// assert_eq!(item.pub_date(), Some("Sun, 1 Jan 2017 12:00:00 +0000")); /// /// item.set_pub_date(FixedOffset::east_opt(2 * 3600).unwrap().with_ymd_and_hms(2017, 1, 1, 12, 0, 0).unwrap().to_rfc2822()); /// assert_eq!(item.pub_date(), Some("Sun, 1 Jan 2017 12:00:00 +0200")); /// # } /// ``` pub fn set_pub_date(&mut self, pub_date: V) where V: Into>, { self.pub_date = pub_date.into(); } /// Return the source URL for this item. /// /// # Examples /// /// ``` /// use rss::{Item, Source}; /// /// let mut item = Item::default(); /// item.set_source(Source::default()); /// assert!(item.source().is_some()); /// ``` pub fn source(&self) -> Option<&Source> { self.source.as_ref() } /// Set the source URL for this item. /// /// # Examples /// /// ``` /// use rss::{Item, Source}; /// /// let mut item = Item::default(); /// item.set_source(Source::default()); /// ``` pub fn set_source(&mut self, source: V) where V: Into>, { self.source = source.into(); } /// Return the content of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_content("Item content".to_string()); /// assert_eq!(item.content(), Some("Item content")); /// ``` pub fn content(&self) -> Option<&str> { self.content.as_deref() } /// Set the content of this item. /// /// # Examples /// /// ``` /// use rss::Item; /// /// let mut item = Item::default(); /// item.set_content("Item content".to_string()); /// ``` pub fn set_content(&mut self, content: V) where V: Into>, { self.content = content.into(); } /// Return the Atom extension for this item. /// /// # Examples /// /// ``` /// use rss::Item; /// use rss::extension::atom::AtomExtension; /// /// let mut item = Item::default(); /// item.set_atom_ext(AtomExtension::default()); /// assert!(item.atom_ext().is_some()); /// ``` #[cfg(feature = "atom")] pub fn atom_ext(&self) -> Option<&atom::AtomExtension> { self.atom_ext.as_ref() } /// Set the Atom extension for this item. /// /// # Examples /// /// ``` /// use rss::Item; /// use rss::extension::atom::AtomExtension; /// /// let mut item = Item::default(); /// item.set_atom_ext(AtomExtension::default()); /// ``` #[cfg(feature = "atom")] pub fn set_atom_ext(&mut self, atom_ext: V) where V: Into>, { self.atom_ext = atom_ext.into(); } /// Return the iTunes extension for this item. /// /// # Examples /// /// ``` /// use rss::Item; /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut item = Item::default(); /// item.set_itunes_ext(ITunesItemExtension::default()); /// assert!(item.itunes_ext().is_some()); /// ``` pub fn itunes_ext(&self) -> Option<&itunes::ITunesItemExtension> { self.itunes_ext.as_ref() } /// Set the iTunes extension for this item. /// /// # Examples /// /// ``` /// use rss::Item; /// use rss::extension::itunes::ITunesItemExtension; /// /// let mut item = Item::default(); /// item.set_itunes_ext(ITunesItemExtension::default()); /// ``` pub fn set_itunes_ext(&mut self, itunes_ext: V) where V: Into>, { self.itunes_ext = itunes_ext.into(); } /// Return the Dublin Core extension for this item. /// /// # Examples /// /// ``` /// use rss::Item; /// use rss::extension::dublincore::DublinCoreExtension; /// /// let mut item = Item::default(); /// item.set_dublin_core_ext(DublinCoreExtension::default()); /// assert!(item.dublin_core_ext().is_some()); /// ``` pub fn dublin_core_ext(&self) -> Option<&dublincore::DublinCoreExtension> { self.dublin_core_ext.as_ref() } /// Set the Dublin Core extension for this item. /// /// # Examples /// /// ``` /// use rss::Item; /// use rss::extension::dublincore::DublinCoreExtension; /// /// let mut item = Item::default(); /// item.set_dublin_core_ext(DublinCoreExtension::default()); /// ``` pub fn set_dublin_core_ext(&mut self, dublin_core_ext: V) where V: Into>, { self.dublin_core_ext = dublin_core_ext.into(); } /// Return the extensions for this item. /// /// # Examples /// /// ``` /// use std::collections::BTreeMap; /// use rss::Item; /// use rss::extension::{ExtensionMap, Extension}; /// /// let extension = Extension::default(); /// /// let mut item_map = BTreeMap::>::new(); /// item_map.insert("ext:name".to_string(), vec![extension]); /// /// let mut extension_map = ExtensionMap::default(); /// extension_map.insert("ext".to_string(), item_map); /// /// let mut item = Item::default(); /// item.set_extensions(extension_map); /// assert_eq!(item.extensions() /// .get("ext") /// .and_then(|m| m.get("ext:name")) /// .map(|v| v.len()), /// Some(1)); /// ``` pub fn extensions(&self) -> &ExtensionMap { &self.extensions } /// Set the extensions for this item. /// /// # Examples /// /// ``` /// use rss::Item; /// use rss::extension::ExtensionMap; /// /// let mut item = Item::default(); /// item.set_extensions(ExtensionMap::default()); /// ``` pub fn set_extensions(&mut self, extensions: V) where V: Into, { self.extensions = extensions.into(); } } impl Item { /// Builds an Item from source XML pub fn from_xml( namespaces: &BTreeMap, reader: &mut Reader, atts: Attributes, ) -> Result { let mut item = Item::default(); let mut extensions = ExtensionMap::new(); let mut buf = Vec::new(); let namespaces = read_namespace_declarations(reader, atts, namespaces)?; loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => match decode(element.name().as_ref(), reader)?.as_ref() { "category" => { let category = Category::from_xml(reader, element.attributes())?; item.categories.push(category); } "guid" => { let guid = Guid::from_xml(reader, element.attributes())?; item.guid = Some(guid); } "enclosure" => item.enclosure = Some(Enclosure::from_xml(reader, &element)?), "source" => { let source = Source::from_xml(reader, element.attributes())?; item.source = Some(source); } "title" => item.title = element_text(reader)?, "link" => { if let Some(link) = element_text(reader)?.filter(|text| !text.is_empty()) { item.link = Some(link); } } "description" => item.description = element_text(reader)?, "author" => item.author = element_text(reader)?, "comments" => item.comments = element_text(reader)?, "pubDate" => item.pub_date = element_text(reader)?, "content:encoded" => item.content = element_text(reader)?, n => { if let Some((prefix, name)) = extension_name(n) { let scope_namespases = read_namespace_declarations( reader, element.attributes(), namespaces.as_ref(), )?; let ext_ns = scope_namespases.get(prefix).map(|s| s.as_str()); let ext = parse_extension_element(reader, element.attributes())?; match ext_ns { #[cfg(feature = "atom")] Some(ns @ atom::NAMESPACE) => { extension_entry(&mut extensions, ns, name).push(ext); } Some(ns) if is_itunes_namespace(ns) => { extension_entry(&mut extensions, itunes::NAMESPACE, name) .push(ext); } Some(ns @ dublincore::NAMESPACE) => { extension_entry(&mut extensions, ns, name).push(ext); } _ => extension_entry(&mut item.extensions, prefix, name).push(ext), } } else { skip(element.name(), reader)?; } } }, Event::End(_) => break, Event::Eof => return Err(Error::Eof), _ => {} } buf.clear(); } // Process each of the namespaces we know #[cfg(feature = "atom")] if let Some(v) = extensions.remove(atom::NAMESPACE) { item.atom_ext = Some(atom::AtomExtension::from_map(v)); } if let Some(v) = extensions.remove(itunes::NAMESPACE) { item.itunes_ext = Some(itunes::ITunesItemExtension::from_map(v)) } if let Some(v) = extensions.remove(dublincore::NAMESPACE) { item.dublin_core_ext = Some(dublincore::DublinCoreExtension::from_map(v)) } Ok(item) } } impl ToXml for Item { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "item"; writer.write_event(Event::Start(BytesStart::new(name)))?; if let Some(title) = self.title.as_ref() { writer.write_text_element("title", title)?; } if let Some(link) = self.link.as_ref() { writer.write_text_element("link", link)?; } if let Some(description) = self.description.as_ref() { writer.write_cdata_element("description", description)?; } if let Some(author) = self.author.as_ref() { writer.write_text_element("author", author)?; } writer.write_objects(&self.categories)?; if let Some(comments) = self.comments.as_ref() { writer.write_text_element("comments", comments)?; } if let Some(enclosure) = self.enclosure.as_ref() { writer.write_object(enclosure)?; } if let Some(guid) = self.guid.as_ref() { writer.write_object(guid)?; } if let Some(pub_date) = self.pub_date.as_ref() { writer.write_text_element("pubDate", pub_date)?; } if let Some(source) = self.source.as_ref() { writer.write_object(source)?; } if let Some(content) = self.content.as_ref() { writer.write_cdata_element("content:encoded", content)?; } for map in self.extensions.values() { for extensions in map.values() { for extension in extensions { extension.to_xml(writer)?; } } } #[cfg(feature = "atom")] if let Some(ext) = self.atom_ext.as_ref() { ext.to_xml(writer)?; } if let Some(ext) = self.itunes_ext.as_ref() { ext.to_xml(writer)?; } if let Some(ext) = self.dublin_core_ext.as_ref() { ext.to_xml(writer)?; } writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } fn used_namespaces(&self) -> BTreeMap { let mut namespaces = BTreeMap::new(); if self.content.is_some() { namespaces.insert( "content".to_owned(), "http://purl.org/rss/1.0/modules/content/".to_owned(), ); } if let Some(ext) = self.itunes_ext() { namespaces.extend(ext.used_namespaces()); } if let Some(ext) = self.dublin_core_ext() { namespaces.extend(ext.used_namespaces()); } #[cfg(feature = "atom")] if let Some(ext) = self.atom_ext() { namespaces.extend(ext.used_namespaces()); } namespaces } } #[cfg(feature = "builders")] impl ItemBuilder { /// Builds a new `Item`. pub fn build(&self) -> Item { self.build_impl().unwrap() } } rss-2.0.11/src/lib.rs000064400000000000000000000075431046102023000124300ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. #![warn(missing_docs)] #![doc(html_root_url = "https://docs.rs/rss/")] //! Library for serializing the RSS web content syndication format. //! //! # Reading //! //! A channel can be read from any object that implements the `BufRead` trait. //! //! ## From a file //! //! ```rust,no_run //! use std::fs::File; //! use std::io::BufReader; //! use rss::Channel; //! //! let file = File::open("example.xml").unwrap(); //! let channel = Channel::read_from(BufReader::new(file)).unwrap(); //! ``` //! //! ### From a buffer //! //! **Note**: This example requires [reqwest](https://crates.io/crates/reqwest) crate. //! //! ```rust,ignore //! use std::error::Error; //! use rss::Channel; //! //! async fn example_feed() -> Result> { //! let content = reqwest::get("http://example.com/feed.xml") //! .await? //! .bytes() //! .await?; //! let channel = Channel::read_from(&content[..])?; //! Ok(channel) //! } //! ``` //! //! # Writing //! //! A channel can be written to any object that implements the `Write` trait or converted to an //! XML string using the `ToString` trait. //! //! ```rust //! use rss::Channel; //! //! let channel = Channel::default(); //! channel.write_to(::std::io::sink()).unwrap(); // write to the channel to a writer //! let string = channel.to_string(); // convert the channel to a string //! ``` //! //! # Creation //! //! Builder methods are provided to assist in the creation of channels. //! //! **Note**: This requires the `builders` feature, which is enabled by default. //! //! ``` //! use rss::ChannelBuilder; //! //! let channel = ChannelBuilder::default() //! .title("Channel Title") //! .link("http://example.com") //! .description("An RSS feed.") //! .build(); //! ``` //! //! ## Validation //! //! Validation methods are provided to validate the contents of a channel against the //! RSS specification. //! //! **Note**: This requires enabling the `validation` feature. //! //! ```rust,ignore //! use rss::Channel; //! use rss::validation::Validate; //! //! let channel = Channel::default(); //! channel.validate().unwrap(); //! ``` #[cfg(feature = "builders")] #[macro_use] extern crate derive_builder; extern crate quick_xml; #[cfg(feature = "serde")] #[cfg(feature = "validation")] extern crate chrono; #[cfg(feature = "validation")] extern crate mime; #[cfg(feature = "serde")] #[macro_use] extern crate serde; #[cfg(feature = "validation")] extern crate url; mod category; mod channel; mod cloud; mod enclosure; mod guid; mod image; mod item; mod source; mod textinput; mod error; mod toxml; mod util; /// Types and methods for namespaced extensions. pub mod extension; /// Methods for validating RSS feeds. #[cfg(feature = "validation")] pub mod validation; pub use crate::category::Category; #[cfg(feature = "builders")] pub use crate::category::CategoryBuilder; pub use crate::channel::Channel; #[cfg(feature = "builders")] pub use crate::channel::ChannelBuilder; pub use crate::cloud::Cloud; #[cfg(feature = "builders")] pub use crate::cloud::CloudBuilder; pub use crate::enclosure::Enclosure; #[cfg(feature = "builders")] pub use crate::enclosure::EnclosureBuilder; pub use crate::guid::Guid; #[cfg(feature = "builders")] pub use crate::guid::GuidBuilder; pub use crate::image::Image; #[cfg(feature = "builders")] pub use crate::image::ImageBuilder; pub use crate::item::Item; #[cfg(feature = "builders")] pub use crate::item::ItemBuilder; pub use crate::source::Source; #[cfg(feature = "builders")] pub use crate::source::SourceBuilder; pub use crate::textinput::TextInput; #[cfg(feature = "builders")] pub use crate::textinput::TextInputBuilder; pub use crate::error::Error; rss-2.0.11/src/source.rs000064400000000000000000000071661046102023000131630ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::{BufRead, Write}; use quick_xml::events::attributes::Attributes; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::error::Error; use crate::toxml::ToXml; use crate::util::{attr_value, decode, element_text}; /// Represents the source of an RSS item. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct Source { /// The URL of the source. pub url: String, /// The title of the source. pub title: Option, } impl Source { /// Return the URL of this source. /// /// # Examples /// /// ``` /// use rss::Source; /// /// let mut source = Source::default(); /// source.set_url("http://example.com"); /// assert_eq!(source.url(), "http://example.com"); /// ``` pub fn url(&self) -> &str { self.url.as_str() } /// Set the URL of this source. /// /// # Examples /// /// ``` /// use rss::Source; /// /// let mut source = Source::default(); /// source.set_url("http://example.com"); /// ``` pub fn set_url(&mut self, url: V) where V: Into, { self.url = url.into(); } /// Return the title of this source. /// /// # Examples /// /// ``` /// use rss::Source; /// /// let mut source = Source::default(); /// source.set_title("Source Title".to_string()); /// assert_eq!(source.title(), Some("Source Title")); /// ``` pub fn title(&self) -> Option<&str> { self.title.as_deref() } /// Set the title of this source. /// /// # Examples /// /// ``` /// use rss::Source; /// /// let mut source = Source::default(); /// source.set_title("Source Title".to_string()); /// ``` pub fn set_title(&mut self, title: V) where V: Into>, { self.title = title.into(); } } impl Source { /// Builds a Source from source XML pub fn from_xml( reader: &mut Reader, mut atts: Attributes, ) -> Result { let mut source = Source::default(); for attr in atts.with_checks(false).flatten() { if decode(attr.key.as_ref(), reader)?.as_ref() == "url" { source.url = attr_value(&attr, reader)?.to_string(); break; } } source.title = element_text(reader)?; Ok(source) } } impl ToXml for Source { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "source"; let mut element = BytesStart::new(name); element.push_attribute(("url", &*self.url)); writer.write_event(Event::Start(element))?; if let Some(ref text) = self.title { writer.write_event(Event::Text(BytesText::new(text)))?; } writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } } #[cfg(feature = "builders")] impl SourceBuilder { /// Builds a new `Source`. pub fn build(&self) -> Source { self.build_impl().unwrap() } } rss-2.0.11/src/textinput.rs000064400000000000000000000140571046102023000137240ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::io::{BufRead, Write}; use quick_xml::events::attributes::Attributes; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::Error as XmlError; use quick_xml::Reader; use quick_xml::Writer; use crate::error::Error; use crate::toxml::{ToXml, WriterExt}; use crate::util::{decode, element_text, skip}; /// Represents a text input for an RSS channel. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "builders", derive(Builder))] #[cfg_attr( feature = "builders", builder( setter(into), default, build_fn(name = "build_impl", private, error = "never::Never") ) )] pub struct TextInput { /// The label of the Submit button for the text input. pub title: String, /// A description of the text input. pub description: String, /// The name of the text object. pub name: String, /// The URL of the CGI script that processes the text input request. pub link: String, } impl TextInput { /// Return the title for this text field. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_title("Input Title"); /// assert_eq!(text_input.title(), "Input Title"); /// ``` pub fn title(&self) -> &str { self.title.as_str() } /// Set the title for this text field. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_title("Input Title"); /// ``` pub fn set_title(&mut self, title: V) where V: Into, { self.title = title.into(); } /// Return the description of this text field. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_description("Input description"); /// assert_eq!(text_input.description(), "Input description"); /// ``` pub fn description(&self) -> &str { self.description.as_str() } /// Set the description of this text field. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_description("Input description"); /// ``` pub fn set_description(&mut self, description: V) where V: Into, { self.description = description.into(); } /// Return the name of the text object in this input. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_name("Input name"); /// assert_eq!(text_input.name(), "Input name"); /// ``` pub fn name(&self) -> &str { self.name.as_str() } /// Set the name of the text object in this input. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_name("Input name");; /// ``` pub fn set_name(&mut self, name: V) where V: Into, { self.name = name.into(); } /// Return the URL of the GCI script that processes the text input request. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_link("http://example.com/submit"); /// assert_eq!(text_input.link(), "http://example.com/submit"); /// ``` pub fn link(&self) -> &str { self.link.as_str() } /// Set the URL of the GCI script that processes the text input request. /// /// # Examples /// /// ``` /// use rss::TextInput; /// /// let mut text_input = TextInput::default(); /// text_input.set_link("http://example.com/submit"); /// ``` pub fn set_link(&mut self, link: V) where V: Into, { self.link = link.into(); } } impl TextInput { /// Builds a TextInput from source XML pub fn from_xml(reader: &mut Reader, _: Attributes) -> Result { let mut text_input = TextInput::default(); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => match decode(element.name().as_ref(), reader)?.as_ref() { "title" => text_input.title = element_text(reader)?.unwrap_or_default(), "description" => { text_input.description = element_text(reader)?.unwrap_or_default() } "name" => text_input.name = element_text(reader)?.unwrap_or_default(), "link" => text_input.link = element_text(reader)?.unwrap_or_default(), _ => skip(element.name(), reader)?, }, Event::End(_) => break, Event::Eof => return Err(Error::Eof), _ => {} } buf.clear(); } Ok(text_input) } } impl ToXml for TextInput { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { let name = "textInput"; writer.write_event(Event::Start(BytesStart::new(name)))?; writer.write_text_element("title", &self.title)?; writer.write_text_element("description", &self.description)?; writer.write_text_element("name", &self.name)?; writer.write_text_element("link", &self.link)?; writer.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } } #[cfg(feature = "builders")] impl TextInputBuilder { /// Builds a new `TextInput`. pub fn build(&self) -> TextInput { self.build_impl().unwrap() } } rss-2.0.11/src/toxml.rs000064400000000000000000000060131046102023000130140ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::BTreeMap; use std::io::Write; use quick_xml::events::{BytesCData, BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Error as XmlError; use quick_xml::Writer; pub trait ToXml { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError>; fn used_namespaces(&self) -> BTreeMap { BTreeMap::new() } } impl<'a, T: ToXml> ToXml for &'a T { fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { (*self).to_xml(writer) } } pub trait WriterExt { fn write_text_element(&mut self, name: N, text: T) -> Result<(), XmlError> where N: AsRef, T: AsRef; fn write_text_elements(&mut self, name: N, values: I) -> Result<(), XmlError> where N: AsRef, T: AsRef, I: IntoIterator; fn write_cdata_element(&mut self, name: N, text: T) -> Result<(), XmlError> where N: AsRef, T: AsRef; fn write_object(&mut self, object: T) -> Result<(), XmlError> where T: ToXml; fn write_objects(&mut self, objects: I) -> Result<(), XmlError> where T: ToXml, I: IntoIterator; } impl WriterExt for Writer { fn write_text_element(&mut self, name: N, text: T) -> Result<(), XmlError> where N: AsRef, T: AsRef, { let name = name.as_ref(); self.write_event(Event::Start(BytesStart::new(name)))?; self.write_event(Event::Text(BytesText::new(text.as_ref())))?; self.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } fn write_text_elements(&mut self, name: N, values: I) -> Result<(), XmlError> where N: AsRef, T: AsRef, I: IntoIterator, { for value in values { self.write_text_element(&name, value)?; } Ok(()) } fn write_cdata_element(&mut self, name: N, text: T) -> Result<(), XmlError> where N: AsRef, T: AsRef, { let name = name.as_ref(); self.write_event(Event::Start(BytesStart::new(name)))?; BytesCData::escaped(text.as_ref()) .try_for_each(|event| self.write_event(Event::CData(event)))?; self.write_event(Event::End(BytesEnd::new(name)))?; Ok(()) } #[inline] fn write_object(&mut self, object: T) -> Result<(), XmlError> where T: ToXml, { object.to_xml(self) } fn write_objects(&mut self, objects: I) -> Result<(), XmlError> where T: ToXml, I: IntoIterator, { for object in objects { object.to_xml(self)?; } Ok(()) } } rss-2.0.11/src/util.rs000064400000000000000000000033421046102023000126300ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::borrow::Cow; use std::io::BufRead; use quick_xml::events::attributes::Attribute; use quick_xml::events::Event; use quick_xml::name::QName; use quick_xml::Reader; use crate::error::Error; pub(crate) fn decode<'s, B: BufRead>( bytes: &'s [u8], reader: &Reader, ) -> Result, Error> { let text = reader.decoder().decode(bytes)?; Ok(text) } pub(crate) fn attr_value<'s, B: BufRead>( attr: &'s Attribute<'s>, reader: &Reader, ) -> Result, Error> { let value = attr.decode_and_unescape_value(reader.decoder())?; Ok(value) } pub(crate) fn skip(end: QName<'_>, reader: &mut Reader) -> Result<(), Error> { reader.read_to_end_into(end, &mut Vec::new())?; Ok(()) } pub fn element_text(reader: &mut Reader) -> Result, Error> { let mut content = String::new(); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf)? { Event::Start(element) => { skip(element.name(), reader)?; } Event::Text(element) => { let decoded = element.unescape()?; content.push_str(decoded.as_ref()); } Event::CData(element) => { content.push_str(decode(&element, reader)?.as_ref()); } Event::End(_) | Event::Eof => break, _ => {} } buf.clear(); } Ok(Some(content.trim().to_owned()).filter(|c| !c.is_empty())) } rss-2.0.11/src/validation.rs000064400000000000000000000157741046102023000140210ustar 00000000000000// This file is part of rss. // // Copyright © 2015-2021 The rust-syndication Developers // // This program is free software; you can redistribute it and/or modify // it under the terms of the MIT License and/or Apache 2.0 License. use std::collections::HashSet; use std::error::Error as StdError; use std::fmt; use std::num::ParseIntError; use chrono::DateTime; use chrono::ParseError as DateParseError; use mime::FromStrError as MimeParseError; use mime::Mime; use url::ParseError as UrlParseError; use url::Url; use crate::{Category, Channel, Cloud, Enclosure, Image, Item, Source, TextInput}; #[derive(Debug)] /// Errors that occur during validation. pub enum ValidationError { /// An error while parsing a string to a date. DateParsing(DateParseError), /// An error while parsing a string to an integer. IntParsing(ParseIntError), /// An error while parsing a string to a URL. UrlParsing(UrlParseError), /// An error while parsing a string to a MIME type. MimeParsing(MimeParseError), /// A different validation error. Validation(String), } impl StdError for ValidationError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match *self { ValidationError::DateParsing(ref err) => Some(err), ValidationError::IntParsing(ref err) => Some(err), ValidationError::UrlParsing(ref err) => Some(err), _ => None, } } } impl fmt::Display for ValidationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { ValidationError::DateParsing(ref err) => err.fmt(f), ValidationError::IntParsing(ref err) => err.fmt(f), ValidationError::UrlParsing(ref err) => err.fmt(f), ValidationError::MimeParsing(_) => write!(f, "Unable to parse MIME type"), ValidationError::Validation(ref s) => write!(f, "{}", s), } } } impl From for ValidationError { fn from(err: DateParseError) -> Self { ValidationError::DateParsing(err) } } impl From for ValidationError { fn from(err: ParseIntError) -> Self { ValidationError::IntParsing(err) } } impl From for ValidationError { fn from(err: UrlParseError) -> Self { ValidationError::UrlParsing(err) } } impl From for ValidationError { fn from(err: MimeParseError) -> Self { ValidationError::MimeParsing(err) } } /// A trait to support data validation. pub trait Validate { /// Validate the data against the RSS specification. fn validate(&self) -> Result<(), ValidationError>; } macro_rules! validate { ($e: expr, $msg: expr) => {{ if !($e) { return Err(ValidationError::Validation(String::from($msg))); } }}; } impl Validate for Channel { fn validate(&self) -> Result<(), ValidationError> { Url::parse(self.link())?; for category in self.categories() { category.validate()?; } if let Some(cloud) = self.cloud() { cloud.validate()?; } if let Some(docs) = self.docs() { Url::parse(docs)?; } if let Some(image) = self.image() { image.validate()?; } for item in self.items() { item.validate()?; } if let Some(last_build_date) = self.last_build_date() { DateTime::parse_from_rfc2822(last_build_date)?; } if let Some(pub_date) = self.pub_date() { DateTime::parse_from_rfc2822(pub_date)?; } for hour in self.skip_hours() { let hour = hour.parse::()?; validate!( (0..=23).contains(&hour), "Channel skip hour is not between 0 and 23" ); } let valid_days = { let mut set = HashSet::with_capacity(7); set.insert("Monday"); set.insert("Tuesday"); set.insert("Wednesday"); set.insert("Thursday"); set.insert("Friday"); set.insert("Saturday"); set.insert("Sunday"); set }; for day in self.skip_days() { validate!( valid_days.contains(day.as_str()), format!("Unknown skip day: {}", day) ); } if let Some(text_input) = self.text_input() { text_input.validate()?; } if let Some(ttl) = self.ttl() { let ttl = ttl.parse::()?; validate!(ttl > 0, "Channel TTL is not greater than 0"); } Ok(()) } } impl Validate for Category { fn validate(&self) -> Result<(), ValidationError> { if let Some(domain) = self.domain() { Url::parse(domain)?; } Ok(()) } } impl Validate for Cloud { fn validate(&self) -> Result<(), ValidationError> { let port = self.port().parse::()?; validate!(port > 0, "Cloud port must be greater than 0"); Url::parse(self.domain())?; validate!( vec!["xml-rpc", "soap", "http-post"].contains(&self.protocol()), format!("Unknown cloud protocol: {}", self.protocol()) ); Ok(()) } } impl Validate for Enclosure { fn validate(&self) -> Result<(), ValidationError> { Url::parse(self.url())?; self.mime_type().parse::()?; let length = self.length().parse::()?; validate!(length > 0, "Enclosure length is not greater than 0"); Ok(()) } } impl Validate for TextInput { fn validate(&self) -> Result<(), ValidationError> { Url::parse(self.link())?; Ok(()) } } impl Validate for Image { fn validate(&self) -> Result<(), ValidationError> { Url::parse(self.link())?; Url::parse(self.url())?; if let Some(width) = self.width() { let width = width.parse::()?; validate!( (0..=144).contains(&width), "Image width is not between 0 and 144" ); } if let Some(height) = self.height() { let height = height.parse::()?; validate!( (0..=144).contains(&height), "Image height is not between 0 and 144" ); } Ok(()) } } impl Validate for Item { fn validate(&self) -> Result<(), ValidationError> { if let Some(link) = self.link() { Url::parse(link)?; } if let Some(comments) = self.comments() { Url::parse(comments)?; } if let Some(enclosure) = self.enclosure() { enclosure.validate()?; } if let Some(pub_date) = self.pub_date() { DateTime::parse_from_rfc2822(pub_date)?; } if let Some(source) = self.source() { source.validate()?; } Ok(()) } } impl Validate for Source { fn validate(&self) -> Result<(), ValidationError> { Url::parse(self.url())?; Ok(()) } }