serenity/model/
id.rs

1//! A collection of newtypes defining type-strong IDs.
2
3use std::fmt;
4use std::num::{NonZeroI64, NonZeroU64};
5
6use serde::de::Error;
7
8use super::prelude::*;
9
10macro_rules! newtype_display_impl {
11    ($name:ident, |$this:ident| $inner:expr) => {
12        impl fmt::Display for $name {
13            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14                fmt::Display::fmt(&(|$this: $name| $inner)(*self), f)
15            }
16        }
17    };
18}
19
20macro_rules! forward_fromstr_impl {
21    ($name:ident, $wrapper:path) => {
22        impl std::str::FromStr for $name {
23            type Err = <u64 as std::str::FromStr>::Err;
24
25            fn from_str(s: &str) -> Result<Self, Self::Err> {
26                Ok(Self($wrapper(s.parse()?)))
27            }
28        }
29    };
30}
31
32macro_rules! id_u64 {
33    ($($name:ident: $doc:literal;)*) => {
34        $(
35            #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
36            #[doc = $doc]
37            pub struct $name(InnerId);
38
39            impl $name {
40                #[doc = concat!("Creates a new ", stringify!($name), " from a u64.")]
41                /// # Panics
42                /// Panics if `id` is zero.
43                #[inline]
44                #[must_use]
45                #[track_caller]
46                pub const fn new(id: u64) -> Self {
47                    match NonZeroU64::new(id) {
48                        Some(inner) => Self(InnerId(inner)),
49                        None => panic!(concat!("Attempted to call ", stringify!($name), "::new with invalid (0) value"))
50                    }
51                }
52
53                /// Retrieves the inner `id` as a [`u64`].
54                #[inline]
55                #[must_use]
56                pub const fn get(self) -> u64 {
57                    self.0.0.get()
58                }
59
60                #[doc = concat!("Retrieves the time that the ", stringify!($name), " was created.")]
61                #[must_use]
62                pub fn created_at(&self) -> Timestamp {
63                    Timestamp::from_discord_id(self.get())
64                }
65            }
66
67            impl Default for $name {
68                fn default() -> Self {
69                    Self(InnerId(NonZeroU64::MIN))
70                }
71            }
72
73            // This is a hack so functions can accept iterators that either:
74            // 1. return the id itself (e.g: `MessageId`)
75            // 2. return a reference to it (`&MessageId`).
76            impl AsRef<$name> for $name {
77                fn as_ref(&self) -> &Self {
78                    self
79                }
80            }
81
82            impl<'a> From<&'a $name> for $name {
83                fn from(id: &'a $name) -> $name {
84                    id.clone()
85                }
86            }
87
88            impl From<u64> for $name {
89                fn from(id: u64) -> $name {
90                    $name::new(id)
91                }
92            }
93
94            impl From<NonZeroU64> for $name {
95                fn from(id: NonZeroU64) -> $name {
96                    $name(InnerId(id))
97                }
98            }
99
100            impl PartialEq<u64> for $name {
101                fn eq(&self, u: &u64) -> bool {
102                    self.get() == *u
103                }
104            }
105
106            impl From<$name> for NonZeroU64 {
107                fn from(id: $name) -> NonZeroU64 {
108                    id.0.0
109                }
110            }
111
112            impl From<$name> for NonZeroI64 {
113                fn from(id: $name) -> NonZeroI64 {
114                    unsafe {NonZeroI64::new_unchecked(id.get() as i64)}
115                }
116            }
117
118            impl From<$name> for u64 {
119                fn from(id: $name) -> u64 {
120                    id.get()
121                }
122            }
123
124            impl From<$name> for i64 {
125                fn from(id: $name) -> i64 {
126                    id.get() as i64
127                }
128            }
129
130            newtype_display_impl!($name, |this| this.0.0);
131            forward_fromstr_impl!($name, InnerId);
132
133            #[cfg(feature = "typesize")]
134            impl typesize::TypeSize for $name {}
135        )*
136    }
137}
138
139/// The inner storage of an ID.
140#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
141#[repr(packed)]
142pub(crate) struct InnerId(NonZeroU64);
143
144struct SnowflakeVisitor;
145
146impl serde::de::Visitor<'_> for SnowflakeVisitor {
147    type Value = InnerId;
148
149    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150        formatter.write_str("a string or integer snowflake that is not u64::MAX")
151    }
152
153    // Called by formats like TOML.
154    fn visit_i64<E: Error>(self, value: i64) -> Result<Self::Value, E> {
155        self.visit_u64(u64::try_from(value).map_err(Error::custom)?)
156    }
157
158    fn visit_u64<E: Error>(self, value: u64) -> Result<Self::Value, E> {
159        NonZeroU64::new(value)
160            .map(InnerId)
161            .ok_or_else(|| Error::custom("invalid value, expected non-max"))
162    }
163
164    fn visit_str<E: Error>(self, value: &str) -> Result<Self::Value, E> {
165        value.parse().map(InnerId).map_err(Error::custom)
166    }
167}
168
169impl<'de> serde::Deserialize<'de> for InnerId {
170    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<InnerId, D::Error> {
171        deserializer.deserialize_any(SnowflakeVisitor)
172    }
173}
174
175impl serde::Serialize for InnerId {
176    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
177        serializer.collect_str(&{ self.0 })
178    }
179}
180
181id_u64! {
182    AttachmentId: "An identifier for an attachment.";
183    ApplicationId: "An identifier for an Application.";
184    ChannelId: "An identifier for a Channel";
185    EmojiId: "An identifier for an Emoji";
186    GenericId: "An identifier for an unspecific entity.";
187    GuildId: "An identifier for a Guild";
188    IntegrationId: "An identifier for an Integration";
189    MessageId: "An identifier for a Message";
190    RoleId: "An identifier for a Role";
191    ScheduledEventId: "An identifier for a Scheduled Event";
192    StickerId: "An identifier for a sticker.";
193    StickerPackId: "An identifier for a sticker pack.";
194    StickerPackBannerId: "An identifier for a sticker pack banner.";
195    SkuId: "An identifier for a SKU.";
196    UserId: "An identifier for a User";
197    WebhookId: "An identifier for a [`Webhook`]";
198    AuditLogEntryId: "An identifier for an audit log entry.";
199    InteractionId: "An identifier for an interaction.";
200    CommandId: "An identifier for a slash command.";
201    CommandPermissionId: "An identifier for a slash command permission Id.";
202    CommandVersionId: "An identifier for a slash command version Id.";
203    TargetId: "An identifier for a slash command target Id.";
204    StageInstanceId: "An identifier for a stage channel instance.";
205    RuleId: "An identifier for an auto moderation rule";
206    ForumTagId: "An identifier for a forum tag.";
207    EntitlementId: "An identifier for an entitlement.";
208}
209
210/// An identifier for a Shard.
211///
212/// This identifier is special, it simply models internal IDs for type safety,
213/// and therefore cannot be [`Serialize`]d or [`Deserialize`]d.
214#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
215#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
216pub struct ShardId(pub u32);
217
218impl ShardId {
219    /// Retrieves the value as a [`u32`].
220    ///
221    /// This is not a [`u64`] as [`ShardId`]s are not a discord concept and are simply used for
222    /// internal type safety.
223    #[must_use]
224    pub fn get(self) -> u32 {
225        self.0
226    }
227}
228
229newtype_display_impl!(ShardId, |this| this.0);
230
231/// An identifier for a [`Poll Answer`](super::channel::PollAnswer).
232///
233/// This is identifier is special as it is not a snowflake.
234///
235/// The specific algorithm used is currently just a sequential index but this is subject to change.
236#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
237#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
238#[repr(packed)]
239pub struct AnswerId(u8);
240
241impl AnswerId {
242    /// Retrieves the value as a [`u64`].
243    ///
244    /// Keep in mind that this is **not a snowflake** and the values are subject to change.
245    #[must_use]
246    pub fn get(self) -> u64 {
247        self.0.into()
248    }
249}
250
251newtype_display_impl!(AnswerId, |this| this.0);
252forward_fromstr_impl!(AnswerId, std::convert::identity);
253
254#[cfg(test)]
255mod tests {
256    use std::num::NonZeroU64;
257
258    use super::{GuildId, InnerId};
259
260    #[test]
261    fn test_created_at() {
262        // The id is from discord's snowflake docs
263        let id = GuildId::new(175928847299117063);
264        assert_eq!(id.created_at().unix_timestamp(), 1462015105);
265        assert_eq!(id.created_at().to_string(), "2016-04-30T11:18:25.796Z");
266    }
267
268    #[test]
269    fn test_id_serde() {
270        use serde::{Deserialize, Serialize};
271
272        use crate::json::{assert_json, json};
273
274        #[derive(Debug, PartialEq, Deserialize, Serialize)]
275        struct S {
276            id: InnerId,
277        }
278
279        #[derive(Debug, PartialEq, Deserialize, Serialize)]
280        struct Opt {
281            id: Option<GuildId>,
282        }
283
284        let id = GuildId::new(17_5928_8472_9911_7063);
285        assert_json(&id, json!("175928847299117063"));
286
287        let s = S {
288            id: InnerId(NonZeroU64::new(17_5928_8472_9911_7063).unwrap()),
289        };
290        assert_json(&s, json!({"id": "175928847299117063"}));
291
292        let s = Opt {
293            id: Some(GuildId::new(17_5928_8472_9911_7063)),
294        };
295        assert_json(&s, json!({"id": "175928847299117063"}));
296    }
297}