diff --git a/Cargo.toml b/Cargo.toml index af6add5..940f411 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sitemap-rs" -version = "0.1.1" +version = "0.1.2" edition = "2021" rust-version = "1.63" authors = ["Todd Everett Griffin "] diff --git a/README.md b/README.md index 504fe35..92df81f 100644 --- a/README.md +++ b/README.md @@ -264,17 +264,6 @@ It is by far fast enough for my use-cases, so I didn't have to reach for anythin If you like what this library provides, but simply need the ability to parse sitemaps and could also use a speed boost - please consider pushing a pull request! (Preferably one that replaces `xml-builder` with `quick-xml` lol) -#### Builder pattern - -Currently, the only way to create instances of each struct is by using `::new()`. -By implementing the [Builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) on each struct, less work has to be done -on the library users' side as they don't have to throw in many `None` values for each optional field they don't want to use. -Not only would this make the library more ergonomic to use, but it would vastly improve readability (specifically at -each struct initialization point). - -This hasn't been prioritized yet as I am currently satisfied with `::new()` for my use cases. -Pull requests are welcome! - #### Codified country codes In video sitemaps, there is a tag called `` where the text is a space-delimited list of country codes diff --git a/examples/generate_image_sitemap.rs b/examples/generate_image_sitemap.rs index 576ac46..3fe84cd 100644 --- a/examples/generate_image_sitemap.rs +++ b/examples/generate_image_sitemap.rs @@ -5,31 +5,19 @@ use std::path::PathBuf; fn main() { let urls: Vec = vec![ - Url::new( - String::from("http://example.com/sample1.html"), - None, - None, - None, - Some(vec![ + Url::builder(String::from("http://example.com/sample1.html")) + .images(vec![ Image::new(String::from("http://example.com/image.jpg")), Image::new(String::from("http://example.com/photo.jpg")), - ]), - None, - None, - ) - .expect("failed a validation"), - Url::new( - String::from("http://example.com/sample2.html"), - None, - None, - None, - Some(vec![Image::new(String::from( + ]) + .build() + .expect("failed a validation"), + Url::builder(String::from("http://example.com/sample2.html")) + .images(vec![Image::new(String::from( "http://example.com/picture.jpg", - ))]), - None, - None, - ) - .expect("failed a validation"), + ))]) + .build() + .expect("failed a validation"), ]; let url_set: UrlSet = UrlSet::new(urls).expect("failed a validation"); diff --git a/examples/generate_news_sitemap.rs b/examples/generate_news_sitemap.rs index 2d23378..066887d 100644 --- a/examples/generate_news_sitemap.rs +++ b/examples/generate_news_sitemap.rs @@ -5,22 +5,18 @@ use sitemap_rs::url_set::UrlSet; use std::path::PathBuf; fn main() { - let urls: Vec = vec![Url::new( - String::from("http://www.example.org/business/article55.html"), - None, - None, - None, - None, - None, - Some(News::new( - Publication::new(String::from("The Example Times"), String::from("en")), - DateTime::from_utc( - NaiveDate::from_ymd(2008, 12, 23).and_hms(0, 0, 0), - FixedOffset::east(0), - ), - String::from("Companies A, B in Merger Talks"), - )), - ) + let urls: Vec = vec![Url::builder(String::from( + "http://www.example.org/business/article55.html", + )) + .news(News::new( + Publication::new(String::from("The Example Times"), String::from("en")), + DateTime::from_utc( + NaiveDate::from_ymd(2008, 12, 23).and_hms(0, 0, 0), + FixedOffset::east(0), + ), + String::from("Companies A, B in Merger Talks"), + )) + .build() .expect("failed a validation")]; let url_set: UrlSet = UrlSet::new(urls).expect("failed a validation"); diff --git a/examples/generate_url_sitemap.rs b/examples/generate_url_sitemap.rs index ea21218..cfeada1 100644 --- a/examples/generate_url_sitemap.rs +++ b/examples/generate_url_sitemap.rs @@ -4,19 +4,15 @@ use sitemap_rs::url_set::UrlSet; use std::path::PathBuf; fn main() { - let urls: Vec = vec![Url::new( - String::from("http://www.example.com/"), - Some(DateTime::from_utc( + let urls: Vec = vec![Url::builder(String::from("http://www.example.com/")) + .last_modified(DateTime::from_utc( NaiveDate::from_ymd(2005, 1, 1).and_hms(0, 0, 0), FixedOffset::east(0), - )), - Some(ChangeFrequency::Monthly), - Some(0.8), - None, - None, - None, - ) - .expect("failed a validation")]; + )) + .change_frequency(ChangeFrequency::Monthly) + .priority(0.8) + .build() + .expect("failed a validation")]; let url_set: UrlSet = UrlSet::new(urls).expect("failed a validation"); url_set diff --git a/examples/generate_video_sitemap.rs b/examples/generate_video_sitemap.rs index 38495bf..1942e0e 100644 --- a/examples/generate_video_sitemap.rs +++ b/examples/generate_video_sitemap.rs @@ -6,61 +6,60 @@ use std::collections::HashSet; use std::path::PathBuf; fn main() { - let urls: Vec = vec![Url::new( - String::from("http://www.example.com/videos/some_video_landing_page.html"), - None, - None, - None, - None, - Some(vec![Video::new( - String::from("http://www.example.com/thumbs/123.jpg"), - String::from("Grilling steaks for summer"), - String::from("Alkis shows you how to get perfectly done steaks every time"), - String::from("http://streamserver.example.com/video123.mp4"), - String::from("http://www.example.com/videoplayer.php?video=123"), - Some(600), - Some(DateTime::from_utc( - NaiveDate::from_ymd(2021, 11, 5).and_hms(11, 20, 30), - FixedOffset::east(8 * 3600), - )), - Some(4.2), - Some(12345), - Some(DateTime::from_utc( - NaiveDate::from_ymd(2007, 11, 5).and_hms(11, 20, 30), - FixedOffset::east(8 * 3600), - )), - Some(true), - Some(Restriction::new( - HashSet::from([ - String::from("IE"), - String::from("GB"), - String::from("US"), - String::from("CA"), - ]), - Relationship::Allow, - )), - Some(Platform::new( - HashSet::from([PlatformType::Web, PlatformType::Tv]), - Relationship::Allow, - )), - Some(true), - Some(Uploader::new( - String::from("GrillyMcGrillserson"), - Some(String::from( - "http://www.example.com/users/grillymcgrillerson", - )), - )), - Some(false), - Some(vec![ - String::from("steak"), - String::from("meat"), - String::from("summer"), - String::from("outdoor"), - ]), - ) - .expect("failed a validation")]), - None, + let video: Video = Video::builder( + String::from("http://www.example.com/thumbs/123.jpg"), + String::from("Grilling steaks for summer"), + String::from("Alkis shows you how to get perfectly done steaks every time"), + String::from("http://streamserver.example.com/video123.mp4"), + String::from("http://www.example.com/videoplayer.php?video=123"), ) + .duration(600) + .expiration_date(DateTime::from_utc( + NaiveDate::from_ymd(2021, 11, 5).and_hms(11, 20, 30), + FixedOffset::east(8 * 3600), + )) + .rating(4.2) + .view_count(12345) + .publication_date(DateTime::from_utc( + NaiveDate::from_ymd(2007, 11, 5).and_hms(11, 20, 30), + FixedOffset::east(8 * 3600), + )) + .family_friendly(true) + .restriction(Restriction::new( + HashSet::from([ + String::from("IE"), + String::from("GB"), + String::from("US"), + String::from("CA"), + ]), + Relationship::Allow, + )) + .platform(Platform::new( + HashSet::from([PlatformType::Web, PlatformType::Tv]), + Relationship::Allow, + )) + .requires_subscription(true) + .uploader(Uploader::new( + String::from("GrillyMcGrillserson"), + Some(String::from( + "http://www.example.com/users/grillymcgrillerson", + )), + )) + .live(false) + .tags(vec![ + String::from("steak"), + String::from("meat"), + String::from("summer"), + String::from("outdoor"), + ]) + .build() + .expect("failed a validation"); + + let urls: Vec = vec![Url::builder(String::from( + "http://www.example.com/videos/some_video_landing_page.html", + )) + .videos(vec![video]) + .build() .expect("failed a validation")]; let url_set: UrlSet = UrlSet::new(urls).expect("failed a validation"); diff --git a/src/lib.rs b/src/lib.rs index 137e405..8c7c489 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,19 +10,15 @@ //! use sitemap_rs::url_set::UrlSet; //! use std::path::PathBuf; //! -//! let urls: Vec = vec![Url::new( -//! String::from("http://www.example.com/"), -//! Some(DateTime::from_utc( +//! let urls: Vec = vec![Url::builder(String::from("http://www.example.com/")) +//! .last_modified(DateTime::from_utc( //! NaiveDate::from_ymd(2005, 1, 1).and_hms(0, 0, 0), //! FixedOffset::east(0), -//! )), -//! Some(ChangeFrequency::Monthly), -//! Some(0.8), -//! None, -//! None, -//! None, -//! ) -//! .expect("failed a validation")]; +//! )) +//! .change_frequency(ChangeFrequency::Monthly) +//! .priority(0.8) +//! .build() +//! .expect("failed a validation")]; //! //! let url_set: UrlSet = UrlSet::new(urls).expect("failed a validation"); //! url_set @@ -53,10 +49,12 @@ pub mod sitemap; pub mod sitemap_index; pub mod sitemap_index_error; pub mod url; +pub mod url_builder; pub mod url_error; pub mod url_set; pub mod url_set_error; pub mod video; +pub mod video_builder; pub mod video_error; pub const NAMESPACE: &str = "http://www.sitemaps.org/schemas/sitemap/0.9"; diff --git a/src/sitemap.rs b/src/sitemap.rs index b71e927..87fd350 100644 --- a/src/sitemap.rs +++ b/src/sitemap.rs @@ -40,15 +40,11 @@ impl Sitemap { sitemap.add_child(loc)?; // add , if it exists - match self.last_modified { - None => (), - Some(last_modified) => { - let mut last_mod: XMLElement = XMLElement::new("lastmod"); - last_mod.add_text( - last_modified.to_rfc3339_opts(RFC_3339_SECONDS_FORMAT, RFC_3339_USE_Z), - )?; - sitemap.add_child(last_mod)?; - } + if let Some(last_modified) = self.last_modified { + let mut last_mod: XMLElement = XMLElement::new("lastmod"); + last_mod + .add_text(last_modified.to_rfc3339_opts(RFC_3339_SECONDS_FORMAT, RFC_3339_USE_Z))?; + sitemap.add_child(last_mod)?; } Ok(sitemap) diff --git a/src/url.rs b/src/url.rs index c003351..b2a9028 100644 --- a/src/url.rs +++ b/src/url.rs @@ -1,5 +1,6 @@ use crate::image::Image; use crate::news::News; +use crate::url_builder::UrlBuilder; use crate::url_error::UrlError; use crate::video::Video; use crate::{RFC_3339_SECONDS_FORMAT, RFC_3339_USE_Z}; @@ -74,15 +75,12 @@ impl Url { news: Option, ) -> Result { // make sure priority is within bounds: 0.0 <= priority <= 1.0 - match priority { - None => (), - Some(p) => { - if p < 0.0 { - return Err(UrlError::PriorityTooLow(p)); - } - if p > 1.0 { - return Err(UrlError::PriorityTooHigh(p)); - } + if let Some(p) = priority { + if p < 0.0 { + return Err(UrlError::PriorityTooLow(p)); + } + if p > 1.0 { + return Err(UrlError::PriorityTooHigh(p)); } } @@ -113,6 +111,11 @@ impl Url { }) } + #[must_use] + pub const fn builder(location: String) -> UrlBuilder { + UrlBuilder::new(location) + } + /// # Errors /// /// Will return `XMLError` if there is a problem creating XML elements. @@ -125,63 +128,44 @@ impl Url { url.add_child(loc)?; // add , if it exists - match self.last_modified { - None => (), - Some(last_modified) => { - let mut lastmod: XMLElement = XMLElement::new("lastmod"); - lastmod.add_text( - last_modified.to_rfc3339_opts(RFC_3339_SECONDS_FORMAT, RFC_3339_USE_Z), - )?; - url.add_child(lastmod)?; - } + if let Some(last_modified) = self.last_modified { + let mut lastmod: XMLElement = XMLElement::new("lastmod"); + lastmod + .add_text(last_modified.to_rfc3339_opts(RFC_3339_SECONDS_FORMAT, RFC_3339_USE_Z))?; + url.add_child(lastmod)?; } // add , if it exists - match self.change_frequency { - None => (), - Some(change_frequency) => { - let mut changefreq: XMLElement = XMLElement::new("changefreq"); - changefreq.add_text(change_frequency.to_string())?; - url.add_child(changefreq)?; - } + if let Some(change_frequency) = self.change_frequency { + let mut changefreq: XMLElement = XMLElement::new("changefreq"); + changefreq.add_text(change_frequency.to_string())?; + url.add_child(changefreq)?; } // add , if it exists - match self.priority { - None => (), - Some(p) => { - let mut priority: XMLElement = XMLElement::new("priority"); - priority.add_text(p.to_string())?; - url.add_child(priority)?; - } + if let Some(p) = self.priority { + let mut priority: XMLElement = XMLElement::new("priority"); + priority.add_text(p.to_string())?; + url.add_child(priority)?; } // add , if any exist - match self.images { - None => (), - Some(images) => { - for image in images { - url.add_child(image.to_xml()?)?; - } + if let Some(images) = self.images { + for image in images { + url.add_child(image.to_xml()?)?; } } // add , if any exist - match self.videos { - None => (), - Some(videos) => { - for video in videos { - url.add_child(video.to_xml()?)?; - } + if let Some(videos) = self.videos { + for video in videos { + url.add_child(video.to_xml()?)?; } } // add , if any exist - match self.news { - None => (), - Some(news) => { - url.add_child(news.to_xml()?)?; - } + if let Some(news) = self.news { + url.add_child(news.to_xml()?)?; } Ok(url) diff --git a/src/url_builder.rs b/src/url_builder.rs new file mode 100644 index 0000000..937e999 --- /dev/null +++ b/src/url_builder.rs @@ -0,0 +1,118 @@ +use crate::image::Image; +use crate::news::News; +use crate::url::{ChangeFrequency, Url}; +use crate::url_error::UrlError; +use crate::video::Video; +use chrono::{DateTime, FixedOffset}; + +/// A \ entry within a sitemap.xml. +/// +/// This is a Builder for Url. +#[derive(Debug, Clone)] +pub struct UrlBuilder { + /// URL of the page. + /// + /// This URL must begin with the protocol (such as http) and end with a trailing slash, if your web server requires it. + /// This value must be less than 2,048 characters. + pub location: String, + + /// The date of last modification of the page. + /// + /// This date should be in W3C Datetime format. + /// This format allows you to omit the time portion, if desired, and use YYYY-MM-DD. + /// Note that the date must be set to the date the linked page was last modified, not when the sitemap is generated. + /// Note also that this tag is separate from the If-Modified-Since (304) header the server can return, and search engines may use the information from both sources differently. + pub last_modified: Option>, + + /// How frequently the page is likely to change. + /// + /// This value provides general information to search engines and may not correlate exactly to how often they crawl the page. + /// The value "always" should be used to describe documents that change each time they are accessed. + /// The value "never" should be used to describe archived URLs. + /// Please note that the value of this tag is considered a hint and not a command. + /// Even though search engine crawlers may consider this information when making decisions, they may crawl pages marked "hourly" less frequently than that, and they may crawl pages marked "yearly" more frequently than that. + /// Crawlers may periodically crawl pages marked "never" so that they can handle unexpected changes to those pages. + pub change_frequency: Option, + + /// The priority of this URL relative to other URLs on your site. + /// + /// Valid values range from 0.0 to 1.0. + /// This value does not affect how your pages are compared to pages on other sites—it only lets the search engines know which pages you deem most important for the crawlers. + /// The default priority of a page is 0.5. + /// Please note that the priority you assign to a page is not likely to influence the position of your URLs in a search engine's result pages. + /// Search engines may use this information when selecting between URLs on the same site, so you can use this tag to increase the likelihood that your most important pages are present in a search index. + /// Also, please note that assigning a high priority to all the URLs on your site is not likely to help you. + /// Since the priority is relative, it is only used to select between URLs on your site. + pub priority: Option, + + /// Images associated with this URL. + pub images: Option>, + + /// Videos associated with this URL. + pub videos: Option>, + + /// News associated with this URL. + pub news: Option, +} + +impl UrlBuilder { + #[must_use] + pub const fn new(location: String) -> Self { + Self { + location, + last_modified: None, + change_frequency: None, + priority: None, + images: None, + videos: None, + news: None, + } + } + + pub fn last_modified(&mut self, last_modified: DateTime) -> &mut Self { + self.last_modified = Some(last_modified); + self + } + + pub fn change_frequency(&mut self, change_frequency: ChangeFrequency) -> &mut Self { + self.change_frequency = Some(change_frequency); + self + } + + pub fn priority(&mut self, priority: f32) -> &mut Self { + self.priority = Some(priority); + self + } + + pub fn images(&mut self, images: Vec) -> &mut Self { + self.images = Some(images); + self + } + + pub fn videos(&mut self, videos: Vec