twilight_embed_builder/
builder.rs

1//! Create embeds.
2
3use super::image_source::ImageSource;
4use std::{
5    error::Error,
6    fmt::{Display, Formatter, Result as FmtResult},
7    mem,
8};
9use twilight_model::{
10    channel::embed::{Embed, EmbedAuthor, EmbedField, EmbedFooter, EmbedImage, EmbedThumbnail},
11    util::Timestamp,
12};
13
14/// Error building an embed.
15///
16/// This is returned from [`EmbedBuilder::build`].
17#[derive(Debug)]
18pub struct EmbedError {
19    kind: EmbedErrorType,
20}
21
22impl EmbedError {
23    /// Immutable reference to the type of error that occurred.
24    #[must_use = "retrieving the type has no effect if left unused"]
25    pub const fn kind(&self) -> &EmbedErrorType {
26        &self.kind
27    }
28
29    /// Consume the error, returning the source error if there is any.
30    #[allow(clippy::unused_self)]
31    #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
32    pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
33        None
34    }
35
36    /// Consume the error, returning the owned error type and the source error.
37    #[must_use = "consuming the error into its parts has no effect if left unused"]
38    pub fn into_parts(self) -> (EmbedErrorType, Option<Box<dyn Error + Send + Sync>>) {
39        (self.kind, None)
40    }
41}
42
43impl Display for EmbedError {
44    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
45        match &self.kind {
46            EmbedErrorType::AuthorNameEmpty { .. } => f.write_str("the author name is empty"),
47            EmbedErrorType::AuthorNameTooLong { .. } => f.write_str("the author name is too long"),
48            EmbedErrorType::ColorNotRgb { color } => {
49                f.write_str("the color ")?;
50                Display::fmt(color, f)?;
51
52                f.write_str(" is invalid")
53            }
54            EmbedErrorType::ColorZero => {
55                f.write_str("the given color value is 0, which is not acceptable")
56            }
57            EmbedErrorType::DescriptionEmpty { .. } => f.write_str("the description is empty"),
58            EmbedErrorType::DescriptionTooLong { .. } => f.write_str("the description is too long"),
59            EmbedErrorType::FieldNameEmpty { .. } => f.write_str("the field name is empty"),
60            EmbedErrorType::FieldNameTooLong { .. } => f.write_str("the field name is too long"),
61            EmbedErrorType::FieldValueEmpty { .. } => f.write_str("the field value is empty"),
62            EmbedErrorType::FieldValueTooLong { .. } => f.write_str("the field value is too long"),
63            EmbedErrorType::FooterTextEmpty { .. } => f.write_str("the footer text is empty"),
64            EmbedErrorType::FooterTextTooLong { .. } => f.write_str("the footer text is too long"),
65            EmbedErrorType::TitleEmpty { .. } => f.write_str("the title is empty"),
66            EmbedErrorType::TitleTooLong { .. } => f.write_str("the title is too long"),
67            EmbedErrorType::TotalContentTooLarge { .. } => {
68                f.write_str("the total content of the embed is too large")
69            }
70            EmbedErrorType::TooManyFields { .. } => {
71                f.write_str("more than 25 fields were provided")
72            }
73        }
74    }
75}
76
77impl Error for EmbedError {}
78
79/// Type of [`EmbedError`] that occurred.
80#[derive(Debug)]
81#[non_exhaustive]
82pub enum EmbedErrorType {
83    /// Name is empty.
84    AuthorNameEmpty {
85        /// Provided name. Although empty, the same owned allocation is
86        /// included.
87        name: String,
88    },
89    /// Name is longer than 256 UTF-16 code points.
90    AuthorNameTooLong {
91        /// Provided name.
92        name: String,
93    },
94    /// Color was larger than a valid RGB hexadecimal value.
95    ColorNotRgb {
96        /// Provided color hex value.
97        color: u32,
98    },
99    /// Color was 0. The value would be thrown out by Discord and is equivalent
100    /// to null.
101    ColorZero,
102    /// Description is empty.
103    DescriptionEmpty {
104        /// Provided description. Although empty, the same owned allocation is
105        /// included.
106        description: String,
107    },
108    /// Description is longer than 4096 UTF-16 code points.
109    DescriptionTooLong {
110        /// Provided description.
111        description: String,
112    },
113    /// Name is empty.
114    FieldNameEmpty {
115        /// Provided name. Although empty, the same owned allocation is
116        /// included.
117        name: String,
118        /// Provided value.
119        value: String,
120    },
121    /// Name is longer than 256 UTF-16 code points.
122    FieldNameTooLong {
123        /// Provided name.
124        name: String,
125        /// Provided value.
126        value: String,
127    },
128    /// Value is empty.
129    FieldValueEmpty {
130        /// Provided name.
131        name: String,
132        /// Provided value. Although empty, the same owned allocation is
133        /// included.
134        value: String,
135    },
136    /// Value is longer than 1024 UTF-16 code points.
137    FieldValueTooLong {
138        /// Provided name.
139        name: String,
140        /// Provided value.
141        value: String,
142    },
143    /// Footer text is empty.
144    FooterTextEmpty {
145        /// Provided text. Although empty, the same owned allocation is
146        /// included.
147        text: String,
148    },
149    /// Footer text is longer than 2048 UTF-16 code points.
150    FooterTextTooLong {
151        /// Provided text.
152        text: String,
153    },
154    /// Title is empty.
155    TitleEmpty {
156        /// Provided title. Although empty, the same owned allocation is
157        /// included.
158        title: String,
159    },
160    /// Title is longer than 256 UTF-16 code points.
161    TitleTooLong {
162        /// Provided title.
163        title: String,
164    },
165    /// The total content of the embed is too large.
166    ///
167    /// Refer to [`EmbedBuilder::EMBED_LENGTH_LIMIT`] for more information about
168    /// what goes into this limit.
169    TotalContentTooLarge {
170        /// The total length of the embed.
171        length: usize,
172    },
173    /// Too many fields were provided.
174    ///
175    /// Refer to [`EmbedBuilder::EMBED_FIELD_LIMIT`] for more information about
176    /// what the limit is.
177    TooManyFields {
178        /// The provided fields.
179        fields: Vec<EmbedField>,
180    },
181}
182
183/// Create an embed with a builder.
184///
185/// # Examples
186///
187/// Refer to the [crate-level documentation] for examples.
188///
189/// [crate-level documentation]: crate
190#[allow(clippy::module_name_repetitions)]
191#[derive(Clone, Debug, Eq, PartialEq)]
192#[must_use = "must be built into an embed"]
193pub struct EmbedBuilder(Embed);
194
195impl EmbedBuilder {
196    /// The maximum number of UTF-16 code points that can be in an author name.
197    pub const AUTHOR_NAME_LENGTH_LIMIT: usize = 256;
198
199    /// The maximum accepted color value.
200    pub const COLOR_MAXIMUM: u32 = 0xff_ff_ff;
201
202    /// The maximum number of UTF-16 code points that can be in a description.
203    pub const DESCRIPTION_LENGTH_LIMIT: usize = 4096;
204
205    /// The maximum number of fields that can be in an embed.
206    pub const EMBED_FIELD_LIMIT: usize = 25;
207
208    /// The maximum total textual length of the embed in UTF-16 code points.
209    ///
210    /// This combines the text of the author name, description, footer text,
211    /// field names and values, and title.
212    pub const EMBED_LENGTH_LIMIT: usize = 6000;
213
214    /// The maximum number of UTF-16 code points that can be in a field name.
215    pub const FIELD_NAME_LENGTH_LIMIT: usize = 256;
216
217    /// The maximum number of UTF-16 code points that can be in a field value.
218    pub const FIELD_VALUE_LENGTH_LIMIT: usize = 1024;
219
220    /// The maximum number of UTF-16 code points that can be in a footer's text.
221    pub const FOOTER_TEXT_LENGTH_LIMIT: usize = 2048;
222
223    /// The maximum number of UTF-16 code points that can be in a title.
224    pub const TITLE_LENGTH_LIMIT: usize = 256;
225
226    /// Create a new default embed builder.
227    ///
228    /// See the [crate-level documentation] for examples and additional
229    /// information.
230    ///
231    /// This is equivalent to the [default implementation].
232    ///
233    /// [crate-level documentation]: crate
234    /// [default implementation]: Self::default
235    pub const fn new() -> Self {
236        EmbedBuilder(Embed {
237            author: None,
238            color: None,
239            description: None,
240            fields: Vec::new(),
241            footer: None,
242            image: None,
243            kind: String::new(),
244            provider: None,
245            thumbnail: None,
246            timestamp: None,
247            title: None,
248            url: None,
249            video: None,
250        })
251    }
252
253    /// Build this into an embed.
254    ///
255    /// # Errors
256    ///
257    /// Returns an [`EmbedErrorType::AuthorNameEmpty`] error type if the
258    /// provided name is empty.
259    ///
260    /// Returns an [`EmbedErrorType::AuthorNameTooLong`] error type if the
261    /// provided name is longer than [`AUTHOR_NAME_LENGTH_LIMIT`].
262    ///
263    /// Returns an [`EmbedErrorType::ColorNotRgb`] error type if the provided
264    /// color is not a valid RGB integer. Refer to [`COLOR_MAXIMUM`] to know
265    /// what the maximum accepted value is.
266    ///
267    /// Returns an [`EmbedErrorType::ColorZero`] error type if the provided
268    /// color is 0, which is not an acceptable value.
269    ///
270    /// Returns an [`EmbedErrorType::DescriptionEmpty`] error type if a provided
271    /// description is empty.
272    ///
273    /// Returns an [`EmbedErrorType::DescriptionTooLong`] error type if a
274    /// provided description is longer than [`DESCRIPTION_LENGTH_LIMIT`].
275    ///
276    /// Returns an [`EmbedErrorType::FieldNameEmpty`] error type if a provided
277    /// field name is empty.
278    ///
279    /// Returns an [`EmbedErrorType::FieldNameTooLong`] error type if a provided
280    /// field name is longer than [`FIELD_NAME_LENGTH_LIMIT`].
281    ///
282    /// Returns an [`EmbedErrorType::FieldValueEmpty`] error type if a provided
283    /// field value is empty.
284    ///
285    /// Returns an [`EmbedErrorType::FieldValueTooLong`] error type if a
286    /// provided field value is longer than [`FIELD_VALUE_LENGTH_LIMIT`].
287    ///
288    /// Returns an [`EmbedErrorType::FooterTextEmpty`] error type if the
289    /// provided text is empty.
290    ///
291    /// Returns an [`EmbedErrorType::FooterTextTooLong`] error type if the
292    /// provided text is longer than the limit defined at [`FOOTER_TEXT_LENGTH_LIMIT`].
293    ///
294    /// Returns an [`EmbedErrorType::TitleEmpty`] error type if the provided
295    /// title is empty.
296    ///
297    /// Returns an [`EmbedErrorType::TitleTooLong`] error type if the provided
298    /// text is longer than the limit defined at [`TITLE_LENGTH_LIMIT`].
299    ///
300    /// Returns an [`EmbedErrorType::TooManyFields`] error type if there are too
301    /// many fields in the embed. Refer to [`EMBED_FIELD_LIMIT`] for the limit
302    /// value.
303    ///
304    /// Returns an [`EmbedErrorType::TotalContentTooLarge`] error type if the
305    /// textual content of the embed is too large. Refer to
306    /// [`EMBED_LENGTH_LIMIT`] for the limit value and what counts towards it.
307    ///
308    /// [`AUTHOR_NAME_LENGTH_LIMIT`]: Self::AUTHOR_NAME_LENGTH_LIMIT
309    /// [`COLOR_MAXIMUM`]: Self::COLOR_MAXIMUM
310    /// [`DESCRIPTION_LENGTH_LIMIT`]: Self::DESCRIPTION_LENGTH_LIMIT
311    /// [`EMBED_FIELD_LIMIT`]: Self::EMBED_FIELD_LIMIT
312    /// [`EMBED_LENGTH_LIMIT`]: Self::EMBED_LENGTH_LIMIT
313    /// [`FIELD_NAME_LENGTH_LIMIT`]: Self::FIELD_NAME_LENGTH_LIMIT
314    /// [`FIELD_VALUE_LENGTH_LIMIT`]: Self::FIELD_VALUE_LENGTH_LIMIT
315    /// [`FOOTER_TEXT_LENGTH_LIMIT`]: Self::FOOTER_TEXT_LENGTH_LIMIT
316    /// [`TITLE_LENGTH_LIMIT`]: Self::TITLE_LENGTH_LIMIT
317    #[allow(clippy::too_many_lines)]
318    #[must_use = "should be used as part of something like a message"]
319    pub fn build(mut self) -> Result<Embed, EmbedError> {
320        if self.0.fields.len() > Self::EMBED_FIELD_LIMIT {
321            return Err(EmbedError {
322                kind: EmbedErrorType::TooManyFields {
323                    fields: self.0.fields,
324                },
325            });
326        }
327
328        if let Some(color) = self.0.color {
329            if color == 0 {
330                return Err(EmbedError {
331                    kind: EmbedErrorType::ColorZero,
332                });
333            }
334
335            if color > Self::COLOR_MAXIMUM {
336                return Err(EmbedError {
337                    kind: EmbedErrorType::ColorNotRgb { color },
338                });
339            }
340        }
341
342        let mut total = 0;
343
344        if let Some(author) = self.0.author.take() {
345            if author.name.is_empty() {
346                return Err(EmbedError {
347                    kind: EmbedErrorType::AuthorNameEmpty { name: author.name },
348                });
349            }
350
351            if author.name.chars().count() > Self::AUTHOR_NAME_LENGTH_LIMIT {
352                return Err(EmbedError {
353                    kind: EmbedErrorType::AuthorNameTooLong { name: author.name },
354                });
355            }
356
357            total += author.name.chars().count();
358
359            self.0.author.replace(author);
360        }
361
362        if let Some(description) = self.0.description.take() {
363            if description.is_empty() {
364                return Err(EmbedError {
365                    kind: EmbedErrorType::DescriptionEmpty { description },
366                });
367            }
368
369            if description.chars().count() > Self::DESCRIPTION_LENGTH_LIMIT {
370                return Err(EmbedError {
371                    kind: EmbedErrorType::DescriptionTooLong { description },
372                });
373            }
374
375            total += description.chars().count();
376            self.0.description.replace(description);
377        }
378
379        if let Some(footer) = self.0.footer.take() {
380            if footer.text.is_empty() {
381                return Err(EmbedError {
382                    kind: EmbedErrorType::FooterTextEmpty { text: footer.text },
383                });
384            }
385
386            if footer.text.chars().count() > Self::FOOTER_TEXT_LENGTH_LIMIT {
387                return Err(EmbedError {
388                    kind: EmbedErrorType::FooterTextTooLong { text: footer.text },
389                });
390            }
391
392            total += footer.text.chars().count();
393            self.0.footer.replace(footer);
394        }
395
396        {
397            let field_count = self.0.fields.len();
398            let fields = mem::replace(&mut self.0.fields, Vec::with_capacity(field_count));
399
400            for field in fields {
401                if field.name.is_empty() {
402                    return Err(EmbedError {
403                        kind: EmbedErrorType::FieldNameEmpty {
404                            name: field.name,
405                            value: field.value,
406                        },
407                    });
408                }
409
410                if field.name.chars().count() > Self::FIELD_NAME_LENGTH_LIMIT {
411                    return Err(EmbedError {
412                        kind: EmbedErrorType::FieldNameTooLong {
413                            name: field.name,
414                            value: field.value,
415                        },
416                    });
417                }
418
419                if field.value.is_empty() {
420                    return Err(EmbedError {
421                        kind: EmbedErrorType::FieldValueEmpty {
422                            name: field.name,
423                            value: field.value,
424                        },
425                    });
426                }
427
428                if field.value.chars().count() > Self::FIELD_VALUE_LENGTH_LIMIT {
429                    return Err(EmbedError {
430                        kind: EmbedErrorType::FieldValueTooLong {
431                            name: field.name,
432                            value: field.value,
433                        },
434                    });
435                }
436
437                total += field.name.chars().count() + field.value.chars().count();
438                self.0.fields.push(field);
439            }
440        }
441
442        if let Some(title) = self.0.title.take() {
443            if title.is_empty() {
444                return Err(EmbedError {
445                    kind: EmbedErrorType::TitleEmpty { title },
446                });
447            }
448
449            if title.chars().count() > Self::TITLE_LENGTH_LIMIT {
450                return Err(EmbedError {
451                    kind: EmbedErrorType::TitleTooLong { title },
452                });
453            }
454
455            total += title.chars().count();
456            self.0.title.replace(title);
457        }
458
459        if total > Self::EMBED_LENGTH_LIMIT {
460            return Err(EmbedError {
461                kind: EmbedErrorType::TotalContentTooLarge { length: total },
462            });
463        }
464
465        if self.0.kind.is_empty() {
466            self.0.kind = "rich".to_string();
467        }
468
469        Ok(self.0)
470    }
471
472    /// Set the author.
473    ///
474    /// # Examples
475    ///
476    /// Create an embed author:
477    ///
478    /// ```
479    /// use twilight_embed_builder::{EmbedAuthorBuilder, EmbedBuilder};
480    ///
481    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
482    /// let author = EmbedAuthorBuilder::new("Twilight".into())
483    ///     .url("https://github.com/twilight-rs/twilight")
484    ///     .build();
485    ///
486    /// let embed = EmbedBuilder::new().author(author).build()?;
487    /// # Ok(()) }
488    /// ```
489    pub fn author(self, author: impl Into<EmbedAuthor>) -> Self {
490        self._author(author.into())
491    }
492
493    fn _author(mut self, author: EmbedAuthor) -> Self {
494        self.0.author.replace(author);
495
496        self
497    }
498
499    /// Set the color.
500    ///
501    /// This must be a valid hexadecimal RGB value. `0x000000` is not an
502    /// acceptable value as it would be thrown out by Discord. Refer to
503    /// [`COLOR_MAXIMUM`] for the maximum acceptable value.
504    ///
505    /// # Examples
506    ///
507    /// Set the color of an embed to `0xfd69b3`:
508    ///
509    /// ```
510    /// use twilight_embed_builder::EmbedBuilder;
511    ///
512    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
513    /// let embed = EmbedBuilder::new()
514    ///     .color(0xfd_69_b3)
515    ///     .description("a description")
516    ///     .build()?;
517    /// # Ok(()) }
518    /// ```
519    ///
520    /// [`COLOR_MAXIMUM`]: Self::COLOR_MAXIMUM
521    pub fn color(mut self, color: u32) -> Self {
522        self.0.color.replace(color);
523
524        self
525    }
526
527    /// Set the description.
528    ///
529    /// Refer to [`DESCRIPTION_LENGTH_LIMIT`] for the maximum number of UTF-16
530    /// code points that can be in a description.
531    ///
532    /// # Examples
533    ///
534    /// ```
535    /// use twilight_embed_builder::EmbedBuilder;
536    ///
537    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
538    /// let embed = EmbedBuilder::new().description("this is an embed").build()?;
539    /// # Ok(()) }
540    /// ```
541    ///
542    /// [`DESCRIPTION_LENGTH_LIMIT`]: Self::DESCRIPTION_LENGTH_LIMIT
543    pub fn description(self, description: impl Into<String>) -> Self {
544        self._description(description.into())
545    }
546
547    fn _description(mut self, description: String) -> Self {
548        self.0.description.replace(description);
549
550        self
551    }
552
553    /// Add a field to the embed.
554    ///
555    /// # Examples
556    ///
557    /// ```
558    /// use twilight_embed_builder::{EmbedBuilder, EmbedFieldBuilder};
559    ///
560    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
561    /// let embed = EmbedBuilder::new()
562    ///     .description("this is an embed")
563    ///     .field(EmbedFieldBuilder::new("a field", "and its value"))
564    ///     .build()?;
565    /// # Ok(()) }
566    /// ```
567    pub fn field(self, field: impl Into<EmbedField>) -> Self {
568        self._field(field.into())
569    }
570
571    fn _field(mut self, field: EmbedField) -> Self {
572        self.0.fields.push(field);
573
574        self
575    }
576
577    /// Set the footer of the embed.
578    ///
579    /// # Examples
580    ///
581    /// ```
582    /// use twilight_embed_builder::{EmbedBuilder, EmbedFooterBuilder};
583    ///
584    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
585    /// let embed = EmbedBuilder::new()
586    ///     .description("this is an embed")
587    ///     .footer(EmbedFooterBuilder::new("a footer"))
588    ///     .build()?;
589    /// # Ok(()) }
590    /// ```
591    pub fn footer(self, footer: impl Into<EmbedFooter>) -> Self {
592        self._footer(footer.into())
593    }
594
595    fn _footer(mut self, footer: EmbedFooter) -> Self {
596        self.0.footer.replace(footer);
597
598        self
599    }
600
601    /// Set the image.
602    ///
603    /// # Examples
604    ///
605    /// Set the image source to a URL:
606    ///
607    /// ```
608    /// use twilight_embed_builder::{EmbedBuilder, EmbedFooterBuilder, ImageSource};
609    ///
610    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
611    /// let source = ImageSource::url("https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png")?;
612    /// let embed = EmbedBuilder::new()
613    ///     .footer(EmbedFooterBuilder::new("twilight"))
614    ///     .image(source)
615    ///     .build()?;
616    /// # Ok(()) }
617    /// ```
618    pub fn image(mut self, image_source: ImageSource) -> Self {
619        self.0.image.replace(EmbedImage {
620            height: None,
621            proxy_url: None,
622            url: image_source.0,
623            width: None,
624        });
625
626        self
627    }
628
629    /// Add a thumbnail.
630    ///
631    /// # Examples
632    ///
633    /// Set the thumbnail to an image attachment with the filename
634    /// `"twilight.png"`:
635    ///
636    /// ```
637    /// use twilight_embed_builder::{EmbedBuilder, ImageSource};
638    ///
639    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
640    /// let embed = EmbedBuilder::new()
641    ///     .description("a picture of twilight")
642    ///     .thumbnail(ImageSource::attachment("twilight.png")?)
643    ///     .build()?;
644    /// # Ok(()) }
645    /// ```
646    pub fn thumbnail(mut self, image_source: ImageSource) -> Self {
647        self.0.thumbnail.replace(EmbedThumbnail {
648            height: None,
649            proxy_url: None,
650            url: image_source.0,
651            width: None,
652        });
653
654        self
655    }
656
657    /// Set the ISO 8601 timestamp.
658    pub const fn timestamp(mut self, timestamp: Timestamp) -> Self {
659        self.0.timestamp = Some(timestamp);
660
661        self
662    }
663
664    /// Set the title.
665    ///
666    /// Refer to [`TITLE_LENGTH_LIMIT`] for the maximum number of UTF-16 code
667    /// points that can be in a title.
668    ///
669    /// # Examples
670    ///
671    /// Set the title to "twilight":
672    ///
673    /// ```
674    /// use twilight_embed_builder::EmbedBuilder;
675    ///
676    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
677    /// let embed = EmbedBuilder::new()
678    ///     .title("twilight")
679    ///     .url("https://github.com/twilight-rs/twilight")
680    ///     .build()?;
681    /// # Ok(()) }
682    /// ```
683    ///
684    /// [`TITLE_LENGTH_LIMIT`]: Self::TITLE_LENGTH_LIMIT
685    pub fn title(self, title: impl Into<String>) -> Self {
686        self._title(title.into())
687    }
688
689    fn _title(mut self, title: String) -> Self {
690        self.0.title.replace(title);
691
692        self
693    }
694
695    /// Set the URL.
696    ///
697    /// # Examples
698    ///
699    /// Set the URL to [twilight's repository]:
700    ///
701    /// ```
702    /// use twilight_embed_builder::{EmbedBuilder, EmbedFooterBuilder};
703    ///
704    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
705    /// let embed = EmbedBuilder::new()
706    ///     .description("twilight's repository")
707    ///     .url("https://github.com/twilight-rs/twilight")
708    ///     .build()?;
709    /// # Ok(()) }
710    /// ```
711    ///
712    /// [twilight's repository]: https://github.com/twilight-rs/twilight
713    pub fn url(self, url: impl Into<String>) -> Self {
714        self._url(url.into())
715    }
716
717    fn _url(mut self, url: String) -> Self {
718        self.0.url.replace(url);
719
720        self
721    }
722}
723
724impl Default for EmbedBuilder {
725    /// Create an embed builder with a default embed.
726    ///
727    /// All embeds have a "rich" type.
728    fn default() -> Self {
729        Self::new()
730    }
731}
732
733impl TryFrom<EmbedBuilder> for Embed {
734    type Error = EmbedError;
735
736    /// Convert an embed builder into an embed.
737    ///
738    /// This is equivalent to calling [`EmbedBuilder::build`].
739    fn try_from(builder: EmbedBuilder) -> Result<Self, Self::Error> {
740        builder.build()
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use super::{EmbedBuilder, EmbedError, EmbedErrorType};
747    use crate::{field::EmbedFieldBuilder, footer::EmbedFooterBuilder, image_source::ImageSource};
748    use static_assertions::{assert_fields, assert_impl_all, const_assert};
749    use std::{error::Error, fmt::Debug};
750    use twilight_model::{
751        channel::embed::{Embed, EmbedField, EmbedFooter},
752        util::Timestamp,
753    };
754
755    assert_impl_all!(EmbedErrorType: Debug, Send, Sync);
756    assert_fields!(EmbedErrorType::AuthorNameEmpty: name);
757    assert_fields!(EmbedErrorType::AuthorNameTooLong: name);
758    assert_fields!(EmbedErrorType::TooManyFields: fields);
759    assert_fields!(EmbedErrorType::ColorNotRgb: color);
760    assert_fields!(EmbedErrorType::DescriptionEmpty: description);
761    assert_fields!(EmbedErrorType::DescriptionTooLong: description);
762    assert_fields!(EmbedErrorType::FooterTextEmpty: text);
763    assert_fields!(EmbedErrorType::FooterTextTooLong: text);
764    assert_fields!(EmbedErrorType::TitleEmpty: title);
765    assert_fields!(EmbedErrorType::TitleTooLong: title);
766    assert_fields!(EmbedErrorType::TotalContentTooLarge: length);
767    assert_fields!(EmbedErrorType::FieldNameEmpty: name, value);
768    assert_fields!(EmbedErrorType::FieldNameTooLong: name, value);
769    assert_fields!(EmbedErrorType::FieldValueEmpty: name, value);
770    assert_fields!(EmbedErrorType::FieldValueTooLong: name, value);
771    assert_impl_all!(EmbedError: Error, Send, Sync);
772    const_assert!(EmbedBuilder::AUTHOR_NAME_LENGTH_LIMIT == 256);
773    const_assert!(EmbedBuilder::COLOR_MAXIMUM == 0xff_ff_ff);
774    const_assert!(EmbedBuilder::DESCRIPTION_LENGTH_LIMIT == 4096);
775    const_assert!(EmbedBuilder::EMBED_FIELD_LIMIT == 25);
776    const_assert!(EmbedBuilder::EMBED_LENGTH_LIMIT == 6000);
777    const_assert!(EmbedBuilder::FIELD_NAME_LENGTH_LIMIT == 256);
778    const_assert!(EmbedBuilder::FIELD_VALUE_LENGTH_LIMIT == 1024);
779    const_assert!(EmbedBuilder::FOOTER_TEXT_LENGTH_LIMIT == 2048);
780    const_assert!(EmbedBuilder::TITLE_LENGTH_LIMIT == 256);
781    assert_impl_all!(EmbedBuilder: Clone, Debug, Eq, PartialEq, Send, Sync);
782    assert_impl_all!(Embed: TryFrom<EmbedBuilder>);
783
784    #[test]
785    fn color_error() {
786        assert!(matches!(
787            EmbedBuilder::new().color(0).build().unwrap_err().kind(),
788            EmbedErrorType::ColorZero
789        ));
790        assert!(matches!(
791            EmbedBuilder::new().color(u32::MAX).build().unwrap_err().kind(),
792            EmbedErrorType::ColorNotRgb { color }
793            if *color == u32::MAX
794        ));
795    }
796
797    #[test]
798    fn description_error() {
799        assert!(matches!(
800            EmbedBuilder::new().description("").build().unwrap_err().kind(),
801            EmbedErrorType::DescriptionEmpty { description }
802            if description.is_empty()
803        ));
804        let description_too_long = EmbedBuilder::DESCRIPTION_LENGTH_LIMIT + 1;
805        assert!(matches!(
806            EmbedBuilder::new().description("a".repeat(description_too_long)).build().unwrap_err().kind(),
807            EmbedErrorType::DescriptionTooLong { description }
808            if description.len() == description_too_long
809        ));
810    }
811
812    #[test]
813    fn title_error() {
814        assert!(matches!(
815            EmbedBuilder::new().title("").build().unwrap_err().kind(),
816            EmbedErrorType::TitleEmpty { title }
817            if title.is_empty()
818        ));
819        let title_too_long = EmbedBuilder::TITLE_LENGTH_LIMIT + 1;
820        assert!(matches!(
821            EmbedBuilder::new().title("a".repeat(title_too_long)).build().unwrap_err().kind(),
822            EmbedErrorType::TitleTooLong { title }
823            if title.len() == title_too_long
824        ));
825    }
826
827    #[test]
828    fn builder() {
829        let footer_image = ImageSource::url(
830            "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png",
831        )
832        .unwrap();
833        let timestamp = Timestamp::from_secs(1_580_608_922).expect("non zero");
834
835        let embed = EmbedBuilder::new()
836            .color(0x00_43_ff)
837            .description("Description")
838            .timestamp(timestamp)
839            .footer(EmbedFooterBuilder::new("Warn").icon_url(footer_image))
840            .field(EmbedFieldBuilder::new("name", "title").inline())
841            .build()
842            .unwrap();
843
844        let expected = Embed {
845            author: None,
846            color: Some(0x00_43_ff),
847            description: Some("Description".to_string()),
848            fields: [EmbedField {
849                inline: true,
850                name: "name".to_string(),
851                value: "title".to_string(),
852            }]
853            .to_vec(),
854            footer: Some(EmbedFooter {
855                icon_url: Some(
856                    "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png"
857                        .to_string(),
858                ),
859                proxy_icon_url: None,
860                text: "Warn".to_string(),
861            }),
862            image: None,
863            kind: "rich".to_string(),
864            provider: None,
865            thumbnail: None,
866            timestamp: Some(timestamp),
867            title: None,
868            url: None,
869            video: None,
870        };
871
872        assert_eq!(embed, expected);
873    }
874}