indieweb 0.10.0

A collection of utilities for working with the IndieWeb.
Documentation
/// Represents the potential statuses a post can take.
///
/// More information about this can be found at <https://indieweb.org/post_status>.
#[derive(Debug, Clone, PartialEq, Hash, Eq, Default)]
pub enum Status {
    /// The content is available for general use.
    #[default]
    Published,
    /// The content is not yet ready for general use.
    Drafted,
    /// The content has been considered "deleted".
    Deleted,
    /// The content has expired from general use.
    Expired,
    /// The content will be published after the specified delay.
    ///
    /// Parsed from syntax like `"published+PT3M"` or `"published+PT1H30M"`.
    ///
    /// See <https://github.com/indieweb/micropub-extensions/issues/47>
    #[cfg(feature = "experimental_publish_delay")]
    DelayedPublish(super::PublishDelay),
    /// A custom status that posts can have.
    Other(String),
}

impl serde::Serialize for Status {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.to_string().serialize(serializer)
    }
}

impl<'de> serde::Deserialize<'de> for Status {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        String::deserialize(deserializer)
            .and_then(|status_str| status_str.parse().map_err(serde::de::Error::custom))
    }
}

impl std::fmt::Display for Status {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Status::Published => f.write_str("published"),
            Status::Drafted => f.write_str("draft"),
            Status::Deleted => f.write_str("deleted"),
            Status::Expired => f.write_str("expired"),
            #[cfg(feature = "experimental_publish_delay")]
            Status::DelayedPublish(delay) => write!(f, "published+{}", delay),
            Status::Other(status) => f.write_str(status),
        }
    }
}

impl std::str::FromStr for Status {
    type Err = crate::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let status_str = s.trim().trim_matches('"');
        if status_str.is_empty() {
            return Ok(Self::Published);
        }

        #[cfg(feature = "experimental_publish_delay")]
        if let Some((status_part, delay_part)) = status_str.split_once('+') {
            if status_part.to_lowercase() == "published" {
                let delay = delay_part.parse::<super::PublishDelay>()?;
                return Ok(Self::DelayedPublish(delay));
            }
            return Err(crate::Error::InvalidPostStatus(format!(
                "only 'published' can have a delay suffix, got '{}'",
                status_part
            )));
        }

        let status_lower = status_str.to_lowercase();
        match status_lower.as_str() {
            "published" => Ok(Self::Published),
            "draft" => Ok(Self::Drafted),
            "deleted" => Ok(Self::Deleted),
            "expired" => Ok(Self::Expired),
            other => Ok(Self::Other(other.to_string())),
        }
    }
}

#[test]
fn post_status() {
    use std::str::FromStr;
    assert_eq!(Some(Status::Drafted), Status::from_str("draft").ok());
    assert_eq!(Some(Status::Published), Status::from_str("published").ok());
    assert_eq!(Some(Status::Published), "published".parse().ok());
    assert_eq!(Some(Status::Published), Status::from_str("PublisHed").ok());
    assert_eq!(Some(Status::Published), "PublisHed".parse().ok());
}

#[cfg(test)]
#[cfg(feature = "experimental_publish_delay")]
mod publish_delay_tests {
    use super::*;

    #[test]
    fn delayed_publish_parsing() {
        let status: Status = "published+PT3M".parse().unwrap();
        assert!(matches!(status, Status::DelayedPublish(_)));
    }

    #[test]
    fn delayed_publish_display() {
        let status: Status = "published+PT3M".parse().unwrap();
        assert_eq!(status.to_string(), "published+PT3M");
    }

    #[test]
    fn delayed_publish_serialization() {
        let status: Status = "published+PT1H".parse().unwrap();
        assert_eq!(
            serde_json::to_string(&status).unwrap(),
            "\"published+PT1H\""
        );
    }

    #[test]
    fn delayed_publish_deserialization() {
        let status: Status = serde_json::from_str("\"published+PT30M\"").unwrap();
        assert!(matches!(status, Status::DelayedPublish(_)));
    }

    #[test]
    fn delayed_publish_invalid_suffix() {
        let result: Result<Status, _> = "draft+PT3M".parse();
        assert!(result.is_err());
    }

    #[test]
    fn delayed_publish_invalid_duration() {
        let result: Result<Status, _> = "published+invalid".parse();
        assert!(result.is_err());
    }
}