///|
/// A Discord snowflake ID, tagged with a phantom marker type so that IDs of
/// different resources cannot be mixed up (`Id[UserMarker]` vs `Id[GuildMarker]`).
///
/// Wire format: Discord serializes snowflakes as decimal strings (and
/// historically as integers in a few places). We accept both on decode and
/// always emit a string, going through the string representation so values
/// above 2^53 never pass through a `Double`.
///
/// ```mbt check
/// test "parse and explicitly retag a snowflake" {
/// let user_id : @model.UserId = @model.Id::parse("175928847299117063")
/// inspect(user_id.to_string(), content="175928847299117063")
/// inspect(user_id.timestamp_ms(), content="1462015105796")
/// let guild_id : @model.GuildId = user_id.cast()
/// assert_eq(guild_id.value(), user_id.value())
/// }
/// ```
#warnings("-unused_type_variable")
pub(all) struct Id[M](UInt64)
///|
/// The raw snowflake value.
pub fn[M] Id::value(self : Id[M]) -> UInt64 {
self.0
}
///|
/// Re-tag an ID with a different marker, for the few places where Discord
/// aliases ID domains (e.g. the `@everyone` role ID equals the guild ID).
pub fn[M, N] Id::cast(self : Id[M]) -> Id[N] {
Id(self.0)
}
///|
/// Milliseconds since the Unix epoch encoded in the snowflake.
pub fn[M] Id::timestamp_ms(self : Id[M]) -> Int64 {
(self.0 >> 22).reinterpret_as_int64() + 1420070400000L
}
///|
/// Parse a decimal snowflake string.
pub fn[M] Id::parse(s : StringView) -> Id[M] raise {
Id(@string.parse_uint64(s))
}
///|
pub impl[M] Eq for Id[M] with fn equal(a, b) {
a.0 == b.0
}
///|
pub impl[M] Compare for Id[M] with fn compare(a, b) {
a.0.compare(b.0)
}
///|
pub impl[M] Hash for Id[M] with fn hash_combine(self, hasher) {
Hash::hash_combine(self.0, hasher)
}
///|
pub impl[M] Show for Id[M] with fn output(self, logger) {
logger.write_string(self.0.to_string())
}
///|
pub impl[M] Debug for Id[M] with fn to_repr(self) {
Repr(self.0)
}
///|
pub impl[M] ToJson for Id[M] with fn to_json(self) {
Json::string(self.0.to_string())
}
///|
pub impl[M] @json.FromJson for Id[M] with fn from_json(json, path) {
match json {
String(s) =>
Id(
@string.parse_uint64(s) catch {
_ =>
raise JsonDecodeError(
(path, "expected a snowflake (decimal string), got \{s}"),
)
},
)
Number(_, repr=Some(r)) =>
// integer form: parse the original decimal text, not the Double,
// so values above 2^53 keep full precision
Id(
@string.parse_uint64(r.to_string()) catch {
_ =>
raise JsonDecodeError(
(path, "expected a snowflake (integer), got \{r}"),
)
},
)
Number(n, repr=None) => Id(n.to_uint64())
_ => raise JsonDecodeError((path, "expected a snowflake"))
}
}