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