use std::io::Write; use std::marker::PhantomData; use std::num::NonZeroU8; use countio::Counter; use quick_xml::{events, Writer}; use time::format_description::well_known::{iso8601, Iso8601}; use crate::{ attribute as attr, build::Builder, record::{EntryRecord, IndexRecord, BYTE_LIMIT, RECORD_LIMIT}, Error, }; /// Sitemap builder for the versatile XML file with an optional support of extensions. /// /// For example: /// /// ```xml /// /// /// /// https://www.example.com/foo.html /// 2022-06-04 /// /// /// ``` /// Enforces [total written/read bytes](BYTE_LIMIT) and [total records](RECORD_LIMIT) limits. /// See [Error]. /// /// ```rust /// use sitemapo::{ /// build::{Builder, XmlBuilder}, /// record::EntryRecord, /// Error, /// }; /// /// fn main() -> Result<(), Error> { /// let buf = Vec::new(); /// let rec = EntryRecord::new("https://example.com/".try_into()?); /// /// let mut builder = XmlBuilder::new(buf)?; /// builder.write(&rec)?; /// let _buf = builder.close()?; /// Ok(()) /// } /// ``` pub struct XmlBuilder { record_type: PhantomData, pub(crate) writer: Counter, pub(crate) records: usize, } const CONFIG: iso8601::EncodedConfig = iso8601::Config::DEFAULT .set_time_precision(iso8601::TimePrecision::Second { decimal_digits: NonZeroU8::new(2), }) .encode(); impl XmlBuilder { /// Creates a new instance with a provided writer. pub(crate) fn from_writer(writer: W) -> Self { Self { record_type: PhantomData, writer: Counter::new(writer), records: 0, } } /// Returns a reference to the underlying writer. pub fn get_ref(&self) -> &W { self.writer.get_ref() } /// Returns a mutable reference to the underlying writer. pub fn get_mut(&mut self) -> &mut W { self.writer.get_mut() } /// Returns an underlying writer. pub(crate) fn into_inner(self) -> W { self.writer.into_inner() } fn create_open_tag(&mut self, tag: &str) -> Result, Error> { let mut temp = Writer::new(Vec::new()); temp.write_bom()?; // let decl = events::BytesDecl::new("1.0", Some("UTF-8"), None); temp.write_event(events::Event::Decl(decl))?; // // const XMLNS: [(&str, &str); 1] = [("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")]; let tag = events::BytesStart::new(tag); let tag = tag.with_attributes(XMLNS); temp.write_event(events::Event::Start(tag))?; Ok(temp.into_inner()) } fn create_close_tag(&mut self, tag: &str) -> Result, Error> { let mut temp = Writer::new(Vec::new()); // // let tag = events::BytesEnd::new(tag); temp.write_event(events::Event::End(tag))?; Ok(temp.into_inner()) } } impl XmlBuilder { pub(crate) fn create_entry_open(&mut self) -> Result, Error> { self.create_open_tag(attr::URL_SET) } pub(crate) fn create_entry_record(&mut self, record: &EntryRecord) -> Result, Error> { if self.records + 1 > RECORD_LIMIT { return Err(Error::EntryLimit { over: 1 }); } let format = &Iso8601::<{ CONFIG }>; let location = record.location.as_ref().unwrap().to_string(); let modified = record.modified.map(|u| u.format(format).expect("Iso8601")); let priority = record.priority.as_ref().map(|u| u.to_string()); let frequency = record.frequency.as_ref().map(|u| u.to_string()); let mut temp = Writer::new(Vec::new()); let element = temp.create_element(attr::URL); let _ = element.write_inner_content(|writer| { let tag = writer.create_element(attr::LOCATION); tag.write_text_content(events::BytesText::new(&location))?; if let Some(modified) = modified { let tag = writer.create_element(attr::LAST_MODIFIED); tag.write_text_content(events::BytesText::new(&modified))?; } if let Some(priority) = priority { let tag = writer.create_element(attr::PRIORITY); tag.write_text_content(events::BytesText::new(&priority))?; } if let Some(frequency) = frequency { let tag = writer.create_element(attr::CHANGE_FREQUENCY); tag.write_text_content(events::BytesText::new(&frequency))?; } Ok(()) })?; let buf = temp.into_inner(); if buf.len() > BYTE_LIMIT { let over_limit = buf.len() - BYTE_LIMIT; return Err(Error::ByteLimit { over: over_limit }); } Ok(buf) } pub(crate) fn create_entry_close(&mut self) -> Result, Error> { self.create_close_tag(attr::URL_SET) } } impl XmlBuilder { pub(crate) fn create_index_open(&mut self) -> Result, Error> { self.create_open_tag(attr::SITEMAP_INDEX) } pub(crate) fn create_index_record(&mut self, record: &IndexRecord) -> Result, Error> { if self.records + 1 > RECORD_LIMIT { return Err(Error::EntryLimit { over: 1 }); } let format = &Iso8601::<{ CONFIG }>; let location = record.location.as_ref().unwrap().to_string(); let modified = record.modified.map(|u| u.format(format).expect("Iso8601")); let mut temp = Writer::new(Vec::new()); let element = temp.create_element(attr::SITEMAP); let _ = element.write_inner_content(|writer| { let tag = writer.create_element(attr::LOCATION); tag.write_text_content(events::BytesText::new(&location))?; if let Some(modified) = modified { let tag = writer.create_element(attr::LAST_MODIFIED); tag.write_text_content(events::BytesText::new(&modified))?; } Ok(()) })?; let buf = temp.into_inner(); if buf.len() > BYTE_LIMIT { let over_limit = buf.len() - BYTE_LIMIT; return Err(Error::ByteLimit { over: over_limit }); } Ok(buf) } pub(crate) fn create_index_close(&mut self) -> Result, Error> { self.create_close_tag(attr::SITEMAP_INDEX) } } impl Builder for XmlBuilder { type Error = Error; fn new(writer: W) -> Result { let mut this = Self::from_writer(writer); let temp = this.create_entry_open()?; this.writer.write_all(&temp)?; Ok(this) } fn write(&mut self, record: &EntryRecord) -> Result<(), Self::Error> { let temp = self.create_entry_record(record)?; self.writer.write_all(&temp)?; self.records += 1; Ok(()) } fn close(mut self) -> Result { let temp = self.create_entry_close()?; self.writer.write_all(&temp)?; Ok(self.into_inner()) } } impl Builder for XmlBuilder { type Error = Error; fn new(writer: W) -> Result { let mut this = Self::from_writer(writer); let temp = this.create_index_open()?; this.writer.write_all(&temp)?; Ok(this) } fn write(&mut self, record: &IndexRecord) -> Result<(), Self::Error> { let temp = self.create_index_record(record)?; self.writer.write_all(&temp)?; self.records += 1; Ok(()) } fn close(mut self) -> Result { let temp = self.create_index_close()?; self.writer.write_all(&temp)?; Ok(self.into_inner()) } } impl std::fmt::Debug for XmlBuilder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("XmlBuilder") .field("bytes", &self.writer.writer_bytes()) .field("records", &self.records) .finish() } } #[cfg(feature = "tokio")] #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] mod tokio { use async_trait::async_trait; use tokio::io::{AsyncWrite, AsyncWriteExt}; use crate::record::{EntryRecord, IndexRecord}; use crate::{ build::{AsyncBuilder, XmlBuilder}, Error, }; #[async_trait] impl AsyncBuilder for XmlBuilder { type Error = Error; async fn new(writer: W) -> Result { let mut this = Self::from_writer(writer); let temp = this.create_entry_open()?; this.writer.write_all(&temp).await?; Ok(this) } async fn write(&mut self, record: &EntryRecord) -> Result<(), Self::Error> { let temp = self.create_entry_record(record)?; self.writer.write_all(&temp).await?; self.records += 1; Ok(()) } async fn close(mut self) -> Result { let temp = self.create_entry_close()?; self.writer.write_all(&temp).await?; Ok(self.into_inner()) } } #[async_trait] impl AsyncBuilder for XmlBuilder { type Error = Error; async fn new(writer: W) -> Result { let mut this = Self::from_writer(writer); let temp = this.create_index_open()?; this.writer.write_all(&temp).await?; Ok(this) } async fn write(&mut self, record: &IndexRecord) -> Result<(), Self::Error> { let temp = self.create_index_record(record)?; self.writer.write_all(&temp).await?; self.records += 1; Ok(()) } async fn close(mut self) -> Result { let temp = self.create_index_close()?; self.writer.write_all(&temp).await?; Ok(self.into_inner()) } } } #[cfg(test)] mod test { use std::io::BufWriter; use url::Url; use crate::{ build::{Builder, XmlBuilder}, record::EntryRecord, }; #[test] fn synk() { let buf = Vec::new(); let mut builder = XmlBuilder::new(buf).unwrap(); let url = Url::parse("https://example.com/").unwrap(); let rec = EntryRecord::new(url); builder.write(&rec).unwrap(); let _buf = builder.close().unwrap(); } #[test] fn synk_with_buf() { let buf = BufWriter::new(Vec::new()); let mut builder = XmlBuilder::new(buf).unwrap(); let url = Url::parse("https://example.com/").unwrap(); let rec = EntryRecord::new(url); builder.write(&rec).unwrap(); let _buf = builder.close().unwrap(); } } #[cfg(feature = "tokio")] #[cfg(test)] mod tokio_test { use tokio::io::{AsyncWriteExt, BufWriter}; use url::Url; use crate::{ build::{AsyncBuilder, XmlBuilder}, record::EntryRecord, Error, }; #[tokio::test] async fn asynk() -> Result<(), Error> { let buf = Vec::new(); let mut builder = XmlBuilder::new(buf).await?; let url = Url::parse("https://example.com/")?; let rec = EntryRecord::new(url); builder.write(&rec).await?; let _buf = builder.close().await?; Ok(()) } #[tokio::test] async fn asynk_with_buf() -> Result<(), Error> { let buf = BufWriter::new(Vec::new()); let mut builder = XmlBuilder::new(buf).await?; let url = Url::parse("https://example.com/")?; let rec = EntryRecord::new(url); builder.write(&rec).await?; let mut buf = builder.close().await?; let _ = buf.flush().await?; Ok(()) } }