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            flags: None,
70            footer: None,
71            image: None,
72            kind: "rich".to_owned(),
73            provider: None,
74            thumbnail: None,
75            timestamp: None,
76            title: None,
77            url: None,
78            video: None,
79        })
80    }
81
82    /// Build this into an embed.
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    pub fn image(mut self, image_source: ImageSource) -> Self {
241        self.0.image = Some(EmbedImage {
242            height: None,
243            proxy_url: None,
244            url: image_source.0,
245            width: None,
246            content_type: None,
247            placeholder: None,
248            placeholder_version: None,
249            flags: None,
250        });
251
252        self
253    }
254
255    /// Add a thumbnail.
256    ///
257    /// # Examples
258    ///
259    /// Set the thumbnail to an image attachment with the filename
260    /// `"twilight.png"`:
261    ///
262    /// ```
263    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
264    /// use twilight_util::builder::embed::{EmbedBuilder, ImageSource};
265    ///
266    /// let embed = EmbedBuilder::new()
267    ///     .description("a picture of twilight")
268    ///     .thumbnail(ImageSource::attachment("twilight.png")?)
269    ///     .validate()?
270    ///     .build();
271    /// # Ok(()) }
272    /// ```
273    pub fn thumbnail(mut self, image_source: ImageSource) -> Self {
274        self.0.thumbnail = Some(EmbedThumbnail {
275            height: None,
276            proxy_url: None,
277            url: image_source.0,
278            width: None,
279        });
280
281        self
282    }
283
284    /// Set the ISO 8601 timestamp.
285    pub const fn timestamp(mut self, timestamp: Timestamp) -> Self {
286        self.0.timestamp = Some(timestamp);
287
288        self
289    }
290
291    /// Set the title.
292    ///
293    /// Refer to [`TITLE_LENGTH`] for the maximum number of UTF-16 code points
294    /// that can be in a title.
295    ///
296    /// # Examples
297    ///
298    /// Set the title to "twilight":
299    ///
300    /// ```
301    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
302    /// use twilight_util::builder::embed::EmbedBuilder;
303    ///
304    /// let embed = EmbedBuilder::new()
305    ///     .title("twilight")
306    ///     .url("https://github.com/twilight-rs/twilight")
307    ///     .validate()?
308    ///     .build();
309    /// # Ok(()) }
310    /// ```
311    ///
312    /// [`TITLE_LENGTH`]: twilight_validate::embed::TITLE_LENGTH
313    pub fn title(mut self, title: impl Into<String>) -> Self {
314        self.0.title = Some(title.into());
315
316        self
317    }
318
319    /// Set the URL.
320    ///
321    /// # Examples
322    ///
323    /// Set the URL to [twilight's repository]:
324    ///
325    /// ```
326    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
327    /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder};
328    ///
329    /// let embed = EmbedBuilder::new()
330    ///     .description("twilight's repository")
331    ///     .url("https://github.com/twilight-rs/twilight")
332    ///     .validate()?
333    ///     .build();
334    /// # Ok(()) }
335    /// ```
336    ///
337    /// [twilight's repository]: https://github.com/twilight-rs/twilight
338    pub fn url(mut self, url: impl Into<String>) -> Self {
339        self.0.url = Some(url.into());
340
341        self
342    }
343}
344
345impl Default for EmbedBuilder {
346    /// Create an embed builder with a default embed.
347    ///
348    /// All embeds have a "rich" type.
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354impl From<Embed> for EmbedBuilder {
355    fn from(value: Embed) -> Self {
356        Self(Embed {
357            kind: "rich".to_owned(),
358            ..value
359        })
360    }
361}
362
363impl TryFrom<EmbedBuilder> for Embed {
364    type Error = EmbedValidationError;
365
366    /// Convert an embed builder into an embed, validating its contents.
367    ///
368    /// This is equivalent to calling [`EmbedBuilder::validate`], then
369    /// [`EmbedBuilder::build`].
370    fn try_from(builder: EmbedBuilder) -> Result<Self, Self::Error> {
371        Ok(builder.validate()?.build())
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use static_assertions::assert_impl_all;
379    use std::fmt::Debug;
380
381    assert_impl_all!(EmbedBuilder: Clone, Debug, Eq, PartialEq, Send, Sync);
382    assert_impl_all!(Embed: TryFrom<EmbedBuilder>);
383
384    #[test]
385    fn builder() {
386        let footer_image = ImageSource::url(
387            "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png",
388        )
389        .unwrap();
390        let timestamp = Timestamp::from_secs(1_580_608_922).expect("non zero");
391
392        let embed = EmbedBuilder::new()
393            .color(0x00_43_ff)
394            .description("Description")
395            .timestamp(timestamp)
396            .footer(EmbedFooterBuilder::new("Warn").icon_url(footer_image))
397            .field(EmbedFieldBuilder::new("name", "title").inline())
398            .build();
399
400        let expected = Embed {
401            author: None,
402            color: Some(0x00_43_ff),
403            description: Some("Description".to_string()),
404            fields: [EmbedField {
405                inline: true,
406                name: "name".to_string(),
407                value: "title".to_string(),
408            }]
409            .to_vec(),
410            flags: None,
411            footer: Some(EmbedFooter {
412                icon_url: Some(
413                    "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png"
414                        .to_string(),
415                ),
416                proxy_icon_url: None,
417                text: "Warn".to_string(),
418            }),
419            image: None,
420            kind: "rich".to_string(),
421            provider: None,
422            thumbnail: None,
423            timestamp: Some(timestamp),
424            title: None,
425            url: None,
426            video: None,
427        };
428
429        assert_eq!(embed, expected);
430    }
431}