Skip to main content

synd_client/payload/
subscription.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use synd_feed::types::{Category, FeedType, FeedUrl, Requirement, Time};
3
4use super::PageInfo;
5
6#[derive(Debug, Clone, Deserialize)]
7pub struct SubscriptionPayload {
8    pub feeds: FeedConnection,
9}
10
11#[derive(Debug, Clone, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct FeedConnection {
14    pub nodes: Vec<SubscribedFeed>,
15    pub page_info: PageInfo,
16}
17
18#[derive(Debug, Clone, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SubscribedFeed {
21    pub url: FeedUrl,
22    #[serde(default, with = "super::requirement")]
23    pub requirement: Option<Requirement>,
24    pub category: Option<Category<'static>>,
25    pub crawl_policy: CrawlPolicy,
26    pub feed: Option<FeedDetails>,
27}
28
29#[derive(Debug, Clone, Deserialize)]
30#[serde(rename_all = "camelCase")]
31#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
32pub struct CrawlPolicy {
33    pub polling: PollingPolicy,
34}
35
36#[derive(Debug, Clone)]
37#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
38pub enum PollingPolicy {
39    Manual,
40    Interval {
41        seconds: PollingIntervalSeconds,
42    },
43    Other {
44        kind: String,
45        interval_seconds: Option<i64>,
46    },
47}
48
49impl<'de> Deserialize<'de> for PollingPolicy {
50    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        let wire = PollingPolicyWire::deserialize(deserializer)?;
55        match (wire.kind.as_str(), wire.interval_seconds) {
56            ("MANUAL", None) => Ok(Self::Manual),
57            ("MANUAL", Some(_)) => Err(serde::de::Error::custom(
58                "MANUAL polling policy must omit intervalSeconds",
59            )),
60            ("INTERVAL", Some(seconds)) => Ok(Self::Interval {
61                seconds: seconds.try_into().map_err(serde::de::Error::custom)?,
62            }),
63            ("INTERVAL", None) => Err(serde::de::Error::custom(
64                "INTERVAL polling policy requires intervalSeconds",
65            )),
66            (_, interval_seconds) => Ok(Self::Other {
67                kind: wire.kind,
68                interval_seconds,
69            }),
70        }
71    }
72}
73
74#[derive(Deserialize)]
75#[serde(rename_all = "camelCase")]
76struct PollingPolicyWire {
77    kind: String,
78    interval_seconds: Option<i64>,
79}
80
81#[derive(Debug, Clone, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct FeedDetails {
84    #[serde(rename = "type")]
85    pub feed_type: GraphqlFeedType,
86    pub title: Option<String>,
87    pub updated: Option<Time>,
88    pub website_url: Option<String>,
89    pub description: Option<String>,
90    pub generator: Option<String>,
91    pub entries: EntryMetaConnection,
92    pub links: LinkConnection,
93    pub authors: AuthorsConnection,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum GraphqlFeedType {
98    Atom,
99    Json,
100    Rss0,
101    Rss1,
102    Rss2,
103    Other(String),
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
107#[error("unsupported GraphQL feed type: {0}")]
108pub struct UnsupportedFeedType(pub String);
109
110impl TryFrom<GraphqlFeedType> for FeedType {
111    type Error = UnsupportedFeedType;
112
113    fn try_from(value: GraphqlFeedType) -> Result<Self, Self::Error> {
114        match value {
115            GraphqlFeedType::Atom => Ok(Self::Atom),
116            GraphqlFeedType::Json => Ok(Self::JSON),
117            GraphqlFeedType::Rss0 => Ok(Self::RSS0),
118            GraphqlFeedType::Rss1 => Ok(Self::RSS1),
119            GraphqlFeedType::Rss2 => Ok(Self::RSS2),
120            GraphqlFeedType::Other(value) => Err(UnsupportedFeedType(value)),
121        }
122    }
123}
124
125impl<'de> Deserialize<'de> for GraphqlFeedType {
126    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127    where
128        D: Deserializer<'de>,
129    {
130        let value = String::deserialize(deserializer)?;
131        Ok(match value.as_str() {
132            "ATOM" => Self::Atom,
133            "JSON" => Self::Json,
134            "RSS0" => Self::Rss0,
135            "RSS1" => Self::Rss1,
136            "RSS2" => Self::Rss2,
137            _ => Self::Other(value),
138        })
139    }
140}
141
142#[derive(Debug, Clone, Deserialize)]
143#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
144pub struct EntryMetaConnection {
145    pub nodes: Vec<EntryMeta>,
146}
147
148#[derive(Debug, Clone, Deserialize)]
149#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
150pub struct LinkConnection {
151    pub nodes: Vec<Link>,
152}
153
154#[derive(Debug, Clone, Deserialize)]
155pub struct AuthorsConnection {
156    pub nodes: Vec<String>,
157}
158
159#[derive(Debug, Clone, Deserialize)]
160#[serde(rename_all = "camelCase")]
161#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
162pub struct Link {
163    pub href: String,
164    pub rel: Option<String>,
165    pub media_type: Option<String>,
166    pub title: Option<String>,
167}
168
169#[derive(Debug, Clone, Deserialize)]
170#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
171pub struct EntryMeta {
172    pub title: Option<String>,
173    pub published: Option<Time>,
174    pub updated: Option<Time>,
175    pub summary: Option<String>,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct SubscribeFeedInput {
181    pub url: FeedUrl,
182    #[serde(default, with = "super::requirement")]
183    pub requirement: Option<Requirement>,
184    pub category: Option<Category<'static>>,
185    pub crawl_policy: Option<CrawlPolicyInput>,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
189#[serde(rename_all = "camelCase")]
190pub struct CrawlPolicyInput {
191    pub polling: PollingPolicyInput,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum PollingPolicyInput {
196    Manual,
197    Interval { seconds: PollingIntervalSeconds },
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
202pub struct PollingIntervalSeconds(i64);
203
204impl PollingIntervalSeconds {
205    pub fn get(self) -> i64 {
206        self.0
207    }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
211#[error("polling interval must be greater than zero")]
212pub struct InvalidPollingInterval;
213
214impl TryFrom<i64> for PollingIntervalSeconds {
215    type Error = InvalidPollingInterval;
216
217    fn try_from(seconds: i64) -> Result<Self, Self::Error> {
218        if seconds > 0 {
219            Ok(Self(seconds))
220        } else {
221            Err(InvalidPollingInterval)
222        }
223    }
224}
225
226impl Serialize for PollingPolicyInput {
227    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
228    where
229        S: serde::Serializer,
230    {
231        let wire = match self {
232            Self::Manual => PollingPolicyInputWire {
233                kind: PollingPolicyInputKind::Manual,
234                interval_seconds: None,
235            },
236            Self::Interval { seconds } => PollingPolicyInputWire {
237                kind: PollingPolicyInputKind::Interval,
238                interval_seconds: Some(seconds.get()),
239            },
240        };
241        wire.serialize(serializer)
242    }
243}
244
245#[derive(Serialize)]
246#[serde(rename_all = "camelCase")]
247struct PollingPolicyInputWire {
248    kind: PollingPolicyInputKind,
249    interval_seconds: Option<i64>,
250}
251
252#[derive(Serialize)]
253#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
254enum PollingPolicyInputKind {
255    Manual,
256    Interval,
257}
258
259#[derive(Debug, Clone, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct SubscribeFeedPayload {
262    pub status: ResponseStatus,
263    pub url: FeedUrl,
264    pub disposition: SubscribeDisposition,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub enum SubscribeDisposition {
269    Subscribed,
270    Changed,
271    Other(String),
272}
273
274impl<'de> Deserialize<'de> for SubscribeDisposition {
275    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276    where
277        D: Deserializer<'de>,
278    {
279        let value = String::deserialize(deserializer)?;
280        Ok(match value.as_str() {
281            "SUBSCRIBED" => Self::Subscribed,
282            "CHANGED" => Self::Changed,
283            _ => Self::Other(value),
284        })
285    }
286}
287
288#[derive(Debug, Clone, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct UnsubscribeFeedPayload {
291    pub status: ResponseStatus,
292    pub url: FeedUrl,
293    pub disposition: UnsubscribeDisposition,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub enum UnsubscribeDisposition {
298    Unsubscribed,
299    Other(String),
300}
301
302impl<'de> Deserialize<'de> for UnsubscribeDisposition {
303    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
304    where
305        D: Deserializer<'de>,
306    {
307        let value = String::deserialize(deserializer)?;
308        Ok(match value.as_str() {
309            "UNSUBSCRIBED" => Self::Unsubscribed,
310            _ => Self::Other(value),
311        })
312    }
313}
314
315#[derive(Debug, Clone, Deserialize)]
316pub struct ResponseStatus {
317    pub code: ResponseCode,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
321pub enum ResponseCode {
322    Ok,
323    Unauthorized,
324    InvalidFeedUrl,
325    FeedUnavailable,
326    InternalError,
327    Other(String),
328}
329
330impl<'de> Deserialize<'de> for ResponseCode {
331    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332    where
333        D: Deserializer<'de>,
334    {
335        let value = String::deserialize(deserializer)?;
336        Ok(match value.as_str() {
337            "OK" => Self::Ok,
338            "UNAUTHORIZED" => Self::Unauthorized,
339            "INVALID_FEED_URL" => Self::InvalidFeedUrl,
340            "FEED_UNAVAILABLE" => Self::FeedUnavailable,
341            "INTERNAL_ERROR" => Self::InternalError,
342            _ => Self::Other(value),
343        })
344    }
345}