microformats-types 0.15.0

A representation of the known objects of Microformats
Documentation
//! Parsed document container.

use crate::{Item, LanguageFilter, Relation, Relations};
use serde::ser::SerializeStruct;
use serde::{
    Deserialize, Deserializer, Serialize, Serializer,
    de::{self, Visitor},
};
use std::collections::{BTreeMap, HashSet};

/// A parsed Microformats2 document.
#[derive(Clone, Debug, PartialEq, Default, Eq)]
pub struct Document {
    /// The top-level items in the document.
    pub items: Vec<Item>,
    /// The URL of the document being parsed.
    pub url: Option<url::Url>,
    /// The rel-values defined in the document.
    pub rels: Relations,
    /// The language of the document.
    pub lang: Option<String>,
    #[cfg(feature = "metaformats")]
    pub meta_item: Option<Item>,
    #[cfg(feature = "debug_flow")]
    pub _debug_context: Option<crate::DebugContext>,
}

impl Document {
    /// Creates a new document with the given URL.
    pub fn new(url: Option<url::Url>) -> Self {
        Self {
            url,
            ..Default::default()
        }
    }

    /// Adds a relation to the document, merging with existing relations for the same URL.
    pub fn add_relation(&mut self, url: url::Url, relation: Relation) {
        if let Some(rel) = self.rels.items.get_mut(&url) {
            rel.merge_with(relation);
        } else {
            self.rels.items.insert(url.to_owned(), relation);
        }
    }
}

impl Serialize for Document {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        #[cfg(all(feature = "metaformats", feature = "debug_flow"))]
        let field_count = 6;
        #[cfg(all(not(feature = "metaformats"), feature = "debug_flow"))]
        let field_count = 5;
        #[cfg(all(feature = "metaformats", not(feature = "debug_flow")))]
        let field_count = 5;
        #[cfg(all(not(feature = "metaformats"), not(feature = "debug_flow")))]
        let field_count = 4;

        let mut s = serializer.serialize_struct("Document", field_count)?;

        s.serialize_field("items", &self.items)?;
        s.serialize_field("rel-urls", &self.rels.items)?;
        s.serialize_field("rels", &self.rels.by_rels())?;
        if let Some(lang) = &self.lang {
            s.serialize_field("lang", lang)?;
        } else {
            s.skip_field("lang")?;
        }
        #[cfg(feature = "metaformats")]
        if let Some(meta_item) = &self.meta_item {
            s.serialize_field("meta-item", meta_item)?;
        } else {
            s.skip_field("meta-item")?;
        }
        s.end()
    }
}

#[derive(serde::Deserialize, Debug)]
#[serde(field_identifier, rename_all = "kebab-case")]
enum DocumentDeserializationFields {
    Items,
    RelUrls,
    Rels,
    Url,
    Lang,
    #[cfg(feature = "metaformats")]
    MetaItem,
}

impl<'de> Deserialize<'de> for Document {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct DocumentVisitor;

        impl<'de> Visitor<'de> for DocumentVisitor {
            type Value = Document;

            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str("a Microformat document represented with the expected fields")
            }

            fn visit_map<V>(self, mut map: V) -> Result<Document, V::Error>
            where
                V: de::MapAccess<'de>,
            {
                let mut document = Document::default();
                let mut rel_urls: Option<Relations> = None;

                while let Ok(Some(key)) = map.next_key() {
                    match key {
                        DocumentDeserializationFields::Items => {
                            let raw_items = map.next_value::<Vec<Item>>()?;
                            document.items.extend(raw_items);
                        }
                        DocumentDeserializationFields::Url => {
                            if document.url.is_some() {
                                return Err(de::Error::duplicate_field("url"));
                            }

                            document.url = map.next_value()?;
                        }
                        DocumentDeserializationFields::RelUrls => {
                            if rel_urls.is_some() {
                                return Err(de::Error::duplicate_field("rel-urls"));
                            }

                            rel_urls = map.next_value()?;
                        }
                        DocumentDeserializationFields::Lang => {
                            if document.lang.is_some() {
                                return Err(de::Error::duplicate_field("lang"));
                            }

                            document.lang = map.next_value()?;
                        }
                        DocumentDeserializationFields::Rels => {
                            map.next_value::<BTreeMap<String, Vec<String>>>()?;
                        }
                        #[cfg(feature = "metaformats")]
                        DocumentDeserializationFields::MetaItem => {
                            if document.meta_item.is_some() {
                                return Err(de::Error::duplicate_field("meta-item"));
                            }

                            document.meta_item = map.next_value()?;
                        }
                    }
                }

                document.rels = rel_urls.unwrap_or_default();

                Ok(document)
            }
        }

        #[cfg(feature = "metaformats")]
        let expected_fields = &["items", "rel-urls", "url", "lang", "meta-item"];
        #[cfg(not(feature = "metaformats"))]
        let expected_fields = &["items", "rel-urls", "url", "lang"];

        deserializer.deserialize_struct("Document", expected_fields, DocumentVisitor)
    }
}

impl IntoIterator for Document {
    type Item = Item;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.items
            .into_iter()
            .flat_map(|i| i.into_iter())
            .collect::<Vec<Item>>()
            .into_iter()
    }
}

impl LanguageFilter for Document {
    fn matches_languages(&self, languages: &HashSet<&str>) -> bool {
        self.items
            .iter()
            .any(|item| item.matches_languages(languages))
    }

    fn filter_by_languages_set(&self, languages: &HashSet<&str>) -> Option<Self> {
        let filtered_items: Vec<Item> = self
            .items
            .iter()
            .filter_map(|item| item.filter_by_languages_set(languages))
            .collect();

        if filtered_items.is_empty() {
            return None;
        }

        Some(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(|item| item.filter_by_languages_set(languages)),
            #[cfg(feature = "debug_flow")]
            _debug_context: self._debug_context.clone(),
        })
    }
}