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        if color > COLOR_MAXIMUM {
251            return Err(EmbedValidationError {
252                kind: EmbedValidationErrorType::ColorNotRgb { color },
253            });
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.len();
333    }
334
335    if let Some(description) = &embed.description {
336        chars += description.len();
337    }
338
339    if let Some(footer) = &embed.footer {
340        chars += footer.text.len();
341    }
342
343    for field in &embed.fields {
344        chars += field.name.len();
345        chars += field.value.len();
346    }
347
348    if let Some(title) = &embed.title {
349        chars += title.len();
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::{EmbedAuthor, EmbedField, EmbedFooter},
362        Embed,
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            footer: None,
375            image: None,
376            kind: "rich".to_owned(),
377            provider: None,
378            thumbnail: None,
379            timestamp: None,
380            title: None,
381            url: None,
382            video: None,
383        }
384    }
385
386    #[test]
387    fn embed_base() {
388        let embed = base_embed();
389
390        assert!(super::embed(&embed).is_ok());
391    }
392
393    #[test]
394    fn embed_normal() {
395        let mut embed = base_embed();
396        embed.author.replace(EmbedAuthor {
397            icon_url: None,
398            name: "twilight".to_owned(),
399            proxy_icon_url: None,
400            url: None,
401        });
402        embed.color.replace(0xff_00_00);
403        embed.description.replace("a".repeat(100));
404        embed.fields.push(EmbedField {
405            inline: true,
406            name: "b".repeat(25),
407            value: "c".repeat(200),
408        });
409        embed.title.replace("this is a normal title".to_owned());
410
411        assert!(super::embed(&embed).is_ok());
412    }
413
414    #[test]
415    fn embed_author_name_limit() {
416        let mut embed = base_embed();
417        embed.author.replace(EmbedAuthor {
418            icon_url: None,
419            name: str::repeat("a", 256),
420            proxy_icon_url: None,
421            url: None,
422        });
423        assert!(super::embed(&embed).is_ok());
424
425        embed.author.replace(EmbedAuthor {
426            icon_url: None,
427            name: str::repeat("a", 257),
428            proxy_icon_url: None,
429            url: None,
430        });
431        assert!(matches!(
432            super::embed(&embed).unwrap_err().kind(),
433            EmbedValidationErrorType::AuthorNameTooLarge { chars: 257 }
434        ));
435    }
436
437    #[test]
438    fn embed_description_limit() {
439        let mut embed = base_embed();
440        embed.description.replace(str::repeat("a", 2048));
441        assert!(super::embed(&embed).is_ok());
442
443        embed.description.replace(str::repeat("a", 4096));
444        assert!(super::embed(&embed).is_ok());
445
446        embed.description.replace(str::repeat("a", 4097));
447        assert!(matches!(
448            super::embed(&embed).unwrap_err().kind(),
449            EmbedValidationErrorType::DescriptionTooLarge { chars: 4097 }
450        ));
451    }
452
453    #[test]
454    fn embed_field_count_limit() {
455        let mut embed = base_embed();
456
457        for _ in 0..26 {
458            embed.fields.push(EmbedField {
459                inline: true,
460                name: "a".to_owned(),
461                value: "a".to_owned(),
462            });
463        }
464
465        assert!(matches!(
466            super::embed(&embed).unwrap_err().kind(),
467            EmbedValidationErrorType::TooManyFields { amount: 26 }
468        ));
469    }
470
471    #[test]
472    fn embed_field_name_limit() {
473        let mut embed = base_embed();
474        embed.fields.push(EmbedField {
475            inline: true,
476            name: str::repeat("a", 256),
477            value: "a".to_owned(),
478        });
479        assert!(super::embed(&embed).is_ok());
480
481        embed.fields.push(EmbedField {
482            inline: true,
483            name: str::repeat("a", 257),
484            value: "a".to_owned(),
485        });
486        assert!(matches!(
487            super::embed(&embed).unwrap_err().kind(),
488            EmbedValidationErrorType::FieldNameTooLarge { chars: 257 }
489        ));
490    }
491
492    #[test]
493    fn embed_field_value_limit() {
494        let mut embed = base_embed();
495        embed.fields.push(EmbedField {
496            inline: true,
497            name: "a".to_owned(),
498            value: str::repeat("a", 1024),
499        });
500        assert!(super::embed(&embed).is_ok());
501
502        embed.fields.push(EmbedField {
503            inline: true,
504            name: "a".to_owned(),
505            value: str::repeat("a", 1025),
506        });
507        assert!(matches!(
508            super::embed(&embed).unwrap_err().kind(),
509            EmbedValidationErrorType::FieldValueTooLarge { chars: 1025 }
510        ));
511    }
512
513    #[test]
514    fn embed_footer_text_limit() {
515        let mut embed = base_embed();
516        embed.footer.replace(EmbedFooter {
517            icon_url: None,
518            proxy_icon_url: None,
519            text: str::repeat("a", 2048),
520        });
521        assert!(super::embed(&embed).is_ok());
522
523        embed.footer.replace(EmbedFooter {
524            icon_url: None,
525            proxy_icon_url: None,
526            text: str::repeat("a", 2049),
527        });
528        assert!(matches!(
529            super::embed(&embed).unwrap_err().kind(),
530            EmbedValidationErrorType::FooterTextTooLarge { chars: 2049 }
531        ));
532    }
533
534    #[test]
535    fn embed_title_limit() {
536        let mut embed = base_embed();
537        embed.title.replace(str::repeat("a", 256));
538        assert!(super::embed(&embed).is_ok());
539
540        embed.title.replace(str::repeat("a", 257));
541        assert!(matches!(
542            super::embed(&embed).unwrap_err().kind(),
543            EmbedValidationErrorType::TitleTooLarge { chars: 257 }
544        ));
545    }
546
547    #[test]
548    fn embed_combined_limit() {
549        let mut embed = base_embed();
550        embed.description.replace(str::repeat("a", 2048));
551        embed.title.replace(str::repeat("a", 256));
552
553        for _ in 0..5 {
554            embed.fields.push(EmbedField {
555                inline: true,
556                name: str::repeat("a", 100),
557                value: str::repeat("a", 500),
558            });
559        }
560
561        // we're at 5304 characters now
562        assert!(super::embed(&embed).is_ok());
563
564        embed.footer.replace(EmbedFooter {
565            icon_url: None,
566            proxy_icon_url: None,
567            text: str::repeat("a", 1000),
568        });
569
570        assert!(matches!(
571            super::embed(&embed).unwrap_err().kind(),
572            EmbedValidationErrorType::EmbedTooLarge { chars: 6304 }
573        ));
574    }
575}