use std::{collections::BTreeMap, convert::TryFrom, ops::DerefMut, str::FromStr};
use url::Url;
use crate::types::{temporal, Class, Document, Fragment, Image, Item, KnownClass, PropertyValue};
#[derive(PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(untagged)]
pub enum Property {
String(String),
Temporal(temporal::Value),
Subobject(Object),
Multiple(PropertyList),
Html {
html: String,
text: String,
},
Image {
alt: Option<String>,
url: Url,
},
References(BTreeMap<String, Object>),
Url(Url),
}
impl From<Url> for Property {
fn from(v: Url) -> Self {
Self::Url(v)
}
}
impl From<temporal::Value> for Property {
fn from(v: temporal::Value) -> Self {
Self::Temporal(v)
}
}
impl TryFrom<String> for Property {
type Error = crate::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
if let Ok(tv) = temporal::Value::from_str(&value) {
Ok(Self::Temporal(tv))
} else if let Ok(u) = Url::from_str(&value) {
Ok(Self::Url(u))
} else {
Ok(Self::String(value))
}
}
}
impl Property {
pub fn as_object(&self) -> Option<&Object> {
if let Self::Subobject(v) = self {
Some(v)
} else {
None
}
}
pub fn as_string(&self) -> Option<&String> {
if let Self::String(v) = self {
Some(v)
} else {
None
}
}
pub fn as_list(&self) -> Option<&PropertyList> {
if let Self::Multiple(v) = self {
Some(v)
} else {
None
}
}
pub fn as_html(&self) -> Option<&String> {
if let Self::Html { html, .. } = self {
Some(html)
} else {
None
}
}
pub fn as_text(&self) -> Option<&String> {
if let Self::Html { text, .. } = self {
Some(text)
} else {
None
}
}
fn into_list(self) -> Vec<Property> {
if let Self::Multiple(list) = self {
list.0
} else {
vec![self]
}
}
fn flatten(self) -> Self {
let list = self.into_list();
if list.len() == 1 {
list[0].to_owned()
} else {
Self::Multiple(list.into())
}
}
pub fn as_url(&self) -> Option<&Url> {
if let Self::Url(v) = self {
Some(v)
} else {
None
}
}
pub fn is_string(&self) -> bool {
matches!(self, Self::String(..))
}
pub fn is_url(&self) -> bool {
matches!(self, Self::Url(..))
}
pub fn is_temporal(&self) -> bool {
matches!(self, Self::Temporal(..))
}
fn is_empty(&self) -> bool {
if let Self::Multiple(v) = self {
v.is_empty()
} else {
false
}
}
fn as_object_list(&self) -> Option<&BTreeMap<String, Object>> {
if let Self::References(refs) = self {
Some(refs)
} else {
None
}
}
pub fn as_temporal(&self) -> Option<&temporal::Value> {
if let Self::Temporal(v) = self {
Some(v)
} else {
None
}
}
pub fn temporal(value: impl Into<temporal::Value>) -> Self {
Self::Temporal(value.into())
}
pub fn is_object(&self) -> bool {
matches!(self, Self::Subobject(..))
}
}
impl From<time::OffsetDateTime> for Property {
fn from(dt: time::OffsetDateTime) -> Self {
let stamp: crate::types::temporal::Stamp = dt.into();
Self::Temporal(crate::types::temporal::Value::Timestamp(stamp))
}
}
#[derive(PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, Default)]
pub struct PropertyList(Vec<Property>);
impl From<PropertyList> for Property {
fn from(val: PropertyList) -> Self {
Property::Multiple(val)
}
}
impl FromIterator<Property> for PropertyList {
fn from_iter<T: IntoIterator<Item = Property>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl From<Vec<Property>> for PropertyList {
fn from(value: Vec<Property>) -> Self {
Self(value)
}
}
impl std::ops::Deref for PropertyList {
type Target = Vec<Property>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for PropertyList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<PropertyValue> for Property {
type Error = crate::Error;
fn try_from(value: PropertyValue) -> Result<Self, Self::Error> {
match value {
PropertyValue::Plain(v) => Ok(Self::String(v.to_string())),
PropertyValue::Url(u) => Ok(Self::Url((*u).clone())),
PropertyValue::Temporal(t) => Ok(Self::Temporal(t)),
PropertyValue::Fragment(Fragment {
html, value: text, ..
}) => Ok(Self::Html { html, text }),
PropertyValue::Item(i) => i.try_into().map(Self::Subobject),
PropertyValue::Image(Image { value: url, alt }) => Ok(Self::Image { alt, url }),
}
}
}
impl TryFrom<Property> for PropertyValue {
type Error = crate::Error;
fn try_from(value: Property) -> Result<Self, Self::Error> {
match value {
Property::String(s) => Ok(Self::Plain(crate::types::TextValue::new(s))),
Property::Url(u) => Ok(Self::Url(crate::types::UrlValue::new(u))),
Property::Temporal(t) => Ok(Self::Temporal(t)),
Property::Html { html, text } => Ok(Self::Fragment(Fragment {
html,
value: text,
#[cfg(feature = "per_element_lang")]
lang: None,
links: Vec::new(),
})),
Property::Subobject(obj) => Ok(Self::Item(obj.try_into()?)),
Property::Image { alt, url } => Ok(Self::Image(Image { alt, value: url })),
Property::Multiple(_) => Err(crate::Error::Types(crate::types::Error::NotAnObject)),
Property::References(_) => Err(crate::Error::Types(crate::types::Error::NotAnObject)),
}
}
}
static JSON_LD_CONTEXT_URI: &str = "http://www.w3.org/ns/jf2";
static RESERVED_PROPERTY_NAMES: [&str; 8] = [
"type",
"children",
"references",
"content-type",
"html",
"text",
"lang",
"@context",
];
impl<'a> TryFrom<(&'a str, PropertyValue)> for Property {
type Error = crate::Error;
fn try_from((name, value): (&'a str, PropertyValue)) -> Result<Self, Self::Error> {
if RESERVED_PROPERTY_NAMES.contains(&name) {
if ["type", "content-type", "name", "html", "text", "lang"].contains(&name)
&& !matches!(value, PropertyValue::Plain(_))
{
Err(crate::Error::InvalidRequiredProperty {
name: name.into(),
kind: "string".into(),
})
} else {
value.try_into()
}
} else {
value.try_into()
}
}
}
impl TryFrom<Vec<PropertyValue>> for Property {
type Error = crate::Error;
fn try_from(values: Vec<PropertyValue>) -> Result<Self, Self::Error> {
let mut converted_values: Vec<Property> = Vec::default();
for value in values {
converted_values.push(value.try_into()?);
}
if converted_values.len() == 1 {
Ok(converted_values[0].to_owned())
} else {
Ok(Self::Multiple(PropertyList(converted_values)))
}
}
}
impl TryFrom<Property> for Vec<PropertyValue> {
type Error = crate::Error;
fn try_from(value: Property) -> Result<Self, Self::Error> {
match value {
Property::Multiple(property_list) => {
let mut property_values = Vec::new();
for property in property_list.iter() {
property_values.push(property.clone().try_into()?);
}
Ok(property_values)
}
single_property => Ok(vec![single_property.try_into()?]),
}
}
}
#[derive(Default, Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)]
pub struct Object(pub BTreeMap<String, Property>);
impl Object {
pub fn children(&self) -> Vec<Object> {
self.0
.get("children")
.and_then(|list_val| list_val.as_list())
.map(|props| {
props
.iter()
.flat_map(Property::as_object)
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
pub fn set_children(&mut self, children: Vec<Object>) -> Vec<Object> {
let replaced_values = self.0.insert(
"children".into(),
PropertyList::from_iter(children.into_iter().map(Property::from)).into(),
);
if let Some(_values) = replaced_values {
Vec::default()
} else {
Vec::default()
}
}
pub fn url(&self) -> Option<Url> {
self.0.get("url").and_then(|p| p.as_url().cloned())
}
pub(crate) fn extract_references(&mut self) {
let mut references = if let Some(Property::References(refs)) = self.0.remove("references") {
refs
} else {
Default::default()
};
for (property_name, property_value) in self.0.iter_mut() {
if RESERVED_PROPERTY_NAMES.contains(&property_name.as_str()) {
continue;
}
if property_name != "children" {
*property_value = property_value.clone().flatten()
} else {
*property_value = PropertyList(property_value.clone().into_list()).into();
}
if let Property::Subobject(child_obj) = property_value {
if let Some(url) = child_obj.url() {
let new_value = Property::Url(url.clone());
references.insert(url.to_string(), child_obj.to_owned());
*property_value = new_value;
}
}
}
if let Some(Property::Multiple(children)) = self.0.get_mut("children") {
for child in children.iter_mut() {
if let Property::Subobject(child_obj) = child {
child_obj.extract_references();
if let Some(Property::References(refs)) = child_obj.remove("references") {
references.extend(refs)
}
}
}
if !references.is_empty() {
self.insert("references".to_string(), Property::References(references));
}
}
}
fn insert_context_uri(&mut self) {
if !self.contains_key("@context") {
self.insert(
"@context".to_string(),
Property::String(JSON_LD_CONTEXT_URI.to_string()),
);
}
}
pub fn flatten(self) -> Self {
if let Some(only_child) = self
.children()
.first()
.cloned()
.filter(|_| self.children().len() == 1)
{
only_child
} else {
self
}
}
}
impl std::ops::Deref for Object {
type Target = BTreeMap<String, Property>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Object {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Object {
pub fn r#type(&self) -> Class {
self.get("type")
.and_then(|type_property_value| {
if let Property::String(class_str) = type_property_value {
Class::from_str(class_str).ok()
} else {
None
}
})
.unwrap_or(Class::Known(KnownClass::Entry))
}
}
impl TryFrom<Item> for Object {
type Error = crate::Error;
fn try_from(value: Item) -> Result<Self, Self::Error> {
let mut new_object = Self::default();
let resolved_type = value
.r#type
.iter()
.find(|c| c.is_recognized())
.cloned()
.unwrap_or(Class::Known(KnownClass::Entry));
new_object.insert(
"type".to_string(),
Property::String(resolved_type.to_string().replacen("h-", "", 1)),
);
if !value.children.is_empty() {
let mut jf2_objects = Vec::default();
for item in value.children.iter() {
jf2_objects.push(item.clone().try_into().map(Property::Subobject)?);
}
new_object.insert(
"children".to_string(),
Property::Multiple(PropertyList(jf2_objects)),
);
}
let mut remaining_properties = value.properties.clone();
let content = remaining_properties
.remove("content")
.and_then(|content_values| {
content_values
.into_iter()
.flat_map(|prop_value| {
if prop_value.is_empty() {
None
} else if let PropertyValue::Fragment(f) = prop_value {
Some(f)
} else {
None
}
})
.next()
});
if let Some(html_value) = content.as_ref().map(|fr| fr.html.clone()) {
new_object.insert("html".to_string(), Property::String(html_value));
}
if let Some(text_value) = content.as_ref().map(|fr| fr.value.clone()) {
new_object.insert("text".to_string(), Property::String(text_value));
}
let restructed_properties = remaining_properties.into_iter().try_fold(
Default::default(),
|mut properties_acc,
(property_name, property_values)|
-> Result<BTreeMap<String, Property>, crate::Error> {
let values = property_values.into_iter().try_fold(
Default::default(),
|mut list_acc, mf2_value| -> Result<PropertyList, crate::Error> {
list_acc.push(mf2_value.try_into()?);
Ok(list_acc)
},
)?;
let is_children = &property_name == "children";
let property_value = Property::Multiple(values);
properties_acc.insert(
property_name,
if !is_children {
property_value.flatten()
} else {
property_value
},
);
Ok(properties_acc)
},
)?;
new_object.extend(restructed_properties);
Ok(new_object)
}
}
impl TryFrom<Object> for Item {
type Error = crate::Error;
fn try_from(value: Object) -> Result<Self, Self::Error> {
let mut item = Item::new(vec![value.r#type()]);
item.children.extend(value.children().into_iter().try_fold(
Vec::default(),
|mut acc, object| {
acc.push(object.try_into()?);
Result::<_, Self::Error>::Ok(acc)
},
)?);
let references = value
.get("references")
.and_then(|prop| prop.as_object_list())
.cloned()
.unwrap_or_default();
for (property_name, property_value) in value.0.iter() {
if RESERVED_PROPERTY_NAMES.contains(&property_name.as_str()) {
continue; }
let property_values: Vec<PropertyValue> = property_value.clone().try_into()?;
if !property_values.is_empty() {
item.properties
.insert(property_name.clone(), property_values);
}
}
if let Some(html_property) = value.get("html") {
if let Some(html_str) = html_property.as_string() {
let text_property = value
.get("text")
.and_then(|t| t.as_string())
.cloned()
.unwrap_or_default();
let fragment = Fragment {
html: html_str.clone(),
value: text_property,
#[cfg(feature = "per_element_lang")]
lang: None,
links: Vec::new(),
};
item.properties.insert(
"content".to_string(),
vec![PropertyValue::Fragment(fragment)],
);
}
}
if let Some(url_property) = value.get("url") {
if let Some(url_str) = url_property.as_url() {
if let Some(referenced_obj) = references.get(&url_str.to_string()) {
if let Ok(referenced_item) =
<Object as TryInto<Item>>::try_into(referenced_obj.clone())
{
for (prop_name, prop_values) in referenced_item.properties {
item.properties.entry(prop_name).or_insert(prop_values);
}
}
}
}
}
Ok(item)
}
}
impl From<Object> for Property {
fn from(mut object: Object) -> Self {
object.remove("@context");
Self::Subobject(object)
}
}
pub trait IntoJf2 {
fn into_jf2(self) -> Result<Object, crate::Error>;
}
impl IntoJf2 for Item {
fn into_jf2(self) -> Result<Object, crate::Error> {
self.try_into()
}
}
impl IntoJf2 for Document {
fn into_jf2(self) -> Result<Object, crate::Error> {
let mut doc_obj = if self.items.len() == 1
&& self.items[0]
.r#type
.contains(&Class::Known(KnownClass::Feed))
{
self.items[0].clone().try_into()?
} else {
let mut top_level_items = Vec::default();
for item in self.items {
top_level_items.push(item.into_jf2()?.into());
}
let mut doc_obj = Object::default();
doc_obj.insert(
"children".to_string(),
Property::Multiple(PropertyList(top_level_items)),
);
doc_obj
};
doc_obj.insert_context_uri();
doc_obj.extract_references();
Ok(doc_obj)
}
}
pub mod profiles;
mod test;
#[cfg(test)]
mod conversion_tests {
use super::*;
use url::Url;
#[test]
fn test_property_to_property_value_string() {
let jf2_prop = Property::String("hello".to_string());
let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
assert_eq!(
prop_value,
PropertyValue::Plain(crate::types::TextValue::new("hello".to_string()))
);
}
#[test]
fn test_property_to_property_value_url() {
let url = Url::parse("https://example.com").unwrap();
let jf2_prop = Property::Url(url.clone());
let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
assert_eq!(
prop_value,
PropertyValue::Url(crate::types::UrlValue::new(url))
);
}
#[test]
fn test_property_to_property_value_temporal() {
let temporal = temporal::Value::Timestamp(crate::types::temporal::Stamp::now());
let jf2_prop = Property::Temporal(temporal.clone());
let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
assert_eq!(prop_value, PropertyValue::Temporal(temporal));
}
#[test]
fn test_property_to_property_value_html() {
let jf2_prop = Property::Html {
html: "<p>hello</p>".to_string(),
text: "hello".to_string(),
};
let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
if let PropertyValue::Fragment(fragment) = prop_value {
assert_eq!(fragment.html, "<p>hello</p>");
assert_eq!(fragment.value, "hello");
} else {
panic!("Expected Fragment");
}
}
#[test]
fn test_property_to_property_value_image() {
let url = Url::parse("https://example.com/img.jpg").unwrap();
let jf2_prop = Property::Image {
alt: Some("An image".to_string()),
url: url.clone(),
};
let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
if let PropertyValue::Image(image) = prop_value {
assert_eq!(image.alt, Some("An image".to_string()));
assert_eq!(image.value, url);
} else {
panic!("Expected Image");
}
}
#[test]
fn test_property_multiple_to_vec_property_value() {
let props = vec![
Property::String("first".to_string()),
Property::String("second".to_string()),
];
let jf2_prop = Property::Multiple(PropertyList(props));
let prop_values: Vec<PropertyValue> = jf2_prop.try_into().unwrap();
assert_eq!(prop_values.len(), 2);
assert_eq!(
prop_values[0],
PropertyValue::Plain(crate::types::TextValue::new("first".to_string()))
);
assert_eq!(
prop_values[1],
PropertyValue::Plain(crate::types::TextValue::new("second".to_string()))
);
}
#[test]
fn test_single_property_to_vec_property_value() {
let jf2_prop = Property::String("single".to_string());
let prop_values: Vec<PropertyValue> = jf2_prop.try_into().unwrap();
assert_eq!(prop_values.len(), 1);
assert_eq!(
prop_values[0],
PropertyValue::Plain(crate::types::TextValue::new("single".to_string()))
);
}
#[test]
fn test_object_to_item_basic() {
let mut obj = Object::default();
obj.insert("type".to_string(), Property::String("entry".to_string()));
obj.insert(
"name".to_string(),
Property::String("Test Entry".to_string()),
);
let item: Item = obj.try_into().unwrap();
assert_eq!(item.r#type.len(), 1);
assert_eq!(item.r#type[0], Class::Known(KnownClass::Entry));
let names = &item.properties["name"];
assert_eq!(names.len(), 1);
assert_eq!(
names[0],
PropertyValue::Plain(crate::types::TextValue::new("Test Entry".to_string()))
);
}
#[test]
fn test_object_to_item_with_content() {
let mut obj = Object::default();
obj.insert("type".to_string(), Property::String("entry".to_string()));
obj.insert(
"html".to_string(),
Property::String("<p>Content</p>".to_string()),
);
obj.insert("text".to_string(), Property::String("Content".to_string()));
let item: Item = obj.try_into().unwrap();
let content = &item.properties["content"];
assert_eq!(content.len(), 1);
if let PropertyValue::Fragment(fragment) = &content[0] {
assert_eq!(fragment.html, "<p>Content</p>");
assert_eq!(fragment.value, "Content");
} else {
panic!("Expected Fragment");
}
}
#[test]
fn test_object_to_item_with_children() {
let mut child_obj = Object::default();
child_obj.insert("type".to_string(), Property::String("cite".to_string()));
child_obj.insert("name".to_string(), Property::String("Child".to_string()));
let mut parent_obj = Object::default();
parent_obj.insert("type".to_string(), Property::String("entry".to_string()));
parent_obj.insert("name".to_string(), Property::String("Parent".to_string()));
parent_obj.insert(
"children".to_string(),
Property::Multiple(PropertyList(vec![Property::Subobject(child_obj)])),
);
let item: Item = parent_obj.try_into().unwrap();
assert_eq!(item.children.len(), 1);
assert_eq!(item.children[0].r#type.len(), 1);
assert_eq!(item.children[0].r#type[0], Class::Known(KnownClass::Cite));
}
#[test]
fn test_object_to_item_with_references() {
let mut ref_obj = Object::default();
ref_obj.insert("type".to_string(), Property::String("cite".to_string()));
ref_obj.insert(
"name".to_string(),
Property::String("Referenced".to_string()),
);
let mut obj = Object::default();
obj.insert("type".to_string(), Property::String("entry".to_string()));
obj.insert(
"url".to_string(),
Property::Url(Url::parse("https://example.com").unwrap()),
);
let mut references = std::collections::BTreeMap::new();
references.insert("https://example.com".to_string(), ref_obj);
obj.insert("references".to_string(), Property::References(references));
let item: Item = obj.try_into().unwrap();
assert_eq!(item.r#type.len(), 1);
assert_eq!(item.r#type[0], Class::Known(KnownClass::Entry));
}
#[test]
fn test_property_value_to_property_roundtrip() {
let original = PropertyValue::Plain(crate::types::TextValue::new("test".to_string()));
let jf2_prop: Property = original.clone().try_into().unwrap();
let converted: PropertyValue = jf2_prop.try_into().unwrap();
assert_eq!(original, converted);
}
#[test]
fn test_conversion_errors() {
let jf2_prop = Property::Multiple(PropertyList(vec![
Property::String("one".to_string()),
Property::String("two".to_string()),
]));
let result: Result<PropertyValue, _> = jf2_prop.try_into();
assert!(result.is_err());
let jf2_ref_prop = Property::References(std::collections::BTreeMap::new());
let result: Result<PropertyValue, _> = jf2_ref_prop.try_into();
assert!(result.is_err());
}
}