synd_term/types/
mod.rs

1use chrono::DateTime;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use synd_feed::types::{Category, FeedType, FeedUrl, Requirement};
5use tracing::warn;
6
7use crate::{
8    client::synd_api::{
9        mutation,
10        query::{self},
11    },
12    ui,
13};
14
15mod time;
16pub use time::{Time, TimeExt};
17
18mod page_info;
19pub use page_info::PageInfo;
20
21mod requirement_ext;
22pub use requirement_ext::RequirementExt;
23
24pub(crate) mod github;
25
26#[derive(Debug, Clone)]
27#[cfg_attr(test, derive(fake::Dummy))]
28pub struct Link {
29    pub href: String,
30    pub rel: Option<String>,
31    pub media_type: Option<String>,
32    pub title: Option<String>,
33}
34
35impl From<query::subscription::Link> for Link {
36    fn from(v: query::subscription::Link) -> Self {
37        Self {
38            href: v.href,
39            rel: v.rel,
40            media_type: v.media_type,
41            title: v.title,
42        }
43    }
44}
45
46impl From<mutation::subscribe_feed::Link> for Link {
47    fn from(v: mutation::subscribe_feed::Link) -> Self {
48        Self {
49            href: v.href,
50            rel: v.rel,
51            media_type: v.media_type,
52            title: v.title,
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58#[cfg_attr(test, derive(fake::Dummy))]
59pub struct EntryMeta {
60    pub title: Option<String>,
61    pub published: Option<Time>,
62    pub updated: Option<Time>,
63    pub summary: Option<String>,
64}
65
66impl From<query::subscription::EntryMeta> for EntryMeta {
67    fn from(e: query::subscription::EntryMeta) -> Self {
68        Self {
69            title: e.title,
70            published: e.published.map(parse_time),
71            updated: e.updated.map(parse_time),
72            summary: e.summary,
73        }
74    }
75}
76
77impl From<mutation::subscribe_feed::EntryMeta> for EntryMeta {
78    fn from(e: mutation::subscribe_feed::EntryMeta) -> Self {
79        Self {
80            title: e.title,
81            published: e.published.map(parse_time),
82            updated: e.updated.map(parse_time),
83            summary: e.summary,
84        }
85    }
86}
87
88impl EntryMeta {
89    pub fn summary_text(&self, width: usize) -> Option<String> {
90        self.summary.as_deref().and_then(|summary| {
91            match html2text::from_read(summary.as_bytes(), width) {
92                Ok(text) => Some(text),
93                Err(err) => {
94                    warn!("convert summary html to text: {err}");
95                    None
96                }
97            }
98        })
99    }
100}
101
102#[derive(Debug, Clone)]
103#[cfg_attr(test, derive(fake::Dummy))]
104pub struct Feed {
105    pub feed_type: Option<FeedType>,
106    pub title: Option<String>,
107    pub url: FeedUrl,
108    pub updated: Option<Time>,
109    pub links: Vec<Link>,
110    pub website_url: Option<String>,
111    pub description: Option<String>,
112    pub generator: Option<String>,
113    pub entries: Vec<EntryMeta>,
114    pub authors: Vec<String>,
115    requirement: Option<Requirement>,
116    category: Option<Category<'static>>,
117}
118
119impl Feed {
120    pub fn requirement(&self) -> Requirement {
121        self.requirement.unwrap_or(ui::DEFAULT_REQUIREMNET)
122    }
123
124    pub fn category(&self) -> &Category<'static> {
125        self.category.as_ref().unwrap_or(ui::default_category())
126    }
127
128    #[must_use]
129    pub fn with_url(self, url: FeedUrl) -> Self {
130        Self { url, ..self }
131    }
132
133    #[must_use]
134    pub fn with_requirement(self, requirement: Requirement) -> Self {
135        Self {
136            requirement: Some(requirement),
137            ..self
138        }
139    }
140
141    #[must_use]
142    pub fn with_category(self, category: Category<'static>) -> Self {
143        Self {
144            category: Some(category),
145            ..self
146        }
147    }
148}
149
150impl From<query::subscription::Feed> for Feed {
151    fn from(f: query::subscription::Feed) -> Self {
152        Self {
153            feed_type: match f.type_ {
154                query::subscription::FeedType::ATOM => Some(FeedType::Atom),
155                query::subscription::FeedType::RSS1 => Some(FeedType::RSS1),
156                query::subscription::FeedType::RSS2 => Some(FeedType::RSS2),
157                query::subscription::FeedType::RSS0 => Some(FeedType::RSS0),
158                query::subscription::FeedType::JSON => Some(FeedType::JSON),
159                query::subscription::FeedType::Other(_) => None,
160            },
161            title: f.title,
162            url: f.url,
163            updated: f.updated.map(parse_time),
164            links: f.links.nodes.into_iter().map(From::from).collect(),
165            website_url: f.website_url,
166            description: f.description,
167            generator: f.generator,
168            entries: f.entries.nodes.into_iter().map(From::from).collect(),
169            authors: f.authors.nodes,
170            requirement: f.requirement.and_then(|r| match r {
171                query::subscription::Requirement::MUST => Some(Requirement::Must),
172                query::subscription::Requirement::SHOULD => Some(Requirement::Should),
173                query::subscription::Requirement::MAY => Some(Requirement::May),
174                query::subscription::Requirement::Other(_) => None,
175            }),
176            category: f.category,
177        }
178    }
179}
180
181impl From<mutation::subscribe_feed::Feed> for Feed {
182    fn from(f: mutation::subscribe_feed::Feed) -> Self {
183        Self {
184            feed_type: match f.type_ {
185                mutation::subscribe_feed::FeedType::ATOM => Some(FeedType::Atom),
186                mutation::subscribe_feed::FeedType::RSS1 => Some(FeedType::RSS1),
187                mutation::subscribe_feed::FeedType::RSS2 => Some(FeedType::RSS2),
188                mutation::subscribe_feed::FeedType::RSS0 => Some(FeedType::RSS0),
189                mutation::subscribe_feed::FeedType::JSON => Some(FeedType::JSON),
190                mutation::subscribe_feed::FeedType::Other(_) => None,
191            },
192            title: f.title,
193            url: f.url,
194            updated: f.updated.map(parse_time),
195            links: f.links.nodes.into_iter().map(From::from).collect(),
196            website_url: f.website_url,
197            description: f.description,
198            generator: f.generator,
199            entries: f.entries.nodes.into_iter().map(From::from).collect(),
200            authors: f.authors.nodes,
201            requirement: f.requirement.and_then(|r| match r {
202                mutation::subscribe_feed::Requirement::MUST => Some(Requirement::Must),
203                mutation::subscribe_feed::Requirement::SHOULD => Some(Requirement::Should),
204                mutation::subscribe_feed::Requirement::MAY => Some(Requirement::May),
205                mutation::subscribe_feed::Requirement::Other(_) => None,
206            }),
207            category: f.category,
208        }
209    }
210}
211
212#[derive(Debug, Clone)]
213pub struct Entry {
214    pub title: Option<String>,
215    pub published: Option<Time>,
216    pub updated: Option<Time>,
217    pub website_url: Option<String>,
218    pub summary: Option<String>,
219    pub feed_title: Option<String>,
220    pub feed_url: FeedUrl,
221    requirement: Option<Requirement>,
222    category: Option<Category<'static>>,
223}
224
225impl Entry {
226    pub fn summary_text(&self, width: usize) -> Option<String> {
227        self.summary.as_deref().map(|summary| {
228            html2text::config::plain()
229                .string_from_read(summary.as_bytes(), width)
230                .unwrap_or_default()
231        })
232    }
233
234    pub fn requirement(&self) -> Requirement {
235        self.requirement.unwrap_or(ui::DEFAULT_REQUIREMNET)
236    }
237
238    pub fn category(&self) -> &Category<'static> {
239        self.category
240            .as_ref()
241            .unwrap_or_else(|| ui::default_category())
242    }
243}
244
245impl From<query::entries::Entry> for Entry {
246    fn from(v: query::entries::Entry) -> Self {
247        Self {
248            title: v.title,
249            published: v.published.map(parse_time),
250            updated: v.updated.map(parse_time),
251            website_url: v.website_url,
252            feed_title: v.feed.title,
253            feed_url: v.feed.url,
254            summary: v.summary,
255            requirement: match v.feed.requirement {
256                Some(query::entries::Requirement::MUST) => Some(Requirement::Must),
257                Some(query::entries::Requirement::SHOULD) => Some(Requirement::Should),
258                Some(query::entries::Requirement::MAY) => Some(Requirement::May),
259                _ => None,
260            },
261            category: v.feed.category,
262        }
263    }
264}
265
266#[derive(Serialize, Deserialize, JsonSchema)]
267pub struct ExportedFeed {
268    pub title: Option<String>,
269    pub url: FeedUrl,
270    pub requirement: Option<Requirement>,
271    pub category: Option<Category<'static>>,
272}
273
274impl From<query::export_subscription::ExportSubscriptionOutputFeedsNodes> for ExportedFeed {
275    fn from(v: query::export_subscription::ExportSubscriptionOutputFeedsNodes) -> Self {
276        Self {
277            title: v.title,
278            url: v.url,
279            requirement: v.requirement.and_then(|r| match r {
280                query::export_subscription::Requirement::MUST => Some(Requirement::Must),
281                query::export_subscription::Requirement::SHOULD => Some(Requirement::Should),
282                query::export_subscription::Requirement::MAY => Some(Requirement::May),
283                query::export_subscription::Requirement::Other(_) => None,
284            }),
285            category: v.category,
286        }
287    }
288}
289
290impl From<ExportedFeed> for mutation::subscribe_feed::SubscribeFeedInput {
291    fn from(feed: ExportedFeed) -> Self {
292        Self {
293            url: feed.url,
294            requirement: feed.requirement.map(|r| match r {
295                Requirement::Must => mutation::subscribe_feed::Requirement::MUST,
296                Requirement::Should => mutation::subscribe_feed::Requirement::SHOULD,
297                Requirement::May => mutation::subscribe_feed::Requirement::MAY,
298            }),
299            category: feed.category,
300        }
301    }
302}
303
304fn parse_time(t: impl AsRef<str>) -> Time {
305    DateTime::parse_from_rfc3339(t.as_ref())
306        .expect("invalid rfc3339 time")
307        .with_timezone(&chrono::Utc)
308}