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