synd-client 0.4.0

syndicationd API client
Documentation
use serde::{Deserialize, Deserializer, Serialize};
use synd_feed::types::{Category, FeedType, FeedUrl, Requirement, Time};

use super::PageInfo;

#[derive(Debug, Clone, Deserialize)]
pub struct SubscriptionPayload {
    pub feeds: FeedConnection,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FeedConnection {
    pub nodes: Vec<SubscribedFeed>,
    pub page_info: PageInfo,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribedFeed {
    pub url: FeedUrl,
    #[serde(default, with = "super::requirement")]
    pub requirement: Option<Requirement>,
    pub category: Option<Category<'static>>,
    pub crawl_policy: CrawlPolicy,
    pub feed: Option<FeedDetails>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
pub struct CrawlPolicy {
    pub polling: PollingPolicy,
}

#[derive(Debug, Clone)]
#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
pub enum PollingPolicy {
    Manual,
    Interval {
        seconds: PollingIntervalSeconds,
    },
    Other {
        kind: String,
        interval_seconds: Option<i64>,
    },
}

impl<'de> Deserialize<'de> for PollingPolicy {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let wire = PollingPolicyWire::deserialize(deserializer)?;
        match (wire.kind.as_str(), wire.interval_seconds) {
            ("MANUAL", None) => Ok(Self::Manual),
            ("MANUAL", Some(_)) => Err(serde::de::Error::custom(
                "MANUAL polling policy must omit intervalSeconds",
            )),
            ("INTERVAL", Some(seconds)) => Ok(Self::Interval {
                seconds: seconds.try_into().map_err(serde::de::Error::custom)?,
            }),
            ("INTERVAL", None) => Err(serde::de::Error::custom(
                "INTERVAL polling policy requires intervalSeconds",
            )),
            (_, interval_seconds) => Ok(Self::Other {
                kind: wire.kind,
                interval_seconds,
            }),
        }
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PollingPolicyWire {
    kind: String,
    interval_seconds: Option<i64>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FeedDetails {
    #[serde(rename = "type")]
    pub feed_type: GraphqlFeedType,
    pub title: Option<String>,
    pub updated: Option<Time>,
    pub website_url: Option<String>,
    pub description: Option<String>,
    pub generator: Option<String>,
    pub entries: EntryMetaConnection,
    pub links: LinkConnection,
    pub authors: AuthorsConnection,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphqlFeedType {
    Atom,
    Json,
    Rss0,
    Rss1,
    Rss2,
    Other(String),
}

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unsupported GraphQL feed type: {0}")]
pub struct UnsupportedFeedType(pub String);

impl TryFrom<GraphqlFeedType> for FeedType {
    type Error = UnsupportedFeedType;

    fn try_from(value: GraphqlFeedType) -> Result<Self, Self::Error> {
        match value {
            GraphqlFeedType::Atom => Ok(Self::Atom),
            GraphqlFeedType::Json => Ok(Self::JSON),
            GraphqlFeedType::Rss0 => Ok(Self::RSS0),
            GraphqlFeedType::Rss1 => Ok(Self::RSS1),
            GraphqlFeedType::Rss2 => Ok(Self::RSS2),
            GraphqlFeedType::Other(value) => Err(UnsupportedFeedType(value)),
        }
    }
}

impl<'de> Deserialize<'de> for GraphqlFeedType {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        Ok(match value.as_str() {
            "ATOM" => Self::Atom,
            "JSON" => Self::Json,
            "RSS0" => Self::Rss0,
            "RSS1" => Self::Rss1,
            "RSS2" => Self::Rss2,
            _ => Self::Other(value),
        })
    }
}

#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
pub struct EntryMetaConnection {
    pub nodes: Vec<EntryMeta>,
}

#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
pub struct LinkConnection {
    pub nodes: Vec<Link>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct AuthorsConnection {
    pub nodes: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
pub struct Link {
    pub href: String,
    pub rel: Option<String>,
    pub media_type: Option<String>,
    pub title: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
pub struct EntryMeta {
    pub title: Option<String>,
    pub published: Option<Time>,
    pub updated: Option<Time>,
    pub summary: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribeFeedInput {
    pub url: FeedUrl,
    #[serde(default, with = "super::requirement")]
    pub requirement: Option<Requirement>,
    pub category: Option<Category<'static>>,
    pub crawl_policy: Option<CrawlPolicyInput>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CrawlPolicyInput {
    pub polling: PollingPolicyInput,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PollingPolicyInput {
    Manual,
    Interval { seconds: PollingIntervalSeconds },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
pub struct PollingIntervalSeconds(i64);

impl PollingIntervalSeconds {
    pub fn get(self) -> i64 {
        self.0
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("polling interval must be greater than zero")]
pub struct InvalidPollingInterval;

impl TryFrom<i64> for PollingIntervalSeconds {
    type Error = InvalidPollingInterval;

    fn try_from(seconds: i64) -> Result<Self, Self::Error> {
        if seconds > 0 {
            Ok(Self(seconds))
        } else {
            Err(InvalidPollingInterval)
        }
    }
}

impl Serialize for PollingPolicyInput {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let wire = match self {
            Self::Manual => PollingPolicyInputWire {
                kind: PollingPolicyInputKind::Manual,
                interval_seconds: None,
            },
            Self::Interval { seconds } => PollingPolicyInputWire {
                kind: PollingPolicyInputKind::Interval,
                interval_seconds: Some(seconds.get()),
            },
        };
        wire.serialize(serializer)
    }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PollingPolicyInputWire {
    kind: PollingPolicyInputKind,
    interval_seconds: Option<i64>,
}

#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum PollingPolicyInputKind {
    Manual,
    Interval,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribeFeedPayload {
    pub status: ResponseStatus,
    pub url: FeedUrl,
    pub disposition: SubscribeDisposition,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubscribeDisposition {
    Subscribed,
    Changed,
    Other(String),
}

impl<'de> Deserialize<'de> for SubscribeDisposition {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        Ok(match value.as_str() {
            "SUBSCRIBED" => Self::Subscribed,
            "CHANGED" => Self::Changed,
            _ => Self::Other(value),
        })
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UnsubscribeFeedPayload {
    pub status: ResponseStatus,
    pub url: FeedUrl,
    pub disposition: UnsubscribeDisposition,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnsubscribeDisposition {
    Unsubscribed,
    Other(String),
}

impl<'de> Deserialize<'de> for UnsubscribeDisposition {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        Ok(match value.as_str() {
            "UNSUBSCRIBED" => Self::Unsubscribed,
            _ => Self::Other(value),
        })
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ResponseStatus {
    pub code: ResponseCode,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseCode {
    Ok,
    Unauthorized,
    InvalidFeedUrl,
    FeedUnavailable,
    InternalError,
    Other(String),
}

impl<'de> Deserialize<'de> for ResponseCode {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        Ok(match value.as_str() {
            "OK" => Self::Ok,
            "UNAUTHORIZED" => Self::Unauthorized,
            "INVALID_FEED_URL" => Self::InvalidFeedUrl,
            "FEED_UNAVAILABLE" => Self::FeedUnavailable,
            "INTERNAL_ERROR" => Self::InternalError,
            _ => Self::Other(value),
        })
    }
}