twilight_util/builder/embed/
image_source.rs

1//! Sources to image URLs and attachments.
2
3use std::{
4    error::Error,
5    fmt::{Display, Formatter, Result as FmtResult},
6};
7
8/// Error creating an embed field.
9#[derive(Debug)]
10pub struct ImageSourceAttachmentError {
11    kind: ImageSourceAttachmentErrorType,
12}
13
14impl ImageSourceAttachmentError {
15    /// Immutable reference to the type of error that occurred.
16    #[must_use = "retrieving the type has no effect if left unused"]
17    pub const fn kind(&self) -> &ImageSourceAttachmentErrorType {
18        &self.kind
19    }
20
21    /// Consume the error, returning the source error if there is any.
22    #[allow(clippy::unused_self)]
23    #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
24    pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
25        None
26    }
27
28    /// Consume the error, returning the owned error type and the source error.
29    #[must_use = "consuming the error into its parts has no effect if left unused"]
30    pub fn into_parts(
31        self,
32    ) -> (
33        ImageSourceAttachmentErrorType,
34        Option<Box<dyn Error + Send + Sync>>,
35    ) {
36        (self.kind, None)
37    }
38}
39
40impl Display for ImageSourceAttachmentError {
41    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
42        match &self.kind {
43            ImageSourceAttachmentErrorType::ExtensionEmpty => f.write_str("the extension is empty"),
44            ImageSourceAttachmentErrorType::ExtensionMissing => {
45                f.write_str("the extension is missing")
46            }
47        }
48    }
49}
50
51impl Error for ImageSourceAttachmentError {}
52
53/// Type of [`ImageSourceAttachmentError`] that occurred.
54#[derive(Debug)]
55#[non_exhaustive]
56pub enum ImageSourceAttachmentErrorType {
57    /// An extension is present in the provided filename but it is empty.
58    ExtensionEmpty,
59    /// An extension is missing in the provided filename.
60    ExtensionMissing,
61}
62
63/// Error creating an embed field.
64#[derive(Debug)]
65pub struct ImageSourceUrlError {
66    kind: ImageSourceUrlErrorType,
67}
68
69impl ImageSourceUrlError {
70    /// Immutable reference to the type of error that occurred.
71    #[must_use = "retrieving the type has no effect if left unused"]
72    pub const fn kind(&self) -> &ImageSourceUrlErrorType {
73        &self.kind
74    }
75
76    /// Consume the error, returning the source error if there is any.
77    #[allow(clippy::unused_self)]
78    #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
79    pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
80        None
81    }
82
83    /// Consume the error, returning the owned error type and the source error.
84    #[must_use = "consuming the error into its parts has no effect if left unused"]
85    pub fn into_parts(
86        self,
87    ) -> (
88        ImageSourceUrlErrorType,
89        Option<Box<dyn Error + Send + Sync>>,
90    ) {
91        (self.kind, None)
92    }
93}
94
95impl Display for ImageSourceUrlError {
96    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
97        match &self.kind {
98            ImageSourceUrlErrorType::ProtocolUnsupported { .. } => {
99                f.write_str("the provided URL's protocol is unsupported by Discord")
100            }
101        }
102    }
103}
104
105impl Error for ImageSourceUrlError {}
106
107/// Type of [`ImageSourceUrlError`] that occurred.
108#[derive(Debug)]
109#[non_exhaustive]
110pub enum ImageSourceUrlErrorType {
111    /// The Protocol of the URL is unsupported by the Discord REST API.
112    ///
113    /// Refer to [`ImageSource::url`] for a list of protocols that are acceptable.
114    ProtocolUnsupported {
115        /// Provided URL.
116        url: String,
117    },
118}
119
120/// Image sourcing for embed images.
121#[derive(Clone, Debug, Eq, PartialEq)]
122#[non_exhaustive]
123pub struct ImageSource(pub(super) String);
124
125impl ImageSource {
126    /// Create an attachment image source.
127    ///
128    /// This will automatically prepend `attachment://` to the source.
129    ///
130    /// # Errors
131    ///
132    /// Returns an [`ImageSourceAttachmentErrorType::ExtensionEmpty`] if an
133    /// extension exists but is empty.
134    ///
135    /// Returns an [`ImageSourceAttachmentErrorType::ExtensionMissing`] if an
136    /// extension is missing.
137    pub fn attachment(filename: impl AsRef<str>) -> Result<Self, ImageSourceAttachmentError> {
138        let filename = filename.as_ref();
139
140        let dot = filename.rfind('.').ok_or(ImageSourceAttachmentError {
141            kind: ImageSourceAttachmentErrorType::ExtensionMissing,
142        })? + 1;
143
144        if filename
145            .get(dot..)
146            .ok_or(ImageSourceAttachmentError {
147                kind: ImageSourceAttachmentErrorType::ExtensionMissing,
148            })?
149            .is_empty()
150        {
151            return Err(ImageSourceAttachmentError {
152                kind: ImageSourceAttachmentErrorType::ExtensionEmpty,
153            });
154        }
155
156        Ok(Self(format!("attachment://{filename}")))
157    }
158
159    /// Create a URL image source.
160    ///
161    /// The following URL protocols are acceptable:
162    ///
163    /// - https
164    /// - http
165    ///
166    /// # Errors
167    ///
168    /// Returns an [`ImageSourceUrlErrorType::ProtocolUnsupported`] error type
169    /// if the URL's protocol is unsupported.
170    pub fn url(url: impl Into<String>) -> Result<Self, ImageSourceUrlError> {
171        let url = url.into();
172
173        if !url.starts_with("https:") && !url.starts_with("http:") {
174            return Err(ImageSourceUrlError {
175                kind: ImageSourceUrlErrorType::ProtocolUnsupported { url },
176            });
177        }
178
179        Ok(Self(url))
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use static_assertions::{assert_fields, assert_impl_all};
187    use std::fmt::Debug;
188
189    assert_impl_all!(ImageSourceAttachmentErrorType: Debug, Send, Sync);
190    assert_impl_all!(ImageSourceAttachmentError: Error, Send, Sync);
191    assert_impl_all!(ImageSourceUrlErrorType: Debug, Send, Sync);
192    assert_impl_all!(ImageSourceUrlError: Error, Send, Sync);
193    assert_fields!(ImageSourceUrlErrorType::ProtocolUnsupported: url);
194    assert_impl_all!(ImageSource: Clone, Debug, Eq, PartialEq, Send, Sync);
195
196    #[test]
197    fn attachment() -> Result<(), Box<dyn Error>> {
198        assert!(matches!(
199            ImageSource::attachment("abc").unwrap_err().kind(),
200            ImageSourceAttachmentErrorType::ExtensionMissing
201        ));
202        assert!(matches!(
203            ImageSource::attachment("abc.").unwrap_err().kind(),
204            ImageSourceAttachmentErrorType::ExtensionEmpty
205        ));
206        assert_eq!(
207            ImageSource::attachment("abc.png")?,
208            ImageSource("attachment://abc.png".to_owned()),
209        );
210
211        Ok(())
212    }
213
214    #[test]
215    fn url() -> Result<(), Box<dyn Error>> {
216        assert!(matches!(
217            ImageSource::url("ftp://example.com/foo").unwrap_err().kind(),
218            ImageSourceUrlErrorType::ProtocolUnsupported { url }
219            if url == "ftp://example.com/foo"
220        ));
221        assert_eq!(
222            ImageSource::url("https://example.com")?,
223            ImageSource("https://example.com".to_owned()),
224        );
225        assert_eq!(
226            ImageSource::url("http://example.com")?,
227            ImageSource("http://example.com".to_owned()),
228        );
229
230        Ok(())
231    }
232}