use microformats_types::{Document, Item, Properties, PropertyValue};
#[cfg(feature = "cleaner")]
pub trait CleanedDocument {
fn with_only_known_classes(&self) -> Self;
fn as_cleaned(&self) -> Self
where
Self: Sized,
{
self.with_only_known_classes()
}
fn unrecognized_items(&self) -> Vec<&Item>;
}
#[cfg(feature = "cleaner")]
impl CleanedDocument for Document {
fn with_only_known_classes(&self) -> Self {
let filtered_items = self.items.iter().filter_map(filter_item).collect();
Self {
items: filtered_items,
url: self.url.clone(),
rels: self.rels.clone(),
lang: self.lang.clone(),
#[cfg(feature = "metaformats")]
meta_item: self.meta_item.as_ref().and_then(filter_item),
#[cfg(feature = "debug_flow")]
_debug_context: self._debug_context.clone(),
}
}
fn unrecognized_items(&self) -> Vec<&Item> {
let mut result = Vec::new();
for item in &self.items {
result.extend(collect_unrecognized_items(item));
}
result
}
}
fn filter_item(item: &Item) -> Option<Item> {
if item.r#type.iter().any(|c| !c.is_recognized()) {
return None;
}
Some(Item {
r#type: item.r#type.clone(),
properties: filter_properties(&item.properties),
children: item.children.iter().filter_map(filter_item).collect(),
id: item.id.clone(),
lang: item.lang.clone(),
value: item.value.clone(),
})
}
fn filter_properties(props: &Properties) -> Properties {
props
.iter()
.map(|(key, values)| {
let filtered_values = values
.iter()
.filter_map(|pv| match pv {
PropertyValue::Item(nested) => filter_item(nested).map(PropertyValue::Item),
_other => Some(pv.clone()),
})
.collect();
(key.clone(), filtered_values)
})
.collect()
}
fn collect_unrecognized_items<'a>(item: &'a Item) -> Vec<&'a Item> {
let mut result = Vec::new();
if item.r#type.iter().any(|c| !c.is_recognized()) {
result.push(item);
}
for child in item.children.iter() {
result.extend(collect_unrecognized_items(child));
}
for values in item.properties.values() {
for pv in values {
if let PropertyValue::Item(nested) = pv {
result.extend(collect_unrecognized_items(nested));
}
}
}
result
}
#[cfg(feature = "cleaner")]
mod test {
use super::*;
use microformats_types::{Class, Item, Items, KnownClass, PropertyValue};
fn make_document_with_known_class() -> Document {
Document {
items: vec![Item {
r#type: vec![Class::Known(KnownClass::Card)],
properties: Default::default(),
children: Items::from(vec![]),
id: None,
lang: None,
value: None,
}],
url: None,
rels: Default::default(),
lang: None,
#[cfg(feature = "metaformats")]
meta_item: None,
#[cfg(feature = "debug_flow")]
_debug_context: Default::default(),
}
}
fn make_document_with_custom_class() -> Document {
Document {
items: vec![Item {
r#type: vec![Class::Custom("h-custom".to_string())],
properties: Default::default(),
children: Items::from(vec![]),
id: None,
lang: None,
value: None,
}],
url: None,
rels: Default::default(),
lang: None,
#[cfg(feature = "metaformats")]
meta_item: None,
#[cfg(feature = "debug_flow")]
_debug_context: Default::default(),
}
}
fn make_document_with_mixed_classes() -> Document {
Document {
items: vec![
Item {
r#type: vec![Class::Known(KnownClass::Card)],
properties: Default::default(),
children: Items::from(vec![]),
id: None,
lang: None,
value: None,
},
Item {
r#type: vec![Class::Custom("h-custom".to_string())],
properties: Default::default(),
children: Items::from(vec![]),
id: None,
lang: None,
value: None,
},
],
url: None,
rels: Default::default(),
lang: None,
#[cfg(feature = "metaformats")]
meta_item: None,
#[cfg(feature = "debug_flow")]
_debug_context: Default::default(),
}
}
fn make_document_with_nested_custom() -> Document {
Document {
items: vec![Item {
r#type: vec![Class::Known(KnownClass::Entry)],
properties: [(
"author".to_string(),
vec![PropertyValue::Item(Item {
r#type: vec![Class::Custom("h-custom".to_string())],
properties: Default::default(),
children: Items::from(vec![]),
id: None,
lang: None,
value: None,
})],
)]
.into_iter()
.collect(),
children: Items::from(vec![]),
id: None,
lang: None,
value: None,
}],
url: None,
rels: Default::default(),
lang: None,
#[cfg(feature = "metaformats")]
meta_item: None,
#[cfg(feature = "debug_flow")]
_debug_context: Default::default(),
}
}
fn make_document_with_children_custom() -> Document {
Document {
items: vec![Item {
r#type: vec![Class::Known(KnownClass::Card)],
properties: Default::default(),
children: Items::from(vec![Item {
r#type: vec![Class::Custom("h-custom".to_string())],
properties: Default::default(),
children: Items::from(vec![]),
id: None,
lang: None,
value: None,
}]),
id: None,
lang: None,
value: None,
}],
url: None,
rels: Default::default(),
lang: None,
#[cfg(feature = "metaformats")]
meta_item: None,
#[cfg(feature = "debug_flow")]
_debug_context: Default::default(),
}
}
#[test]
fn with_only_known_classes_keeps_recognized_classes() {
let doc = make_document_with_known_class();
let cleaned = doc.with_only_known_classes();
assert_eq!(cleaned.items.len(), 1);
assert!(matches!(
cleaned.items[0].r#type[0],
Class::Known(KnownClass::Card)
));
}
#[test]
fn with_only_known_classes_removes_custom_classes() {
let doc = make_document_with_custom_class();
let cleaned = doc.with_only_known_classes();
assert!(cleaned.items.is_empty());
}
#[test]
fn with_only_known_classes_filters_mixed() {
let doc = make_document_with_mixed_classes();
let cleaned = doc.with_only_known_classes();
assert_eq!(cleaned.items.len(), 1);
assert!(matches!(
cleaned.items[0].r#type[0],
Class::Known(KnownClass::Card)
));
}
#[test]
fn with_only_known_classes_removes_nested_custom_in_properties() {
let doc = make_document_with_nested_custom();
let cleaned = doc.with_only_known_classes();
assert_eq!(cleaned.items.len(), 1);
let author_values = &cleaned.items[0].properties["author"];
assert!(author_values.is_empty());
}
#[test]
fn with_only_known_classes_removes_custom_children() {
let doc = make_document_with_children_custom();
let cleaned = doc.with_only_known_classes();
assert_eq!(cleaned.items.len(), 1);
assert!(cleaned.items[0].children.is_empty());
}
#[test]
fn with_only_known_classes_preserves_url() {
let url = url::Url::parse("https://example.com").unwrap();
let mut doc = make_document_with_known_class();
doc.url = Some(url.clone());
let cleaned = doc.with_only_known_classes();
assert_eq!(cleaned.url, Some(url));
}
#[test]
fn with_only_known_classes_preserves_rels() {
let url = url::Url::parse("https://example.com").unwrap();
let mut doc = make_document_with_known_class();
doc.rels.items.insert(
url.clone(),
microformats_types::Relation {
rels: vec!["me".to_string()],
..Default::default()
},
);
let cleaned = doc.with_only_known_classes();
assert!(cleaned.rels.items.get(&url).is_some());
}
#[test]
fn with_only_known_classes_preserves_lang() {
let mut doc = make_document_with_known_class();
doc.lang = Some("en".to_string());
let cleaned = doc.with_only_known_classes();
assert_eq!(cleaned.lang, Some("en".to_string()));
}
#[test]
fn with_only_known_classes_empty_document() {
let doc = Document {
items: vec![],
url: None,
rels: Default::default(),
lang: None,
#[cfg(feature = "metaformats")]
meta_item: None,
#[cfg(feature = "debug_flow")]
_debug_context: Default::default(),
};
let cleaned = doc.with_only_known_classes();
assert!(cleaned.items.is_empty());
}
#[test]
fn with_only_known_classes_all_custom() {
let doc = make_document_with_custom_class();
let cleaned = doc.with_only_known_classes();
assert!(cleaned.items.is_empty());
}
#[test]
fn as_cleaned_same_as_with_only_known_classes() {
let doc = make_document_with_mixed_classes();
let cleaned_with_method = doc.with_only_known_classes();
let cleaned_with_alias = doc.as_cleaned();
assert_eq!(
cleaned_with_method.items.len(),
cleaned_with_alias.items.len()
);
assert!(matches!(
cleaned_with_alias.items[0].r#type[0],
Class::Known(KnownClass::Card)
));
}
#[test]
fn as_cleaned_with_known_classes() {
let doc = make_document_with_known_class();
let cleaned = doc.as_cleaned();
assert_eq!(cleaned.items.len(), 1);
assert!(matches!(
cleaned.items[0].r#type[0],
Class::Known(KnownClass::Card)
));
}
#[test]
fn as_cleaned_removes_custom_classes() {
let doc = make_document_with_custom_class();
let cleaned = doc.as_cleaned();
assert!(cleaned.items.is_empty());
}
#[test]
fn unrecognized_items_returns_custom() {
let doc = make_document_with_custom_class();
let unrecognized = doc.unrecognized_items();
assert_eq!(unrecognized.len(), 1);
}
#[test]
fn unrecognized_items_empty_for_known() {
let doc = make_document_with_known_class();
let unrecognized = doc.unrecognized_items();
assert!(unrecognized.is_empty());
}
#[test]
fn unrecognized_items_finds_all_custom() {
let doc = make_document_with_mixed_classes();
let unrecognized = doc.unrecognized_items();
assert_eq!(unrecognized.len(), 1);
}
#[test]
fn unrecognized_items_finds_nested_custom() {
let doc = make_document_with_nested_custom();
let unrecognized = doc.unrecognized_items();
assert_eq!(unrecognized.len(), 1);
}
#[test]
fn unrecognized_items_finds_custom_children() {
let doc = make_document_with_children_custom();
let unrecognized = doc.unrecognized_items();
assert_eq!(unrecognized.len(), 1);
}
#[test]
fn unrecognized_items_empty_document() {
let doc = Document {
items: vec![],
url: None,
rels: Default::default(),
lang: None,
#[cfg(feature = "metaformats")]
meta_item: None,
#[cfg(feature = "debug_flow")]
_debug_context: Default::default(),
};
let unrecognized = doc.unrecognized_items();
assert!(unrecognized.is_empty());
}
}