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}