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}