microformats-types 0.15.0

A representation of the known objects of Microformats
Documentation
//! Property value types.

use crate::temporal;
use crate::{Class, Item, TextValue, UrlValue};
use serde::{
    Deserialize, Deserializer, Serialize, Serializer,
    de::{self, MapAccess, Visitor},
};
use std::fmt;
use std::str::FromStr;

/// A list of property values.
pub type NodeList = Vec<PropertyValue>;
/// A map from property names to their values.
pub type Properties = std::collections::BTreeMap<String, NodeList>;

fn short_circuit_url_deserialization<'de, D>(d: D) -> Result<url::Url, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let string_form = String::deserialize(d)?;
    let url_form = url::Url::parse(&string_form).map_err(serde::de::Error::custom)?;

    if url_form.as_str() != string_form {
        Err(serde::de::Error::custom(
            "This string doesn't represent a valid URL despite looking like one.",
        ))
    } else {
        Ok(url_form)
    }
}

fn short_circuit_plain_text_deserialization<'de, D>(d: D) -> Result<String, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let string_form = String::deserialize(d)?;

    url::Url::from_str(&string_form)
        .map_err(serde::de::Error::custom)
        .map(|u| u.as_str().to_string())
        .and_then(|u| {
            if u == string_form && !u.contains(|c: char| c.is_whitespace()) && !u.contains('\n') {
                Err(serde::de::Error::invalid_type(
                    de::Unexpected::Other("URL"),
                    &"plain 'ol string",
                ))
            } else {
                Ok(string_form.clone())
            }
        })
        .or_else(|r: D::Error| {
            if r.to_string().starts_with("invalid type: URL") {
                Err(r)
            } else {
                temporal::Value::from_str(&string_form)
                    .map_err(serde::de::Error::custom)
                    .map(|u| u.to_string())
                    .and_then(|u| {
                        if u == string_form {
                            Err(serde::de::Error::invalid_type(
                                de::Unexpected::Str("temporal data"),
                                &"plain 'ol string",
                            ))
                        } else {
                            Ok(string_form.clone())
                        }
                    })
            }
        })
        .or_else(|r: D::Error| {
            if r.to_string().starts_with("invalid type: URL")
                || r.to_string().contains("temporal data")
            {
                Err(r)
            } else {
                Ok(string_form)
            }
        })
}

fn short_circuit_text_value_deserialization<'de, D>(d: D) -> Result<TextValue, D::Error>
where
    D: serde::Deserializer<'de>,
{
    short_circuit_plain_text_deserialization(d).map(TextValue::new)
}

fn short_circuit_url_value_deserialization<'de, D>(d: D) -> Result<UrlValue, D::Error>
where
    D: serde::Deserializer<'de>,
{
    short_circuit_url_deserialization(d).map(UrlValue::new)
}

/// The value of a microformat property.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum PropertyValue {
    /// A plain text value.
    #[serde(deserialize_with = "short_circuit_text_value_deserialization")]
    Plain(TextValue),

    /// A URL value.
    #[serde(deserialize_with = "short_circuit_url_value_deserialization")]
    Url(UrlValue),

    /// A temporal value (date, time, or duration).
    Temporal(temporal::Value),

    /// An HTML fragment with plain text value.
    Fragment(Fragment),

    /// A nested microformat item.
    #[serde(with = "referenced_item")]
    Item(Item),

    /// An image with optional alt text.
    Image(Image),
}

impl PropertyValue {
    /// Returns true if this property value is empty.
    pub fn is_empty(&self) -> bool {
        match self {
            Self::Temporal(_) | Self::Url(_) | Self::Image(_) => false,
            Self::Plain(s) => s.is_empty(),
            Self::Fragment(f) => f.is_empty(),
            Self::Item(i) => i.is_empty(),
        }
    }
}

impl From<Fragment> for PropertyValue {
    fn from(v: Fragment) -> Self {
        Self::Fragment(v)
    }
}

impl From<Item> for PropertyValue {
    fn from(item: Item) -> Self {
        PropertyValue::Item(item)
    }
}

impl From<String> for PropertyValue {
    fn from(s: String) -> Self {
        PropertyValue::Plain(TextValue::new(s))
    }
}

impl From<&str> for PropertyValue {
    fn from(s: &str) -> Self {
        PropertyValue::Plain(TextValue::new(s.to_string()))
    }
}

impl From<temporal::Stamp> for PropertyValue {
    fn from(t: temporal::Stamp) -> Self {
        Self::Temporal(temporal::Value::Timestamp(t))
    }
}

