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::{embed as validate_embed, EmbedValidationError};
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 #[allow(clippy::missing_const_for_fn)]
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 #[allow(clippy::missing_const_for_fn)]
241 pub fn image(mut self, image_source: ImageSource) -> Self {
242 self.0.image = Some(EmbedImage {
243 height: None,
244 proxy_url: None,
245 url: image_source.0,
246 width: None,
247 });
248
249 self
250 }
251
252 /// Add a thumbnail.
253 ///
254 /// # Examples
255 ///
256 /// Set the thumbnail to an image attachment with the filename
257 /// `"twilight.png"`:
258 ///
259 /// ```
260 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
261 /// use twilight_util::builder::embed::{EmbedBuilder, ImageSource};
262 ///
263 /// let embed = EmbedBuilder::new()
264 /// .description("a picture of twilight")
265 /// .thumbnail(ImageSource::attachment("twilight.png")?)
266 /// .validate()?
267 /// .build();
268 /// # Ok(()) }
269 /// ```
270 #[allow(clippy::missing_const_for_fn)]
271 pub fn thumbnail(mut self, image_source: ImageSource) -> Self {
272 self.0.thumbnail = Some(EmbedThumbnail {
273 height: None,
274 proxy_url: None,
275 url: image_source.0,
276 width: None,
277 });
278
279 self
280 }
281
282 /// Set the ISO 8601 timestamp.
283 pub const fn timestamp(mut self, timestamp: Timestamp) -> Self {
284 self.0.timestamp = Some(timestamp);
285
286 self
287 }
288
289 /// Set the title.
290 ///
291 /// Refer to [`TITLE_LENGTH`] for the maximum number of UTF-16 code points
292 /// that can be in a title.
293 ///
294 /// # Examples
295 ///
296 /// Set the title to "twilight":
297 ///
298 /// ```
299 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
300 /// use twilight_util::builder::embed::EmbedBuilder;
301 ///
302 /// let embed = EmbedBuilder::new()
303 /// .title("twilight")
304 /// .url("https://github.com/twilight-rs/twilight")
305 /// .validate()?
306 /// .build();
307 /// # Ok(()) }
308 /// ```
309 ///
310 /// [`TITLE_LENGTH`]: twilight_validate::embed::TITLE_LENGTH
311 pub fn title(mut self, title: impl Into<String>) -> Self {
312 self.0.title = Some(title.into());
313
314 self
315 }
316
317 /// Set the URL.
318 ///
319 /// # Examples
320 ///
321 /// Set the URL to [twilight's repository]:
322 ///
323 /// ```
324 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
325 /// use twilight_util::builder::embed::{EmbedBuilder, EmbedFooterBuilder};
326 ///
327 /// let embed = EmbedBuilder::new()
328 /// .description("twilight's repository")
329 /// .url("https://github.com/twilight-rs/twilight")
330 /// .validate()?
331 /// .build();
332 /// # Ok(()) }
333 /// ```
334 ///
335 /// [twilight's repository]: https://github.com/twilight-rs/twilight
336 pub fn url(mut self, url: impl Into<String>) -> Self {
337 self.0.url = Some(url.into());
338
339 self
340 }
341}
342
343impl Default for EmbedBuilder {
344 /// Create an embed builder with a default embed.
345 ///
346 /// All embeds have a "rich" type.
347 fn default() -> Self {
348 Self::new()
349 }
350}
351
352impl From<Embed> for EmbedBuilder {
353 fn from(value: Embed) -> Self {
354 Self(Embed {
355 kind: "rich".to_owned(),
356 ..value
357 })
358 }
359}
360
361impl TryFrom<EmbedBuilder> for Embed {
362 type Error = EmbedValidationError;
363
364 /// Convert an embed builder into an embed, validating its contents.
365 ///
366 /// This is equivalent to calling [`EmbedBuilder::validate`], then
367 /// [`EmbedBuilder::build`].
368 fn try_from(builder: EmbedBuilder) -> Result<Self, Self::Error> {
369 Ok(builder.validate()?.build())
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use static_assertions::assert_impl_all;
377 use std::fmt::Debug;
378
379 assert_impl_all!(EmbedBuilder: Clone, Debug, Eq, PartialEq, Send, Sync);
380 assert_impl_all!(Embed: TryFrom<EmbedBuilder>);
381
382 #[test]
383 fn builder() {
384 let footer_image = ImageSource::url(
385 "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png",
386 )
387 .unwrap();
388 let timestamp = Timestamp::from_secs(1_580_608_922).expect("non zero");
389
390 let embed = EmbedBuilder::new()
391 .color(0x00_43_ff)
392 .description("Description")
393 .timestamp(timestamp)
394 .footer(EmbedFooterBuilder::new("Warn").icon_url(footer_image))
395 .field(EmbedFieldBuilder::new("name", "title").inline())
396 .build();
397
398 let expected = Embed {
399 author: None,
400 color: Some(0x00_43_ff),
401 description: Some("Description".to_string()),
402 fields: [EmbedField {
403 inline: true,
404 name: "name".to_string(),
405 value: "title".to_string(),
406 }]
407 .to_vec(),
408 footer: Some(EmbedFooter {
409 icon_url: Some(
410 "https://raw.githubusercontent.com/twilight-rs/twilight/main/logo.png"
411 .to_string(),
412 ),
413 proxy_icon_url: None,
414 text: "Warn".to_string(),
415 }),
416 image: None,
417 kind: "rich".to_string(),
418 provider: None,
419 thumbnail: None,
420 timestamp: Some(timestamp),
421 title: None,
422 url: None,
423 video: None,
424 };
425
426 assert_eq!(embed, expected);
427 }
428}