Skip to main content

rama_http/protocols/rss/rss2/
builder.rs

1use std::marker::PhantomData;
2
3use jiff::Timestamp;
4use rama_net::uri::Uri;
5use rama_utils::macros::generate_set_and_with;
6
7use super::types::{Missing, Present, Rss2Category, Rss2Feed, Rss2Image, Rss2Item};
8use crate::protocols::rss::atom::AtomLink;
9use crate::protocols::rss::feed_ext::FeedExtensions;
10
11/// Type-state builder for [`Rss2Feed`].
12///
13/// `T`, `L`, and `D` track whether `title`, `link`, and `description` have
14/// been provided. [`build`](Rss2FeedBuilder::build) is only available once
15/// all three are `Present`.
16pub struct Rss2FeedBuilder<T, L, D> {
17    pub(super) title: String,
18    pub(super) link: Option<Uri>,
19    pub(super) description: String,
20    pub(super) language: Option<String>,
21    pub(super) copyright: Option<String>,
22    pub(super) managing_editor: Option<String>,
23    pub(super) web_master: Option<String>,
24    pub(super) pub_date: Option<Timestamp>,
25    pub(super) last_build_date: Option<Timestamp>,
26    pub(super) categories: Vec<Rss2Category>,
27    pub(super) generator: Option<String>,
28    pub(super) docs: Option<String>,
29    pub(super) ttl: Option<u32>,
30    pub(super) image: Option<Rss2Image>,
31    pub(super) atom_links: Vec<AtomLink>,
32    pub(super) items: Vec<Rss2Item>,
33    pub(super) extensions: FeedExtensions,
34    pub(super) _pd: PhantomData<(T, L, D)>,
35}
36
37impl Rss2FeedBuilder<Missing, Missing, Missing> {
38    pub(super) fn new() -> Self {
39        Self {
40            title: String::new(),
41            link: None,
42            description: String::new(),
43            language: None,
44            copyright: None,
45            managing_editor: None,
46            web_master: None,
47            pub_date: None,
48            last_build_date: None,
49            categories: Vec::new(),
50            generator: None,
51            docs: None,
52            ttl: None,
53            image: None,
54            atom_links: Vec::new(),
55            items: Vec::new(),
56            extensions: FeedExtensions::default(),
57            _pd: PhantomData,
58        }
59    }
60}
61
62impl<L, D> Rss2FeedBuilder<Missing, L, D> {
63    #[must_use]
64    pub fn title(self, title: impl Into<String>) -> Rss2FeedBuilder<Present, L, D> {
65        Rss2FeedBuilder {
66            title: title.into(),
67            link: self.link,
68            description: self.description,
69            language: self.language,
70            copyright: self.copyright,
71            managing_editor: self.managing_editor,
72            web_master: self.web_master,
73            pub_date: self.pub_date,
74            last_build_date: self.last_build_date,
75            categories: self.categories,
76            generator: self.generator,
77            docs: self.docs,
78            ttl: self.ttl,
79            image: self.image,
80            atom_links: self.atom_links,
81            items: self.items,
82            extensions: self.extensions,
83            _pd: PhantomData,
84        }
85    }
86}
87
88impl<T, D> Rss2FeedBuilder<T, Missing, D> {
89    #[must_use]
90    pub fn link(self, link: Uri) -> Rss2FeedBuilder<T, Present, D> {
91        Rss2FeedBuilder {
92            title: self.title,
93            link: Some(link),
94            description: self.description,
95            language: self.language,
96            copyright: self.copyright,
97            managing_editor: self.managing_editor,
98            web_master: self.web_master,
99            pub_date: self.pub_date,
100            last_build_date: self.last_build_date,
101            categories: self.categories,
102            generator: self.generator,
103            docs: self.docs,
104            ttl: self.ttl,
105            image: self.image,
106            atom_links: self.atom_links,
107            items: self.items,
108            extensions: self.extensions,
109            _pd: PhantomData,
110        }
111    }
112}
113
114impl<T, L> Rss2FeedBuilder<T, L, Missing> {
115    #[must_use]
116    pub fn description(self, desc: impl Into<String>) -> Rss2FeedBuilder<T, L, Present> {
117        Rss2FeedBuilder {
118            title: self.title,
119            link: self.link,
120            description: desc.into(),
121            language: self.language,
122            copyright: self.copyright,
123            managing_editor: self.managing_editor,
124            web_master: self.web_master,
125            pub_date: self.pub_date,
126            last_build_date: self.last_build_date,
127            categories: self.categories,
128            generator: self.generator,
129            docs: self.docs,
130            ttl: self.ttl,
131            image: self.image,
132            atom_links: self.atom_links,
133            items: self.items,
134            extensions: self.extensions,
135            _pd: PhantomData,
136        }
137    }
138}
139
140impl<T, L, D> Rss2FeedBuilder<T, L, D> {
141    generate_set_and_with! {
142        pub fn language(mut self, lang: impl Into<String>) -> Self {
143            self.language = Some(lang.into());
144            self
145        }
146    }
147
148    generate_set_and_with! {
149        pub fn copyright(mut self, copyright: impl Into<String>) -> Self {
150            self.copyright = Some(copyright.into());
151            self
152        }
153    }
154
155    generate_set_and_with! {
156        pub fn managing_editor(mut self, editor: impl Into<String>) -> Self {
157            self.managing_editor = Some(editor.into());
158            self
159        }
160    }
161
162    generate_set_and_with! {
163        pub fn web_master(mut self, wm: impl Into<String>) -> Self {
164            self.web_master = Some(wm.into());
165            self
166        }
167    }
168
169    generate_set_and_with! {
170        pub fn pub_date(mut self, date: Timestamp) -> Self {
171            self.pub_date = Some(date);
172            self
173        }
174    }
175
176    generate_set_and_with! {
177        pub fn last_build_date(mut self, date: Timestamp) -> Self {
178            self.last_build_date = Some(date);
179            self
180        }
181    }
182
183    generate_set_and_with! {
184        /// Append a channel-level category. Call multiple times to attach more.
185        pub fn category(mut self, cat: Rss2Category) -> Self {
186            self.categories.push(cat);
187            self
188        }
189    }
190
191    generate_set_and_with! {
192        pub fn generator(mut self, generator: impl Into<String>) -> Self {
193            self.generator = Some(generator.into());
194            self
195        }
196    }
197
198    generate_set_and_with! {
199        /// RSS 2.0 `<docs>` — URL pointing at the RSS specification this
200        /// document conforms to. Optional; the conventional value is
201        /// `https://www.rssboard.org/rss-specification`.
202        pub fn docs(mut self, docs: impl Into<String>) -> Self {
203            self.docs = Some(docs.into());
204            self
205        }
206    }
207
208    generate_set_and_with! {
209        pub fn ttl(mut self, ttl: u32) -> Self {
210            self.ttl = Some(ttl);
211            self
212        }
213    }
214
215    generate_set_and_with! {
216        pub fn image(mut self, image: Rss2Image) -> Self {
217            self.image = Some(image);
218            self
219        }
220    }
221
222    generate_set_and_with! {
223        /// Append a channel-level `<atom:link>`. The conventional `rel="self"`
224        /// element required by podcast directories is built via
225        /// [`AtomLink::self_link`].
226        pub fn atom_link(mut self, link: AtomLink) -> Self {
227            self.atom_links.push(link);
228            self
229        }
230    }
231
232    generate_set_and_with! {
233        /// Append a single item. Call multiple times to attach more.
234        pub fn item(mut self, item: Rss2Item) -> Self {
235            self.items.push(item);
236            self
237        }
238    }
239
240    generate_set_and_with! {
241        pub fn items(mut self, items: impl IntoIterator<Item = Rss2Item>) -> Self {
242            self.items.extend(items);
243            self
244        }
245    }
246
247    generate_set_and_with! {
248        pub fn feed_extensions(mut self, ext: FeedExtensions) -> Self {
249            self.extensions = ext;
250            self
251        }
252    }
253}
254
255impl Rss2FeedBuilder<Present, Present, Present> {
256    #[must_use]
257    pub fn build(self) -> Rss2Feed {
258        Rss2Feed {
259            title: self.title,
260            #[expect(
261                clippy::expect_used,
262                reason = "type-state guarantees `link` is Present once build() is callable"
263            )]
264            link: self.link.expect("link is Present"),
265            description: self.description,
266            language: self.language,
267            copyright: self.copyright,
268            managing_editor: self.managing_editor,
269            web_master: self.web_master,
270            pub_date: self.pub_date,
271            last_build_date: self.last_build_date,
272            categories: self.categories,
273            generator: self.generator,
274            docs: self.docs,
275            ttl: self.ttl,
276            image: self.image,
277            atom_links: self.atom_links,
278            items: self.items,
279            extensions: self.extensions,
280        }
281    }
282}