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