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};
use chrono::{DateTime, FixedOffset};
use std::fmt::{Display, Formatter};
use xml_builder::{XMLElement, XMLError};
pub const DEFAULT_PRIORITY: f32 = 0.5;
#[derive(Debug, Clone)]
pub struct Url {
pub location: String,
pub links: Vec<Link>,
pub last_modified: Option<DateTime<FixedOffset>>,
pub change_frequency: Option<ChangeFrequency>,
pub priority: Option<f32>,
pub images: Option<Vec<Image>>,
pub videos: Option<Vec<Video>>,
pub news: Option<News>,
}
impl Url {
#[expect(clippy::too_many_arguments)]
pub fn new(
location: String,
links: Vec<Link>,
last_modified: Option<DateTime<FixedOffset>>,
change_frequency: Option<ChangeFrequency>,
priority: Option<f32>,
images: Option<Vec<Image>>,
videos: Option<Vec<Video>>,
news: Option<News>,
) -> Result<Self, UrlError> {
if location.len() >= 2048 {
return Err(UrlError::LocationTooLong(location));
}
if let Some(p) = priority {
if p < 0.0 {
return Err(UrlError::PriorityTooLow(p));
}
if p > 1.0 {
return Err(UrlError::PriorityTooHigh(p));
}
}
let images: Option<Vec<Image>> = match images {
None => None,
Some(images) => {
if images.len() > 1000 {
return Err(UrlError::TooManyImages(images.len()));
}
if images.is_empty() {
None
} else {
Some(images)
}
}
};
Ok(Self {
location,
links,
last_modified,
change_frequency,
priority,
images,
videos,
news,
})
}
#[must_use]
pub const fn builder(location: String) -> UrlBuilder {
UrlBuilder::new(location)
}
pub fn to_xml(self) -> Result<XMLElement, XMLError> {
let mut url: XMLElement = XMLElement::new("url");
let mut loc: XMLElement = XMLElement::new("loc");
loc.add_text(self.location)?;
url.add_child(loc)?;
for link in self.links {
let mut xhtml_link = XMLElement::new("xhtml:link");
xhtml_link.add_attribute("rel", "alternate");
xhtml_link.add_attribute("hreflang", &link.hreflang);
xhtml_link.add_attribute("href", &link.href);
url.add_child(xhtml_link)?;
}
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)?;
}
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)?;
}
if let Some(p) = self.priority {
let mut priority: XMLElement = XMLElement::new("priority");
priority.add_text(p.to_string())?;
url.add_child(priority)?;
}
if let Some(images) = self.images {
for image in images {
url.add_child(image.to_xml()?)?;
}
}
if let Some(videos) = self.videos {
for video in videos {
url.add_child(video.to_xml()?)?;
}
}
if let Some(news) = self.news {
url.add_child(news.to_xml()?)?;
}
Ok(url)
}
}
#[derive(Debug, Clone)]
pub struct Link {
pub hreflang: String,
pub href: String,
}
impl Link {
#[must_use]
pub fn new(hreflang: String, href: String) -> Self {
Self { hreflang, href }
}
}
#[derive(Debug, Copy, Clone)]
pub enum ChangeFrequency {
Always,
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
Never,
}
impl ChangeFrequency {
#[must_use]
pub const fn as_str(&self) -> &str {
match self {
Self::Always => "always",
Self::Hourly => "hourly",
Self::Daily => "daily",
Self::Weekly => "weekly",
Self::Monthly => "monthly",
Self::Yearly => "yearly",
Self::Never => "never",
}
}
}
impl Display for ChangeFrequency {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}