Skip to main content

twilight_validate/
embed.rs

1//! Constants, error types, and functions for validating [`Embed`]s.
2
3use std::{
4    error::Error,
5    fmt::{Display, Formatter, Result as FmtResult},
6};
7use twilight_model::channel::message::Embed;
8
9/// The maximum embed author name length in codepoints.
10pub const AUTHOR_NAME_LENGTH: usize = 256;
11
12/// The maximum accepted color value.
13pub const COLOR_MAXIMUM: u32 = 0xff_ff_ff;
14
15/// The maximum embed description length in codepoints.
16pub const DESCRIPTION_LENGTH: usize = 4096;
17
18/// The maximum combined embed length in codepoints.
19pub const EMBED_TOTAL_LENGTH: usize = 6000;
20
21/// The maximum number of fields in an embed.
22pub const FIELD_COUNT: usize = 25;
23
24/// The maximum length of an embed field name in codepoints.
25pub const FIELD_NAME_LENGTH: usize = 256;
26
27/// The maximum length of an embed field value in codepoints.
28pub const FIELD_VALUE_LENGTH: usize = 1024;
29
30/// The maximum embed footer length in codepoints.
31pub const FOOTER_TEXT_LENGTH: usize = 2048;
32
33/// The maximum embed title length in codepoints.
34pub const TITLE_LENGTH: usize = 256;
35
36/// An embed is not valid.
37///
38/// Referenced values are from [Discord Docs/Embed Limits].
39///
40/// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
41#[derive(Debug)]
42pub struct EmbedValidationError {
43    /// Type of error that occurred.
44    kind: EmbedValidationErrorType,
45}
46
47impl EmbedValidationError {
48    /// Immutable reference to the type of error that occurred.
49    #[must_use = "retrieving the type has no effect if left unused"]
50    pub const fn kind(&self) -> &EmbedValidationErrorType {
51        &self.kind
52    }
53
54    /// Consume the error, returning the source error if there is any.
55    #[allow(clippy::unused_self)]
56    #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
57    pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
58        None
59    }
60
61    /// Consume the error, returning the owned error type and the source error.
62    #[must_use = "consuming the error into its parts has no effect if left unused"]
63    pub fn into_parts(
64        self,
65    ) -> (
66        EmbedValidationErrorType,
67        Option<Box<dyn Error + Send + Sync>>,
68    ) {
69        (self.kind, None)
70    }
71}
72
73impl Display for EmbedValidationError {
74    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
75        match &self.kind {
76            EmbedValidationErrorType::AuthorNameTooLarge { chars } => {
77                f.write_str("the author name is ")?;
78                Display::fmt(chars, f)?;
79                f.write_str(" characters long, but the max is ")?;
80
81                Display::fmt(&AUTHOR_NAME_LENGTH, f)
82            }
83            EmbedValidationErrorType::ColorNotRgb { color } => {
84                f.write_str("the color is ")?;
85                Display::fmt(color, f)?;
86                f.write_str(", but it must be less than ")?;
87
88                Display::fmt(&COLOR_MAXIMUM, f)
89            }
90            EmbedValidationErrorType::DescriptionTooLarge { chars } => {
91                f.write_str("the description is ")?;
92                Display::fmt(chars, f)?;
93                f.write_str(" characters long, but the max is ")?;
94
95                Display::fmt(&DESCRIPTION_LENGTH, f)
96            }
97            EmbedValidationErrorType::EmbedTooLarge { chars } => {
98                f.write_str("the combined total length of the embed is ")?;
99                Display::fmt(chars, f)?;
100                f.write_str(" characters long, but the max is ")?;
101
102                Display::fmt(&EMBED_TOTAL_LENGTH, f)
103            }
104            EmbedValidationErrorType::FieldNameTooLarge { chars } => {
105                f.write_str("a field name is ")?;
106                Display::fmt(chars, f)?;
107                f.write_str(" characters long, but the max is ")?;
108
109                Display::fmt(&FIELD_NAME_LENGTH, f)
110            }
111            EmbedValidationErrorType::FieldValueTooLarge { chars } => {
112                f.write_str("a field value is ")?;
113                Display::fmt(chars, f)?;
114                f.write_str(" characters long, but the max is ")?;
115
116                Display::fmt(&FIELD_VALUE_LENGTH, f)
117            }
118            EmbedValidationErrorType::FooterTextTooLarge { chars } => {
119                f.write_str("the footer's text is ")?;
120                Display::fmt(chars, f)?;
121                f.write_str(" characters long, but the max is ")?;
122
123                Display::fmt(&FOOTER_TEXT_LENGTH, f)
124            }
125            EmbedValidationErrorType::TitleTooLarge { chars } => {
126                f.write_str("the title's length is ")?;
127                Display::fmt(chars, f)?;
128                f.write_str(" characters long, but the max is ")?;
129
130                Display::fmt(&TITLE_LENGTH, f)
131            }
132            EmbedValidationErrorType::TooManyFields { amount } => {
133                f.write_str("there are ")?;
134                Display::fmt(amount, f)?;
135                f.write_str(" fields, but the maximum amount is ")?;
136
137                Display::fmt(&FIELD_COUNT, f)
138            }
139        }
140    }
141}
142
143impl Error for EmbedValidationError {}
144
145/// Type of [`EmbedValidationError`] that occurred.
146#[derive(Debug)]
147#[non_exhaustive]
148pub enum EmbedValidationErrorType {
149    /// Embed author's name is larger than [`AUTHOR_NAME_LENGTH`].
150    AuthorNameTooLarge {
151        /// Provided number of codepoints.
152        chars: usize,
153    },
154    /// Color is larger than a valid RGB hexadecimal value.
155    ColorNotRgb {
156        /// Provided color hex value.
157        color: u32,
158    },
159    /// Embed description is larger than [`DESCRIPTION_LENGTH`].
160    DescriptionTooLarge {
161        /// Provided number of codepoints.
162        chars: usize,
163    },
164    /// Combined content of all embed fields is larger than
165    /// [`EMBED_TOTAL_LENGTH`].
166    ///
167    /// This includes author name, description, footer, field names and values,
168    /// and title.
169    EmbedTooLarge {
170        /// Provided number of codepoints.
171        chars: usize,
172    },
173    /// A field's name is larger than [`FIELD_NAME_LENGTH`].
174    FieldNameTooLarge {
175        /// Provided number of codepoints.
176        chars: usize,
177    },
178    /// A field's value is larger than [`FIELD_VALUE_LENGTH`].
179    FieldValueTooLarge {
180        /// Provided number of codepoints.
181        chars: usize,
182    },
183    /// Footer text is larger than [`FOOTER_TEXT_LENGTH`].
184    FooterTextTooLarge {
185        /// Provided number of codepoints.
186        chars: usize,
187    },
188    /// Title is larger than [`TITLE_LENGTH`].
189    TitleTooLarge {
190        /// Provided number of codepoints.
191        chars: usize,
192    },
193    /// There are more than [`FIELD_COUNT`] number of fields in the embed.
194    TooManyFields {
195        /// Provided number of fields.
196        amount: usize,
197    },
198}
199
200/// Ensure an embed is correct.
201///
202/// # Errors
203///
204/// Returns an error of type [`AuthorNameTooLarge`] if
205/// the author's name is too large.
206///
207/// Returns an error of type [`ColorNotRgb`] if if the provided color is not a
208/// valid RGB integer. Refer to [`COLOR_MAXIMUM`] to know what the maximum
209/// accepted value is.
210///
211/// Returns an error of type [`DescriptionTooLarge`] if the description is too
212/// large.
213///
214/// Returns an error of type [`EmbedTooLarge`] if the embed in total is too
215/// large.
216///
217/// Returns an error of type [`FieldNameTooLarge`] if
218/// a field's name is too long.
219///
220/// Returns an error of type [`FieldValueTooLarge`] if
221/// a field's value is too long.
222///
223/// Returns an error of type [`FooterTextTooLarge`] if
224/// the footer text is too long.
225///
226/// Returns an error of type [`TitleTooLarge`] if the title is too long.
227///
228/// Returns an error of type [`TooManyFields`] if
229/// there are too many fields.
230///
231/// [`AuthorNameTooLarge`]: EmbedValidationErrorType::AuthorNameTooLarge
232/// [`ColorNotRgb`]: EmbedValidationErrorType::ColorNotRgb
233/// [`DescriptionTooLarge`]: EmbedValidationErrorType::DescriptionTooLarge
234/// [`EmbedTooLarge`]: EmbedValidationErrorType::EmbedTooLarge
235/// [`FieldNameTooLarge`]: EmbedValidationErrorType::FieldNameTooLarge
236/// [`FieldValueTooLarge`]: EmbedValidationErrorType::FieldValueTooLarge
237/// [`FooterTextTooLarge`]: EmbedValidationErrorType::FooterTextTooLarge
238/// [`TitleTooLarge`]: EmbedValidationErrorType::TitleTooLarge
239/// [`TooManyFields`]: EmbedValidationErrorType::TooManyFields
240pub fn embed(embed: &Embed) -> Result<(), EmbedValidationError> {
241    let chars = self::chars(embed);
242
243    if chars > EMBED_TOTAL_LENGTH {
244        return Err(EmbedValidationError {
245            kind: EmbedValidationErrorType::EmbedTooLarge { chars },
246        });
247    }
248
249    if let Some(color) = embed.color
250        && color > COLOR_MAXIMUM
251    {
252        return Err(EmbedValidationError {
253            kind: EmbedValidationErrorType::ColorNotRgb { color },
254        });
255    }
256
257    if let Some(description) = embed.description.as_ref() {
258        let chars = description.chars().count();
259
260        if chars > DESCRIPTION_LENGTH {
261            return Err(EmbedValidationError {
262                kind: EmbedValidationErrorType::DescriptionTooLarge { chars },
263            });
264        }
265    }
266
267    if embed.fields.len() > FIELD_COUNT {
268        return Err(EmbedValidationError {
269            kind: EmbedValidationErrorType::TooManyFields {
270                amount: embed.fields.len(),
271            },
272        });
273    }
274
275    for field in &embed.fields {
276        let name_chars = field.name.chars().count();
277
278        if name_chars > FIELD_NAME_LENGTH {
279            return Err(EmbedValidationError {
280                kind: EmbedValidationErrorType::FieldNameTooLarge { chars: name_chars },
281            });
282        }
283
284        let value_chars = field.value.chars().count();
285
286        if value_chars > FIELD_VALUE_LENGTH {
287            return Err(EmbedValidationError {
288                kind: EmbedValidationErrorType::FieldValueTooLarge { chars: value_chars },
289            });
290        }
291    }
292
293    if let Some(footer) = embed.footer.as_ref() {
294        let chars = footer.text.chars().count();
295
296        if chars > FOOTER_TEXT_LENGTH {
297            return Err(EmbedValidationError {
298                kind: EmbedValidationErrorType::FooterTextTooLarge { chars },
299            });
300        }
301    }
302
303    if let Some(name) = embed.author.as_ref().map(|author| &author.name) {
304        let chars = name.chars().count();
305
306        if chars > AUTHOR_NAME_LENGTH {
307            return Err(EmbedValidationError {
308                kind: EmbedValidationErrorType::AuthorNameTooLarge { chars },
309            });
310        }
311    }
312
313    if let Some(title) = embed.title.as_ref() {
314        let chars = title.chars().count();
315
316        if chars > TITLE_LENGTH {
317            return Err(EmbedValidationError {
318                kind: EmbedValidationErrorType::TitleTooLarge { chars },
319            });
320        }
321    }
322
323    Ok(())
324}
325
326/// Calculate the total character count of an embed.
327#[must_use]
328pub fn chars(embed: &Embed) -> usize {
329    let mut chars = 0;
330
331    if let Some(author) = &embed.author {
332        chars += author.name.chars().count();
333    }
334
335    if let Some(description) = &embed.description {
336        chars += description.chars().count();
337    }
338
339    if let Some(footer) = &embed.footer {
340        chars += footer.text.chars().count();
341    }
342
343    for field in &embed.fields {
344        chars += field.name.chars().count();
345        chars += field.value.chars().count();
346    }
347
348    if let Some(title) = &embed.title {
349        chars += title.chars().count();
350    }
351
352    chars
353}
354
355#[cfg(test)]
356mod tests {
357    use super::{EmbedValidationError, EmbedValidationErrorType};
358    use static_assertions::assert_impl_all;
359    use std::fmt::Debug;
360    use twilight_model::channel::message::{
361        Embed,
362        embed::{EmbedAuthor, EmbedField, EmbedFooter},
363    };
364
365    assert_impl_all!(EmbedValidationErrorType: Debug, Send, Sync);
366    assert_impl_all!(EmbedValidationError: Debug, Send, Sync);
367
368    fn base_embed() -> Embed {
369        Embed {
370            author: None,
371            color: None,
372            description: None,
373            fields: Vec::new(),
374            flags: None,
375            footer: None,
376            image: None,
377            kind: "rich".to_owned(),
378            provider: None,
379            thumbnail: None,
380            timestamp: None,
381            title: None,
382            url: None,
383            video: None,
384        }
385    }
386
387    #[test]
388    fn embed_base() {
389        let embed = base_embed();
390
391        assert!(super::embed(&embed).is_ok());
392    }
393
394    #[test]
395    fn embed_normal() {
396        let mut embed = base_embed();
397        embed.author.replace(EmbedAuthor {
398            icon_url: None,
399            name: "twilight".to_owned(),
400            proxy_icon_url: None,
401            url: None,
402        });
403        embed.color.replace(0xff_00_00);
404        embed.description.replace("a".repeat(100));
405        embed.fields.push(EmbedField {
406            inline: true,
407            name: "b".repeat(25),
408            value: "c".repeat(200),
409        });
410        embed.title.replace("this is a normal title".to_owned());
411
412        assert!(super::embed(&embed).is_ok());
413    }
414
415    #[test]
416    fn embed_author_name_limit() {
417        let mut embed = base_embed();
418        embed.author.replace(EmbedAuthor {
419            icon_url: None,
420            name: str::repeat("a", 256),
421            proxy_icon_url: None,
422            url: None,
423        });
424        assert!(super::embed(&embed).is_ok());
425
426        embed.author.replace(EmbedAuthor {
427            icon_url: None,
428            name: str::repeat("a", 257),
429            proxy_icon_url: None,
430            url: None,
431        });
432        assert!(matches!(
433            super::embed(&embed).unwrap_err().kind(),
434            EmbedValidationErrorType::AuthorNameTooLarge { chars: 257 }
435        ));
436    }
437
438    #[test]
439    fn embed_description_limit() {
440        let mut embed = base_embed();
441        embed.description.replace(str::repeat("a", 2048));
442        assert!(super::embed(&embed).is_ok());
443
444        embed.description.replace(str::repeat("a", 4096));
445        assert!(super::embed(&embed).is_ok());
446
447        embed.description.replace(str::repeat("a", 4097));
448        assert!(matches!(
449            super::embed(&embed).unwrap_err().kind(),
450            EmbedValidationErrorType::DescriptionTooLarge { chars: 4097 }
451        ));
452    }
453
454    #[test]
455    fn embed_field_count_limit() {
456        let mut embed = base_embed();
457
458        for _ in 0..26 {
459            embed.fields.push(EmbedField {
460                inline: true,
461                name: "a".to_owned(),
462                value: "a".to_owned(),
463            });
464        }
465
466        assert!(matches!(
467            super::embed(&embed).unwrap_err().kind(),
468            EmbedValidationErrorType::TooManyFields { amount: 26 }
469        ));
470    }
471
472    #[test]
473    fn embed_field_name_limit() {
474        let mut embed = base_embed();
475        embed.fields.push(EmbedField {
476            inline: true,
477            name: str::repeat("a", 256),
478            value: "a".to_owned(),
479        });
480        assert!(super::embed(&embed).is_ok());
481
482        embed.fields.push(EmbedField {
483            inline: true,
484            name: str::repeat("a", 257),
485            value: "a".to_owned(),
486        });
487        assert!(matches!(
488            super::embed(&embed).unwrap_err().kind(),
489            EmbedValidationErrorType::FieldNameTooLarge { chars: 257 }
490        ));
491    }
492
493    #[test]
494    fn embed_field_value_limit() {
495        let mut embed = base_embed();
496        embed.fields.push(EmbedField {
497            inline: true,
498            name: "a".to_owned(),
499            value: str::repeat("a", 1024),
500        });
501        assert!(super::embed(&embed).is_ok());
502
503        embed.fields.push(EmbedField {
504            inline: true,
505            name: "a".to_owned(),
506            value: str::repeat("a", 1025),
507        });
508        assert!(matches!(
509            super::embed(&embed).unwrap_err().kind(),
510            EmbedValidationErrorType::FieldValueTooLarge { chars: 1025 }
511        ));
512    }
513
514    #[test]
515    fn embed_footer_text_limit() {
516        let mut embed = base_embed();
517        embed.footer.replace(EmbedFooter {
518            icon_url: None,
519            proxy_icon_url: None,
520            text: str::repeat("a", 2048),
521        });
522        assert!(super::embed(&embed).is_ok());
523
524        embed.footer.replace(EmbedFooter {
525            icon_url: None,
526            proxy_icon_url: None,
527            text: str::repeat("a", 2049),
528        });
529        assert!(matches!(
530            super::embed(&embed).unwrap_err().kind(),
531            EmbedValidationErrorType::FooterTextTooLarge { chars: 2049 }
532        ));
533    }
534
535    #[test]
536    fn embed_title_limit() {
537        let mut embed = base_embed();
538        embed.title.replace(str::repeat("a", 256));
539        assert!(super::embed(&embed).is_ok());
540
541        embed.title.replace(str::repeat("a", 257));
542        assert!(matches!(
543            super::embed(&embed).unwrap_err().kind(),
544            EmbedValidationErrorType::TitleTooLarge { chars: 257 }
545        ));
546    }
547
548    #[test]
549    fn embed_combined_limit() {
550        let mut embed = base_embed();
551        embed.description.replace(str::repeat("a", 2048));
552        embed.title.replace(str::repeat("a", 256));
553
554        for _ in 0..5 {
555            embed.fields.push(EmbedField {
556                inline: true,
557                name: str::repeat("a", 100),
558                value: str::repeat("a", 500),
559            });
560        }
561
562        // we're at 5304 characters now
563        assert!(super::embed(&embed).is_ok());
564
565        embed.footer.replace(EmbedFooter {
566            icon_url: None,
567            proxy_icon_url: None,
568            text: str::repeat("a", 1000),
569        });
570
571        assert!(matches!(
572            super::embed(&embed).unwrap_err().kind(),
573            EmbedValidationErrorType::EmbedTooLarge { chars: 6304 }
574        ));
575    }
576}