use std::{
collections::BTreeMap,
fmt::{self, Display},
};
use crate::types::{temporal, Class, KnownClass};
use time::{format_description, OffsetDateTime};
use url::Url;
use super::{Object, Property, PropertyList};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("The feed was not declared with the correct type")]
FeedWrongType,
#[error("The feed did not have a declared name")]
FeedMissingName,
#[error("An entry of this feed was missing a name.")]
ChildItemNeedsSingleName,
#[error("An entry with the property {0:} doesn't have a datetime value set.")]
ChildItemShouldHaveTimestamp(String),
#[error("A feed was missing its root URL.")]
FeedMissingUrl,
#[cfg(feature = "atom_syndication")]
#[error(transparent)]
Atom(#[from] AtomError),
#[cfg(feature = "atom_syndication")]
#[error("The feed's category did not provide a name.")]
FeedCategoryMissingName,
#[cfg(feature = "atom_syndication")]
#[error("The value {0:#?} could not be used as an ATOM datetime.")]
InvalidPropertyValueForDate(Property),
#[cfg(feature = "atom_syndication")]
#[error("The feed needs a time at which it's been updated.")]
FeedMissingUpdateTime,
}
#[cfg(feature = "atom_syndication")]
#[derive(thiserror::Error, Debug)]
pub struct AtomError(String);
#[cfg(feature = "atom_syndication")]
impl From<atom_syndication::Error> for AtomError {
fn from(err: atom_syndication::Error) -> Self {
Self(err.to_string())
}
}
#[cfg(feature = "atom_syndication")]
impl Display for AtomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
pub struct Feed(Object);
impl From<Object> for Feed {
fn from(mut value: Object) -> Self {
value.insert_context_uri();
value.extract_references();
value.insert("type".to_string(), "feed".to_string().try_into().unwrap());
Self(value)
}
}
impl From<Feed> for Object {
fn from(feed: Feed) -> Self {
feed.0
}
}
impl std::ops::DerefMut for Feed {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::ops::Deref for Feed {
type Target = Object;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Feed {
pub fn validate(&self) -> Result<(), crate::Error> {
if self.0.r#type() != Class::Known(KnownClass::Feed) {
return Err(Error::FeedWrongType.into());
}
if self.0.get("name").filter(|pv| pv.is_string()).is_none() {
return Err(Error::FeedMissingName.into());
}
let children = self.0.children();
if !children
.iter()
.all(|child| child.get("name").filter(|pv| pv.is_string()).is_some())
{
return Err(Error::ChildItemNeedsSingleName.into());
}
let uids = children
.iter()
.flat_map(|child| child.get("uid").filter(|pv| pv.is_string() || pv.is_url()))
.cloned()
.collect::<PropertyList>();
if uids.len() != children.len() {
return Err(Error::ChildItemNeedsSingleName.into());
}
if children.iter().all(|child| {
let dtp_opt = child.get("updated");
if let Some(Property::Temporal(temporal::Value::Timestamp(dtp))) = dtp_opt {
dtp.is_stamp()
} else {
true
}
}) {
return Err(Error::ChildItemShouldHaveTimestamp("published".to_string()).into());
}
if children.iter().all(|child| {
let dtp_opt = child.get("published");
if let Some(Property::Temporal(temporal::Value::Timestamp(dtp))) = dtp_opt {
dtp.is_stamp()
} else {
true
}
}) {
return Err(Error::ChildItemShouldHaveTimestamp("updated".to_string()).into());
}
Ok(())
}
fn references(&self) -> BTreeMap<String, Object> {
self.get("references")
.and_then(|ref_value| ref_value.as_object_list().cloned())
.unwrap_or_default()
}
pub fn new(items: Vec<Object>) -> Self {
let mut object = Object::default();
object.set_children(items);
Self(object)
}
}
#[cfg(feature = "atom_syndication")]
impl TryInto<atom_syndication::Person> for Object {
type Error = crate::Error;
fn try_into(self) -> Result<atom_syndication::Person, Self::Error> {
let mut builder = atom_syndication::PersonBuilder::default();
if let Some(value) = self
.get("name")
.and_then(|name_value| name_value.as_string().to_owned())
{
builder.name(value);
}
let email_uri = self
.get("url")
.and_then(|value| value.as_list())
.and_then(|url_values| {
url_values
.iter()
.flat_map(|v| v.as_string())
.find(|value| value.starts_with("mailto:"))
.cloned()
.or_else(|| {
url_values
.iter()
.flat_map(|v| v.as_url())
.find(|value| value.scheme() == "mailto")
.map(|u| u.path().to_string())
})
});
builder.email(email_uri);
let author_uri = self
.get("url")
.and_then(|value| value.as_list())
.and_then(|url_values| {
url_values
.iter()
.flat_map(|v| v.as_url())
.find(|value| value.scheme() == "http" || value.scheme() == "https")
.cloned()
})
.map(|u| u.to_string());
builder.uri(author_uri);
Ok(builder.build())
}
}
#[cfg(feature = "atom_syndication")]
impl TryInto<atom_syndication::Category> for Object {
type Error = crate::Error;
fn try_into(self) -> Result<atom_syndication::Category, Self::Error> {
let mut builder = atom_syndication::CategoryBuilder::default();
if let Some(name) = self.get("name").and_then(|v| v.as_string()) {
builder.term(name.to_owned());
}
builder.scheme(
self.get("url")
.and_then(|v| v.as_url())
.map(|u| u.to_string()),
);
Ok(builder.build())
}
}
#[cfg(feature = "atom_syndication")]
impl TryInto<atom_syndication::Entry> for Object {
type Error = crate::Error;
fn try_into(self) -> Result<atom_syndication::Entry, Self::Error> {
let mut builder = atom_syndication::EntryBuilder::default();
let content_url = self
.get("url")
.and_then(|v| v.as_url())
.cloned()
.ok_or_else(|| crate::Error::Jf2Profile(Error::FeedMissingUrl))?;
if let Some(name) = self.get("name").and_then(|v| v.as_string()) {
let mut title = atom_syndication::TextBuilder::default();
title.r#type(atom_syndication::TextType::Text);
title.value(name);
builder.title(title.build());
}
builder.link(atom_syndication::Link {
href: content_url.to_string(),
rel: "canonical".to_string(),
hreflang: None,
mime_type: None,
title: None,
length: None,
});
builder.id(content_url.to_string());
if let Some(content) = self.get("content") {
let (_ty, value) = if let Some(html) = content.as_html() {
(atom_syndication::TextType::Html, html.to_owned())
} else if let Some(text) = content.as_text() {
(atom_syndication::TextType::Text, text.to_owned())
} else {
(atom_syndication::TextType::Text, Default::default())
};
if let Some(temporal::Value::Timestamp(updated_ts)) =
self.get("updated").and_then(|dtpv| dtpv.as_temporal())
{
builder.updated(temporal_into_feed_date(updated_ts)?);
}
builder.content(if value.is_empty() {
None
} else {
Some(
atom_syndication::ContentBuilder::default()
.src(Some(content_url.to_string()))
.value(value)
.content_type(Some("html".to_string()))
.build(),
)
});
}
if let Some(author) = self
.get("author")
.and_then(|author_value| author_value.as_object().cloned())
{
let author_person: atom_syndication::Person = author.try_into()?;
builder.author(author_person);
} else if let Some(authors) = self
.get("author")
.and_then(|authors_value| authors_value.as_list().cloned())
{
let authors_persons = authors.iter().try_fold(
Vec::default(),
|mut acc, val| -> Result<Vec<atom_syndication::Person>, crate::Error> {
if let Property::Subobject(obj) = val {
let person = obj.clone().try_into()?;
acc.push(person);
}
Ok(acc)
},
)?;
builder.authors(authors_persons);
}
if let Some(category_value) = self.get("category").cloned().filter(|v| !v.is_empty()) {
let categories = extract_categories(category_value)?;
builder.categories(categories);
}
if let Some(temporal::Value::Timestamp(stamp)) =
self.get("published").and_then(|v| v.as_temporal().cloned())
{
builder.published(temporal_into_feed_date(&stamp)?);
}
if let Some(temporal::Value::Timestamp(stamp)) =
self.get("updated").and_then(|v| v.as_temporal().cloned())
{
builder.updated(temporal_into_feed_date(&stamp)?);
}
if let Some(summary_text) = self.get("summary").and_then(|v| v.as_text().cloned()) {
builder.summary(
atom_syndication::TextBuilder::default()
.value(summary_text)
.build(),
);
}
Ok(builder.build())
}
}
#[cfg(feature = "atom_syndication")]
fn temporal_into_feed_date(
stamp: &temporal::Stamp,
) -> Result<atom_syndication::FixedDateTime, crate::Error> {
if stamp.is_stamp() {
let concrete_dt: OffsetDateTime = stamp.clone().try_into()?;
let concrete_dt_str = concrete_dt
.format(&format_description::well_known::Rfc3339)
.map_err(time::Error::Format)
.map_err(temporal::Error::Time)
.map_err(microformats_types::Error::Temporal)
.map_err(crate::Error::Types)?;
Ok(
atom_syndication::FixedDateTime::parse_from_rfc3339(&concrete_dt_str)
.map_err(|_| atom_syndication::Error::WrongDatetime(concrete_dt_str))
.map_err(AtomError::from)
.map_err(Error::Atom)?,
)
} else {
Err(Error::Atom(atom_syndication::Error::WrongDatetime(stamp.to_string()).into()).into())
}
}
#[cfg(feature = "atom_syndication")]
impl TryFrom<Feed> for atom_syndication::Feed {
type Error = crate::Error;
fn try_from(jf2_feed: Feed) -> Result<Self, Self::Error> {
let mut atom_feed = atom_syndication::Feed::default();
let uri = jf2_feed.url().ok_or(Error::FeedMissingUrl)?;
atom_feed.set_id(uri);
let photo_value = jf2_feed
.get("photo")
.and_then(|v| v.as_url())
.map(|s| s.to_string());
atom_feed.set_icon(photo_value.clone());
atom_feed.set_logo(photo_value);
let lang_value = jf2_feed.get("lang").and_then(|pv| pv.as_string()).cloned();
atom_feed.set_lang(lang_value);
if let Some(title_value) = jf2_feed.get("name").and_then(|pv| pv.as_string()) {
atom_feed.set_title(title_value.to_owned());
}
if let Some(temporal::Value::Timestamp(stamp)) =
jf2_feed.get("updated").and_then(|v| v.as_temporal())
{
atom_feed.set_updated(temporal_into_feed_date(stamp)?);
} else {
return Err(Error::FeedMissingUpdateTime.into());
}
if let Some(authors) = jf2_feed.get("author").cloned().filter(|v| !v.is_empty()) {
let author_objs = authors.into_list().into_iter().try_fold(
Vec::default(),
|mut acc, author_value| -> Result<Vec<atom_syndication::Person>, crate::Error> {
let author_obj = if let Property::Url(author_url) = author_value {
jf2_feed.references().get(author_url.as_str()).cloned()
} else if let Property::Subobject(author_obj) = author_value {
Some(author_obj)
} else {
return Ok(acc);
};
if let Some(obj) = author_obj {
let author_person = obj.try_into()?;
acc.push(author_person);
}
Ok(acc)
},
)?;
atom_feed.set_authors(author_objs);
}
if let Some(category_value) = jf2_feed.get("category").cloned().filter(|v| !v.is_empty()) {
let categories = extract_categories(category_value)?;
atom_feed.set_categories(categories);
}
let entries = jf2_feed.children().into_iter().try_fold(
Vec::default(),
|mut acc, child_value| -> Result<Vec<atom_syndication::Entry>, crate::Error> {
acc.push(child_value.try_into()?);
Ok(acc)
},
)?;
atom_feed.set_entries(entries);
Ok(atom_feed)
}
}
#[cfg(feature = "atom_syndication")]
fn extract_categories(value: Property) -> Result<Vec<atom_syndication::Category>, crate::Error> {
value.into_list().into_iter().try_fold(
Vec::default(),
|mut acc, v| -> Result<Vec<atom_syndication::Category>, crate::Error> {
let value = match v {
Property::Url(u) => Some(
atom_syndication::CategoryBuilder::default()
.scheme(Some(u.to_string()))
.term(u.to_string())
.build(),
),
Property::Subobject(obj) => Some(obj.try_into()?),
Property::String(term) => Some(
atom_syndication::CategoryBuilder::default()
.term(term)
.build(),
),
_ => None,
};
if let Some(vk) = value {
acc.push(vk)
};
Ok(acc)
},
)
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
pub struct Card(Object);
impl From<Object> for Card {
fn from(mut value: Object) -> Self {
value.insert_context_uri();
value.extract_references();
Self(value)
}
}
impl std::ops::DerefMut for Card {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::ops::Deref for Card {
type Target = Object;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Card {
pub fn validate(&self) -> Result<(), crate::Error> {
if self.0.r#type() != Class::Known(KnownClass::Card) {
return Err(Error::FeedWrongType.into());
}
Ok(())
}
pub fn name(&self) -> String {
self.get("name")
.and_then(|pv| pv.as_string())
.cloned()
.unwrap_or_default()
}
pub fn email(&self) -> Option<String> {
self.get("email").and_then(|pv| pv.as_string()).cloned()
}
pub fn photo(&self) -> Option<Url> {
self.get("photo").and_then(|pv| pv.as_url()).cloned()
}
pub fn url(&self) -> Option<Url> {
self.get("url").and_then(|pv| pv.as_url()).cloned()
}
}