twilight_util/builder/embed/
mod.rs

1//! Create an [`Embed`] with a builder.
2
3pub mod image_source;
4
5mod author;
6mod field;
7mod footer;
8
9pub use self::{
10    author::EmbedAuthorBuilder, field::EmbedFieldBuilder, footer::EmbedFooterBuilder,
11    image_source::ImageSource,
12};
13
14use twilight_model::{
15    channel::message::embed::{
16        Embed, EmbedAuthor, EmbedField, EmbedFooter, EmbedImage, EmbedThumbnail,
17    },
18    util::Timestamp,
19};
20use twilight_validate::embed::{embed as validate_embed, EmbedValidationError};
21
22/// Create an [`Embed`] with a builder.
23///
24/// # Examples
25///
26/// Build a simple embed:
27///
28/// ```no_run
29/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
30/// use twilight_util::builder::embed::{EmbedBuilder, EmbedFieldBuilder};
31///
32/// let embed = EmbedBuilder::new()
33///     .description("Here's a list of reasons why Twilight is the best pony:")
34///     .field(EmbedFieldBuilder::new("Wings", "She has wings.").inline())
35///     .field(
36///         EmbedFieldBuilder::new("Horn", "She can do magic, and she's really good at it.")
37///             .inline(),
38///     )
39///     .validate()?
40///     .build();
41/// # Ok(()) }
42/// ```
43///
44/// Build an embed with an image:
45///
46/// ```no_run
47/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
48/// use twilight_util::builder::embed::{EmbedBuilder, ImageSource};
49///
50/// let embed = EmbedBuilder::new()
51///     .description("Here's a cool image of Twilight Sparkle")
52///     .image(ImageSource::attachment("bestpony.png")?)
53///     .validate()?
54///     .build();
55/// # Ok(()) }
56/// ```
57#[derive(Clone, Debug, Eq, PartialEq)]
58#[must_use = "must be built into an embed"]
59pub struct EmbedBuilder(Embed);
60
61impl EmbedBuilder {
62    /// Create a new embed builder.
63    pub fn new() -> Self {
64        EmbedBuilder(Embed {
65            author: None,
66            color: None,
67            description: None,
68            fields: Vec::new(),
69            footer: None,
70            image: None,
71            kind: "rich".to_owned(),
72            provider: None,
73            thumbnail: None,
74            timestamp: None,
75            title: None,
76            url: None,
77            video: None,
78        })
79    }
80
81    /// Build this into an embed.
82    #[allow(clippy::missing_const_for_fn)]
83    #[must_use = "should be used as part of something like a message"]
84    pub fn build(self) -> Embed {
85        self.0
86    }
87
88    /// Ensure the embed is valid.
89    ///
90    /// # Errors
91    ///
92    /// Refer to the documentation of [`twilight_validate::embed::embed`] for
93    /// possible errors.
94    pub fn validate(self) -> Result<Self, EmbedValidationError> {
95        #[allow(clippy::question_mark)]
96        if let Err(source) = validate_embed(&self.0) {
97            return Err(source);
98        }
99
100        Ok(self)
101    }
102
103    /// Set the author.
104    ///
105    /// # Examples
106    ///
107    /// Create an embed author:
108    ///
109    /// ```
110    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
111    /// use twilight_util::builder::embed::{EmbedAuthorBuilder, EmbedBuilder};
112    ///
113    /// let author = EmbedAuthorBuilder::new("Twilight")
114    ///     .url("https://github.com/twilight-rs/twilight")
115    ///     .build();
116    ///
117    /// let embed = EmbedBuilder::new().author(author).validate()?.build();
118    /// # Ok(()) }
119    /// ```
120    pub fn author(mut self, author: impl Into<EmbedAuthor>) -> Self {
121        self.0.author = Some(author.into());
122
123        self
124    }
125
126    /// Set the color.
127    ///
128    /// This must be a valid hexadecimal RGB value. Refer to
129    /// [`COLOR_MAXIMUM`] for the maximum acceptable value.
130    ///
131    /// # Examples
132    ///
133    /// Set the color of an embed to `0xfd69b3`:
134    ///
135    /// ```
136    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
137    /// use twilight_util::builder::embed::EmbedBuilder;
138    ///
139    /// let embed = EmbedBuilder::new()
140    ///     .color(0xfd_69_b3)
141    ///     .description("a description")
142    ///     .validate()?
143    ///     .build();
144    /// # Ok(()) }
145    /// ```
146    ///
147    /// [`COLOR_MAXIMUM`]: twilight_validate::embed::COLOR_MAXIMUM
148    pub const fn color(mut self, color: u32) -> Self {
149        self.0.color = Some(color);
150
151        self
152    }
153
154    /// Set the description.
155    ///
156    /// Refer to [`DESCRIPTION_LENGTH`] for the maximum number of UTF-16 code
157    /// points that can be in a description.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
163    /// use twilight_util::builder::embed::EmbedBuilder;
164    ///
165    /// let embed = EmbedBuilder::new()
166    ///     .description("this is an embed")
167    ///     .validate()?
168    ///     .build();
169    /// # Ok(()) }
170    /// ```
171    ///
172    /// [`DESCRIPTION_LENGTH`]: twilight_validate::embed::DESCRIPTION_LENGTH
173    pub fn description(mut self, description: impl Into<String>) -> Self {
174        self.0.description = Some(description.into());
175
176        self
177    }
178
179    /// Add a field to the embed.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
185    /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFieldBuilder};
186    ///
187    /// let embed = EmbedBuilder::new()
188    ///     .description("this is an embed")
189    ///     .field(EmbedFieldBuilder::new("a field", "and its value"))
190    ///     .validate()?
191    ///     .build();
192    /// # Ok(()) }
193    /// ```
194    pub fn field(mut self, field: impl Into<EmbedField>) -> Self {
195        self.0.fields.push(field.into());
196
197        self
198    }
199
200    /// Set the footer of the embed.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
206    /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder};
207    ///
208    /// let embed = EmbedBuilder::new()
209    ///     .description("this is an embed")
210    ///     .footer(EmbedFooterBuilder::new("a footer"))
211    ///     .validate()?
212    ///     .build();
213    /// # Ok(()) }
214    /// ```
215    pub fn footer(mut self, footer: impl Into<EmbedFooter>) -> Self {
216        self.0.footer = Some(footer.into());
217
218        self
219    }
220
221    /// Set the image.
222    ///
223    /// # Examples
224    ///
225    /// Set the image source to a URL:
226    ///
227    /// ```
228    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
229    /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder, ImageSource};
230    ///
231    /// let source =
232    ///     ImageSource::url("https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png")?;
233    /// let embed = EmbedBuilder::new()
234    ///     .footer(EmbedFooterBuilder::new("twilight"))
235    ///     .image(source)
236    ///     .validate()?
237    ///     .build();
238    /// # Ok(()) }
239    /// ```
240    #[allow(clippy::missing_const_for_fn)]
241    pub fn image(mut self, image_source: ImageSource) -> Self {
242        self.0.image = Some(EmbedImage {
243            height: None,
244            proxy_url: None,
245            url: image_source.0,
246            width: None,
247        });
248
249        self
250    }
251
252    /// Add a thumbnail.
253    ///
254    /// # Examples
255    ///
256    /// Set the thumbnail to an image attachment with the filename
257    /// `"twilight.png"`:
258    ///
259    /// ```
260    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
261    /// use twilight_util::builder::embed::{EmbedBuilder, ImageSource};
262    ///
263    /// let embed = EmbedBuilder::new()
264    ///     .description("a picture of twilight")
265    ///     .thumbnail(ImageSource::attachment("twilight.png")?)
266    ///     .validate()?
267    ///     .build();
268    /// # Ok(()) }
269    /// ```
270    #[allow(clippy::missing_const_for_fn)]
271    pub fn thumbnail(mut self, image_source: ImageSource) -> Self {
272        self.0.thumbnail = Some(EmbedThumbnail {
273            height: None,
274            proxy_url: None,
275            url: image_source.0,
276            width: None,
277        });
278
279        self
280    }
281
282    /// Set the ISO 8601 timestamp.
283    pub const fn timestamp(mut self, timestamp: Timestamp) -> Self {
284        self.0.timestamp = Some(timestamp);
285
286        self
287    }
288
289    /// Set the title.
290    ///
291    /// Refer to [`TITLE_LENGTH`] for the maximum number of UTF-16 code points
292    /// that can be in a title.
293    ///
294    /// # Examples
295    ///
296    /// Set the title to "twilight":
297    ///
298    /// ```
299    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
300    /// use twilight_util::builder::embed::EmbedBuilder;
301    ///
302    /// let embed = EmbedBuilder::new()
303    ///     .title("twilight")
304    ///     .url("https://github.com/twilight-rs/twilight")
305    ///     .validate()?
306    ///     .build();
307    /// # Ok(()) }
308    /// ```
309    ///
310    /// [`TITLE_LENGTH`]: twilight_validate::embed::TITLE_LENGTH
311    pub fn title(mut self, title: impl Into<String>) -> Self {
312        self.0.title = Some(title.into());
313
314        self
315    }
316
317    /// Set the URL.
318    ///
319    /// # Examples
320    ///
321    /// Set the URL to [twilight's repository]:
322    ///
323    /// ```
324    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
325    /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder};
326    ///
327    /// let embed = EmbedBuilder::new()
328    ///     .description("twilight's repository")
329    ///     .url("https://github.com/twilight-rs/twilight")
330    ///     .validate()?
331    ///     .build();
332    /// # Ok(()) }
333    /// ```
334    ///
335    /// [twilight's repository]: https://github.com/twilight-rs/twilight
336    pub fn url(mut self, url: impl Into<String>) -> Self {
337        self.0.url = Some(url.into());
338
339        self
340    }
341}
342
343impl Default for EmbedBuilder {
344    /// Create an embed builder with a default embed.
345    ///
346    /// All embeds have a "rich" type.
347    fn default() -> Self {
348        Self::new()
349    }
350}
351
352impl From<Embed> for EmbedBuilder {
353    fn from(value: Embed) -> Self {
354        Self(Embed {
355            kind: "rich".to_owned(),
356            ..value
357        })
358    }
359}
360
361impl TryFrom<EmbedBuilder> for Embed {
362    type Error = EmbedValidationError;
363
364    /// Convert an embed builder into an embed, validating its contents.
365    ///
366    /// This is equivalent to calling [`EmbedBuilder::validate`], then
367    /// [`EmbedBuilder::build`].
368    fn try_from(builder: EmbedBuilder) -> Result<Self, Self::Error> {
369        Ok(builder.validate()?.build())
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use static_assertions::assert_impl_all;
377    use std::fmt::Debug;
378
379    assert_impl_all!(EmbedBuilder: Clone, Debug, Eq, PartialEq, Send, Sync);
380    assert_impl_all!(Embed: TryFrom<EmbedBuilder>);
381
382    #[test]
383    fn builder() {
384        let footer_image = ImageSource::url(
385            "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png",
386        )
387        .unwrap();
388        let timestamp = Timestamp::from_secs(1_580_608_922).expect("non zero");
389
390        let embed = EmbedBuilder::new()
391            .color(0x00_43_ff)
392            .description("Description")
393            .timestamp(timestamp)
394            .footer(EmbedFooterBuilder::new("Warn").icon_url(footer_image))
395            .field(EmbedFieldBuilder::new("name", "title").inline())
396            .build();
397
398        let expected = Embed {
399            author: None,
400            color: Some(0x00_43_ff),
401            description: Some("Description".to_string()),
402            fields: [EmbedField {
403                inline: true,
404                name: "name".to_string(),
405                value: "title".to_string(),
406            }]
407            .to_vec(),
408            footer: Some(EmbedFooter {
409                icon_url: Some(
410                    "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png"
411                        .to_string(),
412                ),
413                proxy_icon_url: None,
414                text: "Warn".to_string(),
415            }),
416            image: None,
417            kind: "rich".to_string(),
418            provider: None,
419            thumbnail: None,
420            timestamp: Some(timestamp),
421            title: None,
422            url: None,
423            video: None,
424        };
425
426        assert_eq!(embed, expected);
427    }
428}