twilight_util/link/
webhook.rs1use std::{
7 error::Error,
8 fmt::{Display, Formatter, Result as FmtResult},
9 num::NonZero,
10};
11use twilight_model::id::{Id, marker::WebhookMarker};
12
13#[derive(Debug)]
17pub struct WebhookParseError {
18 kind: WebhookParseErrorType,
19 source: Option<Box<dyn Error + Send + Sync>>,
20}
21
22impl WebhookParseError {
23 #[must_use = "retrieving the type has no effect if left unused"]
25 pub const fn kind(&self) -> &WebhookParseErrorType {
26 &self.kind
27 }
28
29 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
31 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
32 self.source
33 }
34
35 #[must_use = "consuming the error into its parts has no effect if left unused"]
37 pub fn into_parts(self) -> (WebhookParseErrorType, Option<Box<dyn Error + Send + Sync>>) {
38 (self.kind, self.source)
39 }
40}
41
42impl Display for WebhookParseError {
43 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
44 match self.kind {
45 WebhookParseErrorType::IdInvalid => f.write_str("url path segment isn't a valid ID"),
46 WebhookParseErrorType::SegmentMissing => {
47 f.write_str("url is missing a required path segment")
48 }
49 }
50 }
51}
52
53impl Error for WebhookParseError {
54 fn source(&self) -> Option<&(dyn Error + 'static)> {
55 self.source
56 .as_ref()
57 .map(|source| &**source as &(dyn Error + 'static))
58 }
59}
60
61#[derive(Debug)]
63#[non_exhaustive]
64pub enum WebhookParseErrorType {
65 IdInvalid,
67 SegmentMissing,
69}
70
71pub fn parse(url: &str) -> Result<(Id<WebhookMarker>, Option<&str>), WebhookParseError> {
118 let mut segments = {
119 let mut start = url.split("discord.com/api/webhooks/");
120 let path = start.nth(1).ok_or(WebhookParseError {
121 kind: WebhookParseErrorType::SegmentMissing,
122 source: None,
123 })?;
124
125 path.split('/')
126 };
127
128 let id_segment = segments.next().ok_or(WebhookParseError {
129 kind: WebhookParseErrorType::SegmentMissing,
130 source: None,
131 })?;
132
133 if id_segment.is_empty() {
135 return Err(WebhookParseError {
136 kind: WebhookParseErrorType::SegmentMissing,
137 source: None,
138 });
139 }
140
141 let id = id_segment
142 .parse::<NonZero<u64>>()
143 .map_err(|source| WebhookParseError {
144 kind: WebhookParseErrorType::IdInvalid,
145 source: Some(Box::new(source)),
146 })?;
147 let mut token = segments.next();
148
149 if token.is_some_and(str::is_empty) {
151 token = None;
152 }
153
154 Ok((Id::from(id), token))
155}
156
157#[cfg(test)]
158mod tests {
159 use super::{WebhookParseError, WebhookParseErrorType};
160 use static_assertions::assert_impl_all;
161 use std::{error::Error, fmt::Debug};
162 use twilight_model::id::Id;
163
164 assert_impl_all!(WebhookParseErrorType: Debug, Send, Sync);
165 assert_impl_all!(WebhookParseError: Debug, Error, Send, Sync);
166
167 #[test]
168 fn parse_no_token() {
169 assert_eq!(
170 (Id::new(123), None),
171 super::parse("https://discord.com/api/webhooks/123").unwrap(),
172 );
173 assert_eq!(
176 (Id::new(123), None),
177 super::parse("https://discord.com/api/webhooks/123").unwrap(),
178 );
179 assert!(
180 super::parse("https://discord.com/api/webhooks/123/")
181 .unwrap()
182 .1
183 .is_none()
184 );
185 }
186
187 #[test]
188 fn parse_with_token() {
189 assert_eq!(
190 super::parse("https://discord.com/api/webhooks/456/token").unwrap(),
191 (Id::new(456), Some("token")),
192 );
193 assert_eq!(
195 super::parse("https://discord.com/api/webhooks/456/token/github").unwrap(),
196 (Id::new(456), Some("token")),
197 );
198 assert_eq!(
199 super::parse("https://discord.com/api/webhooks/456/token/slack").unwrap(),
200 (Id::new(456), Some("token")),
201 );
202 assert_eq!(
203 super::parse("https://discord.com/api/webhooks/456/token/randomsegment").unwrap(),
204 (Id::new(456), Some("token")),
205 );
206 assert_eq!(
207 super::parse("https://discord.com/api/webhooks/456/token/one/two/three").unwrap(),
208 (Id::new(456), Some("token")),
209 );
210 }
211
212 #[test]
213 fn parse_invalid() {
214 assert!(matches!(
216 super::parse("https://discord.com/foo/bar/456")
217 .unwrap_err()
218 .kind(),
219 &WebhookParseErrorType::SegmentMissing,
220 ));
221 assert!(matches!(
223 super::parse("https://discord.com/api/webhooks/")
224 .unwrap_err()
225 .kind(),
226 &WebhookParseErrorType::SegmentMissing,
227 ));
228 assert!(matches!(
230 super::parse("https://discord.com/api/webhooks/notaninteger")
231 .unwrap_err()
232 .kind(),
233 &WebhookParseErrorType::IdInvalid,
234 ));
235 }
236}