twilight_util/builder/embed/mod.rs
1//! Create an [`Embed`] with a builder.
2
3pub mod image_source;
4
5mod author;
6mod field;
7mod footer;
8
9pub use self::{
10 author::EmbedAuthorBuilder, field::EmbedFieldBuilder, footer::EmbedFooterBuilder,
11 image_source::ImageSource,
12};
13
14use twilight_model::{
15 channel::message::embed::{
16 Embed, EmbedAuthor, EmbedField, EmbedFooter, EmbedImage, EmbedThumbnail,
17 },
18 util::Timestamp,
19};
20use twilight_validate::embed::{EmbedValidationError, embed as validate_embed};
21
22/// Create an [`Embed`] with a builder.
23///
24/// # Examples
25///
26/// Build a simple embed:
27///
28/// ```no_run
29/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
30/// use twilight_util::builder::embed::{EmbedBuilder, EmbedFieldBuilder};
31///
32/// let embed = EmbedBuilder::new()
33/// .description("Here's a list of reasons why Twilight is the best pony:")
34/// .field(EmbedFieldBuilder::new("Wings", "She has wings.").inline())
35/// .field(
36/// EmbedFieldBuilder::new("Horn", "She can do magic, and she's really good at it.")
37/// .inline(),
38/// )
39/// .validate()?
40/// .build();
41/// # Ok(()) }
42/// ```
43///
44/// Build an embed with an image:
45///
46/// ```no_run
47/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
48/// use twilight_util::builder::embed::{EmbedBuilder, ImageSource};
49///
50/// let embed = EmbedBuilder::new()
51/// .description("Here's a cool image of Twilight Sparkle")
52/// .image(ImageSource::attachment("bestpony.png")?)
53/// .validate()?
54/// .build();
55/// # Ok(()) }
56/// ```
57#[derive(Clone, Debug, Eq, PartialEq)]
58#[must_use = "must be built into an embed"]
59pub struct EmbedBuilder(Embed);
60
61impl EmbedBuilder {
62 /// Create a new embed builder.
63 pub fn new() -> Self {
64 EmbedBuilder(Embed {
65 author: None,
66 color: None,
67 description: None,
68 fields: Vec::new(),
69 flags: None,
70 footer: None,
71 image: None,
72 kind: "rich".to_owned(),
73 provider: None,
74 thumbnail: None,
75 timestamp: None,
76 title: None,
77 url: None,
78 video: None,
79 })
80 }
81
82 /// Build this into an embed.
83 #[must_use = "should be used as part of something like a message"]
84 pub fn build(self) -> Embed {
85 self.0
86 }
87
88 /// Ensure the embed is valid.
89 ///
90 /// # Errors
91 ///
92 /// Refer to the documentation of [`twilight_validate::embed::embed`] for
93 /// possible errors.
94 pub fn validate(self) -> Result<Self, EmbedValidationError> {
95 #[allow(clippy::question_mark)]
96 if let Err(source) = validate_embed(&self.0) {
97 return Err(source);
98 }
99
100 Ok(self)
101 }
102
103 /// Set the author.
104 ///
105 /// # Examples
106 ///
107 /// Create an embed author:
108 ///
109 /// ```
110 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
111 /// use twilight_util::builder::embed::{EmbedAuthorBuilder, EmbedBuilder};
112 ///
113 /// let author = EmbedAuthorBuilder::new("Twilight")
114 /// .url("https://github.com/twilight-rs/twilight")
115 /// .build();
116 ///
117 /// let embed = EmbedBuilder::new().author(author).validate()?.build();
118 /// # Ok(()) }
119 /// ```
120 pub fn author(mut self, author: impl Into<EmbedAuthor>) -> Self {
121 self.0.author = Some(author.into());
122
123 self
124 }
125
126 /// Set the color.
127 ///
128 /// This must be a valid hexadecimal RGB value. Refer to
129 /// [`COLOR_MAXIMUM`] for the maximum acceptable value.
130 ///
131 /// # Examples
132 ///
133 /// Set the color of an embed to `0xfd69b3`:
134 ///
135 /// ```
136 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
137 /// use twilight_util::builder::embed::EmbedBuilder;
138 ///
139 /// let embed = EmbedBuilder::new()
140 /// .color(0xfd_69_b3)
141 /// .description("a description")
142 /// .validate()?
143 /// .build();
144 /// # Ok(()) }
145 /// ```
146 ///
147 /// [`COLOR_MAXIMUM`]: twilight_validate::embed::COLOR_MAXIMUM
148 pub const fn color(mut self, color: u32) -> Self {
149 self.0.color = Some(color);
150
151 self
152 }
153
154 /// Set the description.
155 ///
156 /// Refer to [`DESCRIPTION_LENGTH`] for the maximum number of UTF-16 code
157 /// points that can be in a description.
158 ///
159 /// # Examples
160 ///
161 /// ```
162 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
163 /// use twilight_util::builder::embed::EmbedBuilder;
164 ///
165 /// let embed = EmbedBuilder::new()
166 /// .description("this is an embed")
167 /// .validate()?
168 /// .build();
169 /// # Ok(()) }
170 /// ```
171 ///
172 /// [`DESCRIPTION_LENGTH`]: twilight_validate::embed::DESCRIPTION_LENGTH
173 pub fn description(mut self, description: impl Into<String>) -> Self {
174 self.0.description = Some(description.into());
175
176 self
177 }
178
179 /// Add a field to the embed.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
185 /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFieldBuilder};
186 ///
187 /// let embed = EmbedBuilder::new()
188 /// .description("this is an embed")
189 /// .field(EmbedFieldBuilder::new("a field", "and its value"))
190 /// .validate()?
191 /// .build();
192 /// # Ok(()) }
193 /// ```
194 pub fn field(mut self, field: impl Into<EmbedField>) -> Self {
195 self.0.fields.push(field.into());
196
197 self
198 }
199
200 /// Set the footer of the embed.
201 ///
202 /// # Examples
203 ///
204 /// ```
205 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
206 /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder};
207 ///
208 /// let embed = EmbedBuilder::new()
209 /// .description("this is an embed")
210 /// .footer(EmbedFooterBuilder::new("a footer"))
211 /// .validate()?
212 /// .build();
213 /// # Ok(()) }
214 /// ```
215 pub fn footer(mut self, footer: impl Into<EmbedFooter>) -> Self {
216 self.0.footer = Some(footer.into());
217
218 self
219 }
220
221 /// Set the image.
222 ///
223 /// # Examples
224 ///
225 /// Set the image source to a URL:
226 ///
227 /// ```
228 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
229 /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder, ImageSource};
230 ///
231 /// let source =
232 /// ImageSource::url("https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png")?;
233 /// let embed = EmbedBuilder::new()
234 /// .footer(EmbedFooterBuilder::new("twilight"))
235 /// .image(source)
236 /// .validate()?
237 /// .build();
238 /// # Ok(()) }
239 /// ```
240 pub fn image(mut self, image_source: ImageSource) -> Self {
241 self.0.image = Some(EmbedImage {
242 height: None,
243 proxy_url: None,
244 url: image_source.0,
245 width: None,
246 content_type: None,
247 placeholder: None,
248 placeholder_version: None,
249 flags: None,
250 });
251
252 self
253 }
254
255 /// Add a thumbnail.
256 ///
257 /// # Examples
258 ///
259 /// Set the thumbnail to an image attachment with the filename
260 /// `"twilight.png"`:
261 ///
262 /// ```
263 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
264 /// use twilight_util::builder::embed::{EmbedBuilder, ImageSource};
265 ///
266 /// let embed = EmbedBuilder::new()
267 /// .description("a picture of twilight")
268 /// .thumbnail(ImageSource::attachment("twilight.png")?)
269 /// .validate()?
270 /// .build();
271 /// # Ok(()) }
272 /// ```
273 pub fn thumbnail(mut self, image_source: ImageSource) -> Self {
274 self.0.thumbnail = Some(EmbedThumbnail {
275 height: None,
276 proxy_url: None,
277 url: image_source.0,
278 width: None,
279 });
280
281 self
282 }
283
284 /// Set the ISO 8601 timestamp.
285 pub const fn timestamp(mut self, timestamp: Timestamp) -> Self {
286 self.0.timestamp = Some(timestamp);
287
288 self
289 }
290
291 /// Set the title.
292 ///
293 /// Refer to [`TITLE_LENGTH`] for the maximum number of UTF-16 code points
294 /// that can be in a title.
295 ///
296 /// # Examples
297 ///
298 /// Set the title to "twilight":
299 ///
300 /// ```
301 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
302 /// use twilight_util::builder::embed::EmbedBuilder;
303 ///
304 /// let embed = EmbedBuilder::new()
305 /// .title("twilight")
306 /// .url("https://github.com/twilight-rs/twilight")
307 /// .validate()?
308 /// .build();
309 /// # Ok(()) }
310 /// ```
311 ///
312 /// [`TITLE_LENGTH`]: twilight_validate::embed::TITLE_LENGTH
313 pub fn title(mut self, title: impl Into<String>) -> Self {
314 self.0.title = Some(title.into());
315
316 self
317 }
318
319 /// Set the URL.
320 ///
321 /// # Examples
322 ///
323 /// Set the URL to [twilight's repository]:
324 ///
325 /// ```
326 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
327 /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder};
328 ///
329 /// let embed = EmbedBuilder::new()
330 /// .description("twilight's repository")
331 /// .url("https://github.com/twilight-rs/twilight")
332 /// .validate()?
333 /// .build();
334 /// # Ok(()) }
335 /// ```
336 ///
337 /// [twilight's repository]: https://github.com/twilight-rs/twilight
338 pub fn url(mut self, url: impl Into<String>) -> Self {
339 self.0.url = Some(url.into());
340
341 self
342 }
343}
344
345impl Default for EmbedBuilder {
346 /// Create an embed builder with a default embed.
347 ///
348 /// All embeds have a "rich" type.
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354impl From<Embed> for EmbedBuilder {
355 fn from(value: Embed) -> Self {
356 Self(Embed {
357 kind: "rich".to_owned(),
358 ..value
359 })
360 }
361}
362
363impl TryFrom<EmbedBuilder> for Embed {
364 type Error = EmbedValidationError;
365
366 /// Convert an embed builder into an embed, validating its contents.
367 ///
368 /// This is equivalent to calling [`EmbedBuilder::validate`], then
369 /// [`EmbedBuilder::build`].
370 fn try_from(builder: EmbedBuilder) -> Result<Self, Self::Error> {
371 Ok(builder.validate()?.build())
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use static_assertions::assert_impl_all;
379 use std::fmt::Debug;
380
381 assert_impl_all!(EmbedBuilder: Clone, Debug, Eq, PartialEq, Send, Sync);
382 assert_impl_all!(Embed: TryFrom<EmbedBuilder>);
383
384 #[test]
385 fn builder() {
386 let footer_image = ImageSource::url(
387 "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png",
388 )
389 .unwrap();
390 let timestamp = Timestamp::from_secs(1_580_608_922).expect("non zero");
391
392 let embed = EmbedBuilder::new()
393 .color(0x00_43_ff)
394 .description("Description")
395 .timestamp(timestamp)
396 .footer(EmbedFooterBuilder::new("Warn").icon_url(footer_image))
397 .field(EmbedFieldBuilder::new("name", "title").inline())
398 .build();
399
400 let expected = Embed {
401 author: None,
402 color: Some(0x00_43_ff),
403 description: Some("Description".to_string()),
404 fields: [EmbedField {
405 inline: true,
406 name: "name".to_string(),
407 value: "title".to_string(),
408 }]
409 .to_vec(),
410 flags: None,
411 footer: Some(EmbedFooter {
412 icon_url: Some(
413 "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png"
414 .to_string(),
415 ),
416 proxy_icon_url: None,
417 text: "Warn".to_string(),
418 }),
419 image: None,
420 kind: "rich".to_string(),
421 provider: None,
422 thumbnail: None,
423 timestamp: Some(timestamp),
424 title: None,
425 url: None,
426 video: None,
427 };
428
429 assert_eq!(embed, expected);
430 }
431}