use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
num::NonZero,
};
use twilight_model::id::{Id, marker::WebhookMarker};
#[derive(Debug)]
pub struct WebhookParseError {
kind: WebhookParseErrorType,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl WebhookParseError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &WebhookParseErrorType {
&self.kind
}
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
self.source
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(self) -> (WebhookParseErrorType, Option<Box<dyn Error + Send + Sync>>) {
(self.kind, self.source)
}
}
impl Display for WebhookParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self.kind {
WebhookParseErrorType::IdInvalid => f.write_str("url path segment isn't a valid ID"),
WebhookParseErrorType::SegmentMissing => {
f.write_str("url is missing a required path segment")
}
}
}
}
impl Error for WebhookParseError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| &**source as &(dyn Error + 'static))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum WebhookParseErrorType {
IdInvalid,
SegmentMissing,
}
pub fn parse(url: &str) -> Result<(Id<WebhookMarker>, Option<&str>), WebhookParseError> {
let mut segments = {
let mut start = url.split("discord.com/api/webhooks/");
let path = start.nth(1).ok_or(WebhookParseError {
kind: WebhookParseErrorType::SegmentMissing,
source: None,
})?;
path.split('/')
};
let id_segment = segments.next().ok_or(WebhookParseError {
kind: WebhookParseErrorType::SegmentMissing,
source: None,
})?;
if id_segment.is_empty() {
return Err(WebhookParseError {
kind: WebhookParseErrorType::SegmentMissing,
source: None,
});
}
let id = id_segment
.parse::<NonZero<u64>>()
.map_err(|source| WebhookParseError {
kind: WebhookParseErrorType::IdInvalid,
source: Some(Box::new(source)),
})?;
let mut token = segments.next();
if token.is_some_and(str::is_empty) {
token = None;
}
Ok((Id::from(id), token))
}
#[cfg(test)]
mod tests {
use super::{WebhookParseError, WebhookParseErrorType};
use static_assertions::assert_impl_all;
use std::{error::Error, fmt::Debug};
use twilight_model::id::Id;
assert_impl_all!(WebhookParseErrorType: Debug, Send, Sync);
assert_impl_all!(WebhookParseError: Debug, Error, Send, Sync);
#[test]
fn parse_no_token() {
assert_eq!(
(Id::new(123), None),
super::parse("https://discord.com/api/webhooks/123").unwrap(),
);
assert_eq!(
(Id::new(123), None),
super::parse("https://discord.com/api/webhooks/123").unwrap(),
);
assert!(
super::parse("https://discord.com/api/webhooks/123/")
.unwrap()
.1
.is_none()
);
}
#[test]
fn parse_with_token() {
assert_eq!(
super::parse("https://discord.com/api/webhooks/456/token").unwrap(),
(Id::new(456), Some("token")),
);
assert_eq!(
super::parse("https://discord.com/api/webhooks/456/token/github").unwrap(),
(Id::new(456), Some("token")),
);
assert_eq!(
super::parse("https://discord.com/api/webhooks/456/token/slack").unwrap(),
(Id::new(456), Some("token")),
);
assert_eq!(
super::parse("https://discord.com/api/webhooks/456/token/randomsegment").unwrap(),
(Id::new(456), Some("token")),
);
assert_eq!(
super::parse("https://discord.com/api/webhooks/456/token/one/two/three").unwrap(),
(Id::new(456), Some("token")),
);
}
#[test]
fn parse_invalid() {
assert!(matches!(
super::parse("https://discord.com/foo/bar/456")
.unwrap_err()
.kind(),
&WebhookParseErrorType::SegmentMissing,
));
assert!(matches!(
super::parse("https://discord.com/api/webhooks/")
.unwrap_err()
.kind(),
&WebhookParseErrorType::SegmentMissing,
));
assert!(matches!(
super::parse("https://discord.com/api/webhooks/notaninteger")
.unwrap_err()
.kind(),
&WebhookParseErrorType::IdInvalid,
));
}
}