Skip to main content

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