serenity_builder/
embed.rs

1use serenity::all::{Colour, CreateEmbed};
2
3use crate::model::embed::SerenityEmbed;
4
5/// Errors that can occur when converting a [SerenityEmbed] to a [serenity::all::CreateEmbed].
6#[derive(thiserror::Error, Debug)]
7pub enum SerenityEmbedConvertError {
8    /**
9     * This occurs when the embedded description exceeds 4096 characters and is subject to Discord API limitations.
10     *
11     * Serenity-builder will report an error during conversion.
12     * Serenity does not return an error and leaves the response entirely up to the Discord API.
13     */
14    #[error("The description exceeds the maximum length of 4096 characters.")]
15    TooLongDescription,
16    /**
17     * This occurs when the number of embedded fields exceeds 25 and hits the Discord API limit.
18     *
19     * Serenity-builder will report an error during conversion.
20     * Serenity does not return an error and leaves the response entirely up to the Discord API.
21     */
22    #[error("The number of fields exceeds the maximum of 25.")]
23    TooManyFields,
24}
25
26impl SerenityEmbed {
27    /// Convert the embedded structure created in Builder into a model usable in Serenity.
28    ///
29    /// ```rs
30    /// let embed = SerenityEmbed::builder()
31    ///    .title("This is a test title.")
32    ///    /// ... other fields ...
33    ///   .build();
34    ///
35    /// let serenity_embed = embed.convert()?; // Result<CreateEmbed, SerenityEmbedConvertError>
36    /// ```
37    ///
38    /// # How to use
39    ///
40    /// ```rs
41    /// // 1. Create a SerenityEmbed using the builder
42    /// let embed = SerenityEmbed::builder()
43    ///   .title("This is a test title.")
44    ///   .description("This is a test description.")
45    ///   .build(); // Don't forget!: If you forget this, you won't be able to use `convert()`.
46    ///
47    /// // 2. Convert to Serenity's CreateEmbed
48    /// let serenity_embed = embed.convert()?; // Result<CreateEmbed, SerenityEmbedConvertError>
49    ///
50    /// // 3. Use the converted embed in your Serenity message
51    /// let message = serenity::builder::CreateMessage::default()
52    ///  .content("Here is an embed!")
53    ///  // ... other message fields ...
54    /// ```
55    ///
56    /// # Errors
57    ///
58    /// This function may return the following error:
59    ///
60    /// - [`SerenityEmbedConvertError::TooLongDescription`]: The description exceeds the maximum length of 4096 characters.
61    /// - [`SerenityEmbedConvertError::TooManyFields`]: The number of fields exceeds the maximum of 25.
62    pub fn convert(&self) -> Result<CreateEmbed, SerenityEmbedConvertError> {
63        let mut embed = serenity::builder::CreateEmbed::default();
64
65        if let Some(title) = &self.title {
66            embed = embed.title(title)
67        }
68
69        if let Some(description) = &self.description {
70            if description.len() > 4096 {
71                return Err(SerenityEmbedConvertError::TooLongDescription);
72            }
73
74            embed = embed.description(description);
75        }
76
77        if let Some(url) = &self.url {
78            embed = embed.url(url);
79        }
80
81        if let Some(timestamp) = &self.timestamp {
82            embed = embed.timestamp(timestamp);
83        }
84
85        if let Some(color) = &self.color {
86            embed = embed.color(Colour(*color));
87        }
88
89        if let Some(footer_text) = &self.footer_text {
90            let mut footer = serenity::builder::CreateEmbedFooter::new(footer_text);
91            if let Some(icon_url) = &self.footer_icon_url {
92                footer = footer.icon_url(icon_url);
93            }
94            embed = embed.footer(footer);
95        }
96
97        if let Some(image_url) = &self.image_url {
98            embed = embed.image(image_url);
99        }
100
101        if let Some(thumbnail_url) = &self.thumbnail_url {
102            embed = embed.thumbnail(thumbnail_url);
103        }
104
105        if let Some(author_name) = &self.author_name {
106            let mut author = serenity::builder::CreateEmbedAuthor::new(author_name);
107            if let Some(url) = &self.author_url {
108                author = author.url(url);
109            }
110            if let Some(icon_url) = &self.author_icon_url {
111                author = author.icon_url(icon_url);
112            }
113            embed = embed.author(author);
114        }
115
116        if let Some(fields) = &self.fields {
117            if fields.len() > 25 {
118                return Err(SerenityEmbedConvertError::TooManyFields);
119            }
120            // Explicitly create and pass (String, String, bool) to avoid ambiguity in `Into<String>` (inference failure due to multiple impls).
121            let mapped = fields
122                .iter()
123                .map(|f| (f.name.clone(), f.value.clone(), f.inline));
124            embed = embed.fields(mapped)
125        }
126
127        Ok(embed)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::model::embed::SerenityEmbedField;
135    use serenity::all::Timestamp;
136
137    static MOCK_TEXT: &str = "This is a test text.";
138    static MOCK_URL: &str = "https://example.com";
139    static MOCK_TIMESTAMP_STR: &str = "2024-01-01T00:00:00Z";
140    static MOCK_COLOR: u32 = 0xff0000;
141
142    #[test]
143    fn test_embed_conversion() {
144        let fields = vec![
145            SerenityEmbedField::builder()
146                .name(MOCK_TEXT)
147                .value(MOCK_TEXT)
148                .inline(true)
149                .build(),
150            SerenityEmbedField::builder()
151                .name(MOCK_TEXT)
152                .value(MOCK_TEXT)
153                .build(),
154        ];
155
156        // serenity-builder
157        let mock_embed = SerenityEmbed::builder()
158            .title(MOCK_TEXT)
159            .description(MOCK_TEXT)
160            .url(MOCK_URL)
161            .timestamp(Timestamp::parse(MOCK_TIMESTAMP_STR).unwrap())
162            .color(MOCK_COLOR)
163            .footer_text(MOCK_TEXT)
164            .footer_icon_url(MOCK_URL)
165            .image_url(MOCK_URL)
166            .thumbnail_url(MOCK_URL)
167            .author_name(MOCK_TEXT)
168            .author_url(MOCK_URL)
169            .author_icon_url(MOCK_URL)
170            .fields(fields)
171            .build();
172        // serenity
173        let serenity_embed = CreateEmbed::default()
174            .title(MOCK_TEXT)
175            .url(MOCK_URL)
176            .description(MOCK_TEXT)
177            .timestamp(Timestamp::parse(MOCK_TIMESTAMP_STR).unwrap())
178            .color(Colour(MOCK_COLOR))
179            .footer(serenity::builder::CreateEmbedFooter::new(MOCK_TEXT).icon_url(MOCK_URL))
180            .image(MOCK_URL)
181            .thumbnail(MOCK_URL)
182            .author(
183                serenity::builder::CreateEmbedAuthor::new(MOCK_TEXT)
184                    .url(MOCK_URL)
185                    .icon_url(MOCK_URL),
186            )
187            .fields(vec![
188                (MOCK_TEXT.to_string(), MOCK_TEXT.to_string(), true),
189                (MOCK_TEXT.to_string(), MOCK_TEXT.to_string(), false),
190            ]);
191
192        let converted = mock_embed.convert();
193
194        assert!(converted.is_ok());
195        assert_eq!(converted.unwrap(), serenity_embed);
196    }
197}