indieweb 0.10.0

A collection of utilities for working with the IndieWeb.
Documentation
use crate::{http, mf2};
use ::http::Request;
use microformats::types::{Class, KnownClass, PropertyValue};

fn check_if_property_values_have_url(values: &[PropertyValue], url: &url::Url) -> bool {
    values
        .iter()
        .filter_map(|v| {
            if let PropertyValue::Url(u) = v {
                Some(u)
            } else {
                None
            }
        })
        .any(|v| **v == *url)
}

/// Fetches the representative h-card of the remote URL.
///
/// # Examples
///
/// ```not-rust
/// # use indieweb::algorithms::representative_hcard as rep_hcard;
/// # use indieweb::http::ureq::Client as UreqHttpClient;
/// # use url::Url;
/// #
/// # let http_client = UreqHttpClient::default();
/// # let u: Url = "https://jacky.wtf".parse().unwrap();
/// #
/// assert!(rep_hcard::for_url(&http_client, &u).await.is_ok(), "found the h-card");
/// ```
#[tracing::instrument(skip(client))]
pub async fn for_url(
    client: &impl http::Client,
    url: &url::Url,
) -> Result<microformats::types::Item, crate::Error> {
    let resp = client.send_request(
        Request::builder()
            .method("GET")
            .uri(url.to_string())
            .header(::http::header::ACCEPT, "text/html, text/mf2+html, application/json, application/mf2+json, application/jf2+json")
            .body(crate::http::Body::Empty)
            .map_err(crate::Error::Http)?,
    ).await?;
    let doc =
        mf2::http::to_mf2_document(resp.map(|body| body.as_bytes().to_vec()), url.as_str())
            .map_err(crate::Error::from)?;

    from_document(&doc, url).await
}

/// Determines the representative h-card for a [document][microformats::types::Document].
///
/// # Errors
///
/// This function will return an error if no representative card could be found.
pub async fn from_document(
    document: &microformats::types::Document,
    url: &url::Url,
) -> Result<microformats::types::Item, crate::Error> {
    let cards = document
        .items
        .iter()
        .filter(|item| item.r#type.contains(&Class::Known(KnownClass::Card)))
        .cloned()
        .collect::<Vec<_>>();

    tracing::trace!(card_count = cards.len(), "Found cards at this URL.");

    let rel_for_url = document.rels.items.get(url).cloned().unwrap_or_default();

    if let Some(directly_specified_card) = cards
        .iter()
        .find(|card_item| {
            let item_uids = card_item.get_property("uid").unwrap_or_default();
            let item_urls = card_item.get_property("url").unwrap_or_default();

            check_if_property_values_have_url(&item_uids, url)
                && check_if_property_values_have_url(&item_urls, url)
        })
        .cloned()
    {
        tracing::trace!("Found the representative h-card directly on the page.");
        Ok(directly_specified_card.to_owned())
    } else if let Some(rel_me_assoc_card) = cards
        .iter()
        .find(|card_item| {
            rel_for_url.rels.contains(&"me".into())
                && check_if_property_values_have_url(
                    &card_item.get_property("url").unwrap_or_default(),
                    url,
                )
        })
        .cloned()
    {
        tracing::trace!("Found the representative h-card via rel=me discovery.");
        Ok(rel_me_assoc_card.to_owned())
    } else if cards.len() == 1 {
        tracing::trace!("Attempting to use the only card found on the page.");
        cards
            .first()
            .ok_or_else(|| crate::Error::NoRepresentativeHCardFound(url.clone()))
            .cloned()
    } else {
        tracing::trace!("Algorithm exhausted all options on discovery; found nothing.");
        Err(crate::Error::NoRepresentativeHCardFound(url.clone()))
    }
}

#[cfg(test)]
mod test;