use std::io::Write; use std::marker::PhantomData; use countio::Counter; use url::Url; use crate::{ build::Builder, record::{BYTE_LIMIT, RECORD_LIMIT}, Error, }; /// Sitemap builder for the simple TXT file that contains one URL per line. /// /// For example: /// /// ```txt /// https://www.example.com/file1.html /// https://www.example.com/file2.html /// ``` /// /// Enforces [total written/read bytes](BYTE_LIMIT) and [total records](RECORD_LIMIT) limits. /// See [Error]. /// /// ```rust /// use sitemapo::{ /// build::{Builder, TxtBuilder}, /// Error, /// }; /// /// fn main() -> Result<(), Error> { /// let buf = Vec::new(); /// let rec = "https://example.com/".try_into()?; /// /// let mut builder = TxtBuilder::new(buf)?; /// builder.write(&rec)?; /// let _buf = builder.close()?; /// Ok(()) /// } /// ``` pub struct TxtBuilder { record_type: PhantomData, pub(crate) writer: Counter, pub(crate) records: usize, } impl TxtBuilder { /// 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 fn into_inner(self) -> W { self.writer.into_inner() } } impl TxtBuilder { /// 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, } } pub(crate) fn create_next_line(&mut self, url: &Url) -> Result, Error> { const NEWLINE: &str = "\n"; if self.records + 1 > RECORD_LIMIT { return Err(Error::EntryLimit { over: 1 }); } let record = url.to_string(); let record_bytes = record.len() + NEWLINE.len(); let total_bytes = self.writer.writer_bytes() + record_bytes; if total_bytes > BYTE_LIMIT { let over_limit = total_bytes - BYTE_LIMIT; return Err(Error::ByteLimit { over: over_limit }); } Ok((record + NEWLINE).into_bytes()) } } impl Builder for TxtBuilder { type Error = Error; fn new(writer: W) -> Result { Ok(Self::from_writer(writer)) } fn write(&mut self, record: &Url) -> Result<(), Self::Error> { let record = self.create_next_line(record)?; self.writer.write_all(&record)?; self.records += 1; Ok(()) } fn close(self) -> Result { Ok(self.into_inner()) } } impl std::fmt::Debug for TxtBuilder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TxtBuilder") .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 url::Url; use crate::{ build::{AsyncBuilder, TxtBuilder}, Error, }; #[async_trait] impl AsyncBuilder for TxtBuilder { type Error = Error; async fn new(writer: W) -> Result { Ok(Self::from_writer(writer)) } async fn write(&mut self, record: &Url) -> Result<(), Self::Error> { let record = self.create_next_line(record)?; self.writer.write_all(&record).await?; self.records += 1; Ok(()) } async fn close(self) -> Result { Ok(self.into_inner()) } } } #[cfg(test)] mod test { use std::io::BufWriter; use url::Url; use crate::build::{Builder, TxtBuilder}; #[test] fn synk() { let buf = Vec::new(); let mut builder = TxtBuilder::new(buf).unwrap(); let url = Url::parse("https://example.com/").unwrap(); builder.write(&url).unwrap(); let buf = builder.close().unwrap(); let exp = String::from_utf8(buf).unwrap(); assert_eq!(url.to_string() + "\n", exp); } #[test] fn synk_with_buf() { let buf = BufWriter::new(Vec::new()); let mut builder = TxtBuilder::new(buf).unwrap(); let url = Url::parse("https://example.com/").unwrap(); builder.write(&url).unwrap(); let buf = builder.close().unwrap(); let buf = buf.into_inner().unwrap(); let exp = String::from_utf8(buf).unwrap(); assert_eq!(url.to_string() + "\n", exp); } } #[cfg(feature = "tokio")] #[cfg(test)] mod tokio_test { use tokio::io::{AsyncWriteExt, BufWriter}; use url::Url; use crate::{ build::{AsyncBuilder, TxtBuilder}, Error, }; #[tokio::test] async fn asynk() -> Result<(), Error> { let buf = Vec::new(); let mut builder = TxtBuilder::new(buf).await?; let url = Url::parse("https://example.com/")?; builder.write(&url).await?; let buf = builder.close().await?; let exp = String::from_utf8(buf); assert_eq!(Ok(url.to_string() + "\n"), exp); Ok(()) } #[tokio::test] async fn asynk_with_buf() -> Result<(), Error> { let buf = BufWriter::new(Vec::new()); let mut builder = TxtBuilder::new(buf).await?; let url = Url::parse("https://example.com/")?; builder.write(&url).await?; let mut buf = builder.close().await?; let _ = buf.flush().await?; let buf = buf.into_inner(); let exp = String::from_utf8(buf); assert_eq!(Ok(url.to_string() + "\n"), exp); Ok(()) } }