use crate::{Class, Fragment, LanguageFilter, Properties, PropertyValue};
use serde::de::{self, Visitor};
use serde::ser::{SerializeMap, SerializeSeq};
use std::collections::HashSet;
use std::iter::FromIterator;
use std::ops::Deref;
use std::ops::DerefMut;
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum ValueKind {
Url(url::Url),
Plain(String),
}
impl Default for ValueKind {
fn default() -> Self {
Self::Plain(String::default())
}
}
#[derive(serde::Serialize, serde::Deserialize, Default, PartialEq, Eq, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Item {
pub r#type: Vec<Class>,
#[serde(default, with = "referenced_properties")]
pub properties: Properties,
#[serde(
default,
with = "referenced_children",
skip_serializing_if = "referenced_children::is_empty"
)]
pub children: Items,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<ValueKind>,
}
impl std::fmt::Debug for Item {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Item")
.field("type", &self.r#type)
.field("id", &self.id)
.field("value", &self.value)
.field("lang", &self.lang)
.finish()
}
}
impl Item {
pub fn new(types: Vec<Class>) -> Self {
Item {
r#type: types,
..Default::default()
}
}
pub fn is_empty(&self) -> bool {
self.children.is_empty() && self.r#type.is_empty()
}
pub fn remove_whole_property(&mut self, property_name: &str) {
self.properties.remove(property_name);
}
pub fn content(&self) -> Option<Vec<PropertyValue>> {
self.properties.get("content").cloned()
}
pub fn set_content(&mut self, fragment: Fragment) {
self.properties.insert(
"content".to_string(),
vec![PropertyValue::Fragment(fragment)],
);
}
pub fn append_property(&mut self, property_name: &str, property_value: PropertyValue) {
let mut new_values = if let Some(values) = self.properties.get(property_name) {
values.to_vec()
} else {
Vec::default()
};
new_values.push(property_value);
self.properties.insert(property_name.to_owned(), new_values);
}
pub fn has_nested_microformats(&self) -> bool {
let has_nested_value_microformats = self
.properties
.values()
.flatten()
.any(|v| matches!(v, PropertyValue::Item(_)));
has_nested_value_microformats || !self.children.is_empty()
}
pub fn nested_children(&self) -> Vec<Item> {
self.properties
.values()
.flatten()
.filter_map(|value| {
if let PropertyValue::Item(item) = value {
Some(item)
} else {
None
}
})
.cloned()
.collect::<Vec<_>>()
}
pub fn get_property(&self, arg: &str) -> Option<Vec<PropertyValue>> {
self.properties.get(arg).cloned()
}
}
impl TryFrom<serde_json::Map<String, serde_json::Value>> for Item {
type Error = crate::Error;
fn try_from(obj: serde_json::Map<String, serde_json::Value>) -> Result<Self, Self::Error> {
if !obj.contains_key("type") {
return Err(Self::Error::JsonObjectMissingProperty("type".to_string()));
}
if !obj.contains_key("properties") {
return Err(Self::Error::JsonObjectMissingProperty(
"properties".to_string(),
));
}
serde_json::from_value(serde_json::Value::Object(obj)).map_err(Self::Error::JSON)
}
}
impl TryFrom<serde_json::Value> for Item {
type Error = crate::Error;
fn try_from(v: serde_json::Value) -> Result<Self, Self::Error> {
if let serde_json::Value::Object(o) = v {
Self::try_from(o)
} else {
Err(Self::Error::NotAnObject)
}
}
}
impl TryInto<serde_json::Value> for Item {
type Error = crate::Error;
fn try_into(self) -> Result<serde_json::Value, Self::Error> {
serde_json::to_value(self).map_err(crate::Error::JSON)
}
}
impl IntoIterator for Item {
type Item = Item;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
let mut items = self
.children
.iter()
.flat_map(|i| i.clone().into_iter())
.collect::<Vec<Self::Item>>();
items.push(self);
items.into_iter()
}
}
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Items(Vec<Item>);
impl From<Vec<Item>> for Items {
fn from(value: Vec<Item>) -> Self {
Self(value)
}
}
impl Items {
pub fn create_child_item(&mut self, types: &[Class]) -> Item {
let item = Item::new(types.to_vec());
self.0.push(item.to_owned());
item
}
pub fn get_by_id(&self, id: &str) -> Option<Item> {
self.iter()
.flat_map(|item| item.clone().into_iter())
.find(|item| item.id == Some(id.to_string()))
.clone()
}
pub fn get_by_url(&self, url: &url::Url) -> Option<Item> {
self.iter()
.flat_map(|item| item.clone().into_iter())
.find(|item| item.value == Some(ValueKind::Url(url.to_owned())))
.clone()
}
pub fn with_capacity(size_hint: usize) -> Items {
Items(Vec::with_capacity(size_hint))
}
}
impl DerefMut for Items {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Deref for Items {
type Target = Vec<Item>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
mod referenced_properties {
use super::*;
use crate::{NodeList, Properties};
type Value = Properties;
struct PropertyVisitor;
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum PotentialPropertyValue {
List(NodeList),
Value(PropertyValue),
}
impl<'de> Visitor<'de> for PropertyVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a map of properties with values that could be null, a string, a list of either strings, maps or both")
}
fn visit_map<A>(self, mut map_visitor: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut property_map = Properties::default();
while let Some(key) = map_visitor.next_key()? {
let concrete_value: NodeList =
match map_visitor.next_value::<PotentialPropertyValue>() {
Ok(PotentialPropertyValue::List(values)) => values,
Ok(PotentialPropertyValue::Value(node)) => vec![node],
Err(_) => vec![],
};
if let Some(values) = property_map.get_mut(&key) {
values.extend(concrete_value);
} else {
property_map.insert(key, concrete_value);
}
}
Ok(property_map)
}
}
pub fn serialize<S>(properties: &Value, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let mut properties_seq = serializer.serialize_map(Some(properties.len()))?;
for (key, value) in properties.iter() {
properties_seq.serialize_entry(key, value)?;
}
properties_seq.end()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(PropertyVisitor)
}
}
mod referenced_children {
use super::*;
type Value = Items;
struct ChildrenVisitor;
impl<'de> Visitor<'de> for ChildrenVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("expecting a list of children nodes, an empty list or null")
}
fn visit_seq<ChildrenSequenceAccessor>(
self,
mut seq: ChildrenSequenceAccessor,
) -> Result<Self::Value, ChildrenSequenceAccessor::Error>
where
ChildrenSequenceAccessor: de::SeqAccess<'de>,
{
let size_hint = seq.size_hint().unwrap_or(0);
let mut children: Items = Items::with_capacity(size_hint);
while let Some(item) = seq.next_element()? {
children.push(item);
}
Ok(children)
}
}
#[allow(clippy::ptr_arg)]
pub fn serialize<S>(children: &Value, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let mut seq = serializer.serialize_seq(Some(children.deref().len()))?;
let safe_items = children
.iter()
.filter(|item| !item.is_empty())
.cloned()
.collect::<Vec<_>>();
for concrete_item in safe_items {
seq.serialize_element(&concrete_item)?;
}
seq.end()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_seq(ChildrenVisitor)
}
pub fn is_empty(items: &Items) -> bool {
items.is_empty()
}
}
impl LanguageFilter for Item {
fn matches_languages(&self, languages: &HashSet<&str>) -> bool {
if let Some(ref lang) = self.lang {
if languages.contains(lang.as_str()) {
return true;
}
}
self.children
.iter()
.any(|child| child.matches_languages(languages))
}
fn filter_by_languages_set(&self, languages: &HashSet<&str>) -> Option<Self> {
if !self.matches_languages(languages) {
return None;
}
let filtered_children: Vec<Item> = self
.children
.iter()
.filter_map(|child| child.filter_by_languages_set(languages))
.collect();
Some(Self {
r#type: self.r#type.clone(),
properties: self.properties.clone(),
children: filtered_children.into(),
id: self.id.clone(),
lang: self.lang.clone(),
value: self.value.clone(),
})
}
}
impl LanguageFilter for Items {
fn matches_languages(&self, languages: &HashSet<&str>) -> bool {
self.iter().any(|item| item.matches_languages(languages))
}
fn filter_by_languages_set(&self, languages: &HashSet<&str>) -> Option<Self> {
let filtered: Vec<Item> = self
.iter()
.filter_map(|item| item.filter_by_languages_set(languages))
.collect();
if filtered.is_empty() {
None
} else {
Some(Items::from(filtered))
}
}
}
impl FromIterator<Item> for Items {
fn from_iter<I: IntoIterator<Item = Item>>(iter: I) -> Self {
Items(iter.into_iter().collect())
}
}
#[cfg(test)]
mod language_filter_tests {
use super::*;
use crate::{Class, Document, KnownClass, LanguageFilter, Properties};
#[macro_export]
macro_rules! lang_matches {
($item:expr, $($lang:expr),* $(,)?) => {{
let mut set = std::collections::HashSet::new();
$(
set.insert($lang);
)*
$item.matches_languages(&set)
}};
}
fn make_item(lang: Option<&str>, children: Vec<Item>) -> Item {
Item {
r#type: vec![Class::Known(KnownClass::Entry)],
properties: Properties::default(),
children: Items::from(children),
id: None,
lang: lang.map(|s| s.to_string()),
value: None,
}
}
#[test]
fn item_with_matching_lang_matches() {
let item = make_item(Some("en"), vec![]);
let languages: HashSet<&str> = vec!["en", "fr"].into_iter().collect();
assert!(item.matches_languages(&languages));
}
#[test]
fn item_with_non_matching_lang_does_not_match() {
let item = make_item(Some("de"), vec![]);
let languages: HashSet<&str> = vec!["en", "fr"].into_iter().collect();
assert!(!item.matches_languages(&languages));
}
#[test]
fn item_with_none_lang_matches_only_via_children() {
let child_with_en = make_item(Some("en"), vec![]);
let item = make_item(None, vec![child_with_en]);
let languages: HashSet<&str> = vec!["en"].into_iter().collect();
assert!(item.matches_languages(&languages));
let child_with_de = make_item(Some("de"), vec![]);
let item_no_match = make_item(None, vec![child_with_de]);
assert!(!item_no_match.matches_languages(&languages));
}
#[test]
fn items_filtering_works() {
let item_en = make_item(Some("en"), vec![]);
let item_fr = make_item(Some("fr"), vec![]);
let item_de = make_item(Some("de"), vec![]);
let items = Items::from(vec![item_en, item_fr, item_de]);
let languages: HashSet<&str> = vec!["en", "fr"].into_iter().collect();
let filtered = items.filter_by_languages_set(&languages);
assert!(filtered.is_some());
let filtered_items = filtered.unwrap();
assert_eq!(filtered_items.len(), 2);
}
#[test]
fn document_filtering_works() {
let item_en = make_item(Some("en"), vec![]);
let item_fr = make_item(Some("fr"), vec![]);
let item_de = make_item(Some("de"), vec![]);
let mut document = Document::default();
document.items = vec![item_en, item_fr, item_de];
let languages: HashSet<&str> = vec!["en"].into_iter().collect();
let filtered = document.filter_by_languages_set(&languages);
assert!(filtered.is_some());
let filtered_doc = filtered.unwrap();
assert_eq!(filtered_doc.items.len(), 1);
}
#[test]
fn from_iterator_collect_pattern() {
let items: Vec<Item> = vec![make_item(Some("en"), vec![]), make_item(Some("fr"), vec![])];
let collected: Items = items.into_iter().collect();
assert_eq!(collected.len(), 2);
}
#[test]
fn lang_matches_macro_works() {
let item_en = make_item(Some("en"), vec![]);
let item_fr = make_item(Some("fr"), vec![]);
let item_none = make_item(None, vec![]);
assert!(lang_matches!(item_en, "en"));
assert!(!lang_matches!(item_fr, "en"));
assert!(lang_matches!(item_en, "en", "fr", "de"));
assert!(lang_matches!(item_fr, "en", "fr", "de"));
assert!(!lang_matches!(item_none, "en"));
}
#[test]
fn recursive_child_filtering() {
let grandchild = make_item(Some("de"), vec![]);
let child = make_item(Some("en"), vec![grandchild]);
let parent = make_item(None, vec![child]);
let languages: HashSet<&str> = vec!["en"].into_iter().collect();
let filtered = parent.filter_by_languages_set(&languages);
assert!(filtered.is_some());
let filtered_parent = filtered.unwrap();
assert_eq!(filtered_parent.children.len(), 1);
let filtered_child = &filtered_parent.children[0];
assert!(filtered_child.children.is_empty());
}
#[test]
fn empty_language_list_filters_everything() {
let item_with_lang = make_item(Some("en"), vec![]);
let item_without_lang = make_item(None, vec![]);
let empty_languages: HashSet<&str> = HashSet::new();
assert!(!item_with_lang.matches_languages(&empty_languages));
assert!(!item_without_lang.matches_languages(&empty_languages));
assert!(
item_with_lang
.filter_by_languages_set(&empty_languages)
.is_none()
);
assert!(
item_without_lang
.filter_by_languages_set(&empty_languages)
.is_none()
);
}
#[test]
fn item_without_lang_and_children_does_not_match() {
let item = make_item(None, vec![]);
let languages: HashSet<&str> = vec!["en"].into_iter().collect();
assert!(!item.matches_languages(&languages));
}
#[test]
fn filter_preserves_item_properties() {
let mut item = make_item(Some("en"), vec![]);
item.id = Some("test-id".to_string());
item.properties.insert(
"name".to_string(),
vec![crate::PropertyValue::Plain(crate::TextValue::new(
"Test".to_string(),
))],
);
let languages: HashSet<&str> = vec!["en"].into_iter().collect();
let filtered = item.filter_by_languages_set(&languages);
assert!(filtered.is_some());
let filtered_item = filtered.unwrap();
assert_eq!(filtered_item.id, Some("test-id".to_string()));
assert!(filtered_item.properties.contains_key("name"));
}
}