impl From<temporal::Duration> for PropertyValue {
    fn from(t: temporal::Duration) -> Self {
        Self::Temporal(temporal::Value::Duration(t))
    }
}

/// A property value with optional metadata.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct PropertyWithMetadata {
    /// The property value.
    pub value: PropertyValue,
    /// Optional metadata (reserved for future use).
    pub metadata: Option<()>,
}

/// An HTML fragment with plain text representation.
#[derive(
    Debug, Clone, PartialEq, Eq, serde::Serialize, Default, serde::Deserialize, PartialOrd, Ord,
)]
#[serde(rename_all = "kebab-case")]
pub struct Fragment {
    /// The HTML markup.
    #[serde(skip_serializing_if = "String::is_empty")]
    pub html: String,

    /// The plain text value.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub value: String,

    /// The language of the fragment.
    #[cfg(feature = "per_element_lang")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lang: Option<String>,

    /// URLs referenced in the fragment.
    #[serde(skip)]
    pub links: Vec<String>,
}

impl Fragment {
    /// Creates a new Fragment with the given HTML, value, and links.
    #[cfg(not(feature = "per_element_lang"))]
    pub fn new(html: String, value: String, links: Vec<String>) -> Self {
        Self { html, value, links }
    }

    /// Creates a new Fragment with the given HTML, value, links, and optional language.
    #[cfg(feature = "per_element_lang")]
    pub fn new(html: String, value: String, links: Vec<String>, lang: Option<String>) -> Self {
        Self {
            html,
            value,
            lang,
            links,
        }
    }

    /// Creates a new Fragment with the given HTML, value, and links.
    /// Language is set to None when per_element_lang is enabled.
    #[cfg(feature = "per_element_lang")]
    pub fn new_without_lang(html: String, value: String, links: Vec<String>) -> Self {
        Self {
            html,
            value,
            lang: None,
            links,
        }
    }

    /// Returns true if the fragment has no value.
    pub fn is_empty(&self) -> bool {
        self.value.is_empty()
    }

    /// Returns the links referenced in this fragment.
    pub fn links(&self) -> &[String] {
        &self.links
    }
}

/// An image with optional alt text.
#[derive(Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub struct Image {
    /// The image URL.
    pub value: url::Url,

    /// Alternative text for the image.
    #[serde(default)]
    pub alt: Option<String>,
}

impl std::fmt::Debug for Image {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Image")
            .field("value", &self.value.to_string())
            .field("alt", &self.alt)
            .finish()
    }
}

mod referenced_item {
    use super::*;
    use crate::{Items, Properties, ValueKind};
    use std::collections::BTreeMap;

    type Value = Item;

    struct ItemVisitor;

    #[derive(serde::Deserialize, Debug)]
    #[serde(field_identifier, rename_all = "kebab-case")]
    enum ItemDeserializationFields {
        Children,
        Value,
        Id,
        Properties,
        r#Type,
    }

    impl<'de> Visitor<'de> for ItemVisitor {
        type Value = Value;
        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("expecting null or an map representing an item")
        }

        fn visit_map<A>(self, mut item_map: A) -> Result<Self::Value, A::Error>
        where
            A: MapAccess<'de>,
        {
            let mut children: Items = Default::default();
            let mut value: Option<ValueKind> = Default::default();
            let mut id: Option<String> = Default::default();
            let mut types = Vec::new();
            let mut properties = Properties::default();

            while let Some(property) = item_map.next_key()? {
                match property {
                    ItemDeserializationFields::Children => {
                        let new_items = item_map.next_value::<Vec<Item>>()?.into_iter();

                        if children.is_empty() && new_items.len() > 0 {
                            children = new_items.collect::<Vec<Item>>().into();
                        } else {
                            children.extend(new_items);
                        }
                    }
                    ItemDeserializationFields::Value => {
                        if value.is_none() {
                            value = item_map.next_value::<Option<ValueKind>>()?;
                        }
                    }
                    ItemDeserializationFields::Id => {
                        if id.is_none() {
                            id = item_map.next_value::<Option<String>>()?;
                        }
                    }
                    ItemDeserializationFields::Type => {
                        types.extend(item_map.next_value::<Vec<Class>>()?);
                    }
                    ItemDeserializationFields::Properties => {
                        properties.extend(item_map.next_value::<BTreeMap<String, _>>()?);
                    }
                }
            }

            let item = Item {
                r#type: types,
                properties,
                id,
                value,
                children,
                ..Default::default()
            };

            Ok(item)
        }
    }

    pub fn serialize<S>(item: &Value, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_some(&Some(item))
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_struct(
            "Item",
            &["type", "properties", "id", "value", "children"],
            ItemVisitor,
        )
    }
}