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(())
}
}