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, Eq, Hash, Ord, PartialEq, PartialOrd)]
141#[repr(Rust, packed)]
142pub(crate) struct InnerId(NonZeroU64);
143
144impl fmt::Debug for InnerId {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        let inner = self.0;
147        inner.fmt(f)
148    }
149}
150
151struct SnowflakeVisitor;
152
153impl serde::de::Visitor<'_> for SnowflakeVisitor {
154    type Value = InnerId;
155
156    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        formatter.write_str("a string or integer snowflake that is not u64::MAX")
158    }
159
160    // Called by formats like TOML.
161    fn visit_i64<E: Error>(self, value: i64) -> Result<Self::Value, E> {
162        self.visit_u64(u64::try_from(value).map_err(Error::custom)?)
163    }
164
165    fn visit_u64<E: Error>(self, value: u64) -> Result<Self::Value, E> {
166        NonZeroU64::new(value)
167            .map(InnerId)
168            .ok_or_else(|| Error::custom("invalid value, expected non-max"))
169    }
170
171    fn visit_str<E: Error>(self, value: &str) -> Result<Self::Value, E> {
172        value.parse().map(InnerId).map_err(Error::custom)
173    }
174}
175
176impl<'de> serde::Deserialize<'de> for InnerId {
177    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<InnerId, D::Error> {
178        deserializer.deserialize_any(SnowflakeVisitor)
179    }
180}
181
182impl serde::Serialize for InnerId {
183    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
184        serializer.collect_str(&{ self.0 })
185    }
186}
187
188id_u64! {
189    AttachmentId: "An identifier for an attachment.";
190    ApplicationId: "An identifier for an Application.";
191    ChannelId: "An identifier for a Channel";
192    EmojiId: "An identifier for an Emoji";
193    GenericId: "An identifier for an unspecific entity.";
194    GuildId: "An identifier for a Guild";
195    IntegrationId: "An identifier for an Integration";
196    MessageId: "An identifier for a Message";
197    RoleId: "An identifier for a Role";
198    ScheduledEventId: "An identifier for a Scheduled Event";
199    SoundId: "An identifier for a Soundboard sound";
200    StickerId: "An identifier for a sticker.";
201    StickerPackId: "An identifier for a sticker pack.";
202    StickerPackBannerId: "An identifier for a sticker pack banner.";
203    SkuId: "An identifier for a SKU.";
204    UserId: "An identifier for a User";
205    WebhookId: "An identifier for a [`Webhook`]";
206    AuditLogEntryId: "An identifier for an audit log entry.";
207    InteractionId: "An identifier for an interaction.";
208    CommandId: "An identifier for a slash command.";
209    CommandPermissionId: "An identifier for a slash command permission Id.";
210    CommandVersionId: "An identifier for a slash command version Id.";
211    TargetId: "An identifier for a slash command target Id.";
212    StageInstanceId: "An identifier for a stage channel instance.";
213    RuleId: "An identifier for an auto moderation rule";
214    ForumTagId: "An identifier for a forum tag.";
215    EntitlementId: "An identifier for an entitlement.";
216}
217
218/// An identifier for a Shard.
219///
220/// This identifier is special, it simply models internal IDs for type safety,
221/// and therefore cannot be [`Serialize`]d or [`Deserialize`]d.
222#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
223#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
224pub struct ShardId(pub u32);
225
226impl ShardId {
227    /// Retrieves the value as a [`u32`].
228    ///
229    /// This is not a [`u64`] as [`ShardId`]s are not a discord concept and are simply used for
230    /// internal type safety.
231    #[must_use]
232    pub fn get(self) -> u32 {
233        self.0
234    }
235}
236
237newtype_display_impl!(ShardId, |this| this.0);
238
239/// An identifier for a [`Poll Answer`](super::channel::PollAnswer).
240///
241/// This is identifier is special as it is not a snowflake.
242///
243/// The specific algorithm used is currently just a sequential index but this is subject to change.
244#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
245#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
246#[repr(Rust, packed)]
247pub struct AnswerId(u8);
248
249impl AnswerId {
250    /// Retrieves the value as a [`u64`].
251    ///
252    /// Keep in mind that this is **not a snowflake** and the values are subject to change.
253    #[must_use]
254    pub fn get(self) -> u64 {
255        self.0.into()
256    }
257}
258
259newtype_display_impl!(AnswerId, |this| this.0);
260forward_fromstr_impl!(AnswerId, std::convert::identity);
261
262#[cfg(test)]
263mod tests {
264    use std::num::NonZeroU64;
265
266    use super::{GuildId, InnerId};
267
268    #[test]
269    fn test_created_at() {
270        // The id is from discord's snowflake docs
271        let id = GuildId::new(175928847299117063);
272        assert_eq!(id.created_at().unix_timestamp(), 1462015105);
273        assert_eq!(id.created_at().to_string(), "2016-04-30T11:18:25.796Z");
274    }
275
276    #[test]
277    fn test_id_serde() {
278        use serde::{Deserialize, Serialize};
279
280        use crate::json::{assert_json, json};
281
282        #[derive(Debug, PartialEq, Deserialize, Serialize)]
283        struct S {
284            id: InnerId,
285        }
286
287        #[derive(Debug, PartialEq, Deserialize, Serialize)]
288        struct Opt {
289            id: Option<GuildId>,
290        }
291
292        let id = GuildId::new(17_5928_8472_9911_7063);
293        assert_json(&id, json!("175928847299117063"));
294
295        let s = S {
296            id: InnerId(NonZeroU64::new(17_5928_8472_9911_7063).unwrap()),
297        };
298        assert_json(&s, json!({"id": "175928847299117063"}));
299
300        let s = Opt {
301            id: Some(GuildId::new(17_5928_8472_9911_7063)),
302        };
303        assert_json(&s, json!({"id": "175928847299117063"}));
304    }
305}