use crate::core::encoding::{EncodingFormat, encode_base64, encode_bits, encode_hex};
use crate::core::error::{IdtError, Result};
use crate::core::id::{
IdEncodings, IdGenerator, IdKind, InspectionResult, ParsedId, Timestamp, ValidationResult,
};
use serde_json::json;
use std::sync::atomic::{AtomicU64, Ordering};
pub const TWITTER_EPOCH: u64 = 1288834974657;
pub const DISCORD_EPOCH: u64 = 1420070400000;
pub const DEFAULT_EPOCH: u64 = 0;
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
static LAST_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub struct SnowflakeGenerator {
pub epoch: u64,
pub machine_id: u16,
pub datacenter_id: u16,
}
impl Default for SnowflakeGenerator {
fn default() -> Self {
Self {
epoch: DEFAULT_EPOCH,
machine_id: 0,
datacenter_id: 0,
}
}
}
impl SnowflakeGenerator {
pub fn new() -> Self {
Self::default()
}
pub fn twitter() -> Self {
Self {
epoch: TWITTER_EPOCH,
machine_id: 0,
datacenter_id: 0,
}
}
pub fn discord() -> Self {
Self {
epoch: DISCORD_EPOCH,
machine_id: 0,
datacenter_id: 0,
}
}
pub fn with_epoch(mut self, epoch: u64) -> Self {
self.epoch = epoch;
self
}
pub fn with_machine_id(mut self, machine_id: u16) -> Self {
self.machine_id = machine_id & 0x1F; self
}
pub fn with_datacenter_id(mut self, datacenter_id: u16) -> Self {
self.datacenter_id = datacenter_id & 0x1F; self
}
fn current_timestamp(&self) -> u64 {
chrono::Utc::now().timestamp_millis() as u64 - self.epoch
}
fn next_sequence(&self, timestamp: u64) -> u64 {
let last = LAST_TIMESTAMP.swap(timestamp, Ordering::SeqCst);
if timestamp == last {
SEQUENCE.fetch_add(1, Ordering::SeqCst) & 0xFFF
} else {
SEQUENCE.store(1, Ordering::SeqCst);
0
}
}
}
impl IdGenerator for SnowflakeGenerator {
fn generate(&self) -> Result<String> {
let timestamp = self.current_timestamp();
let sequence = self.next_sequence(timestamp);
let id: u64 = (timestamp << 22)
| ((self.datacenter_id as u64 & 0x1F) << 17)
| ((self.machine_id as u64 & 0x1F) << 12)
| (sequence & 0xFFF);
Ok(id.to_string())
}
}
pub struct ParsedSnowflake {
id: u64,
epoch: u64,
input: String,
}
impl ParsedSnowflake {
pub fn parse(input: &str) -> Result<Self> {
Self::parse_with_epoch(input, DEFAULT_EPOCH)
}
pub fn parse_twitter(input: &str) -> Result<Self> {
Self::parse_with_epoch(input, TWITTER_EPOCH)
}
pub fn parse_discord(input: &str) -> Result<Self> {
Self::parse_with_epoch(input, DISCORD_EPOCH)
}
pub fn parse_with_epoch(input: &str, epoch: u64) -> Result<Self> {
let input_trimmed = input.trim();
let id = input_trimmed
.parse::<u64>()
.map_err(|e| IdtError::ParseError(format!("Invalid Snowflake ID: {}", e)))?;
Ok(Self {
id,
epoch,
input: input_trimmed.to_string(),
})
}
pub fn timestamp_ms(&self) -> u64 {
(self.id >> 22) + self.epoch
}
pub fn datacenter_id(&self) -> u16 {
((self.id >> 17) & 0x1F) as u16
}
pub fn machine_id(&self) -> u16 {
((self.id >> 12) & 0x1F) as u16
}
pub fn sequence(&self) -> u16 {
(self.id & 0xFFF) as u16
}
}
impl ParsedId for ParsedSnowflake {
fn kind(&self) -> IdKind {
IdKind::Snowflake
}
fn canonical(&self) -> String {
self.id.to_string()
}
fn as_bytes(&self) -> Vec<u8> {
self.id.to_be_bytes().to_vec()
}
fn timestamp(&self) -> Option<Timestamp> {
Some(Timestamp::new(self.timestamp_ms()))
}
fn inspect(&self) -> InspectionResult {
let bytes = self.as_bytes();
let timestamp = self.timestamp().unwrap();
let components = json!({
"timestamp_ms": self.timestamp_ms(),
"datacenter_id": self.datacenter_id(),
"machine_id": self.machine_id(),
"sequence": self.sequence(),
"epoch": self.epoch,
});
InspectionResult {
id_type: "snowflake".to_string(),
input: self.input.clone(),
canonical: self.canonical(),
valid: true,
timestamp: Some(timestamp),
timestamp_iso: Some(timestamp.to_iso8601()),
timestamp_local_iso: None,
version: None,
variant: if self.epoch == TWITTER_EPOCH {
Some("Twitter".to_string())
} else if self.epoch == DISCORD_EPOCH {
Some("Discord".to_string())
} else {
Some("Custom".to_string())
},
random_bits: None, components: Some(components),
encodings: IdEncodings {
hex: encode_hex(&bytes),
base32: String::new(),
base58: String::new(),
base64: encode_base64(&bytes),
int: Some(self.id.to_string()),
},
}
}
fn validate(&self) -> ValidationResult {
let ts = self.timestamp_ms();
let now = chrono::Utc::now().timestamp_millis() as u64;
if ts > now + 86400000 {
ValidationResult::invalid("Timestamp is in the future")
} else {
ValidationResult::valid("snowflake")
}
}
fn encode(&self, format: EncodingFormat) -> String {
let bytes = self.as_bytes();
match format {
EncodingFormat::Canonical => self.canonical(),
EncodingFormat::Hex => encode_hex(&bytes),
EncodingFormat::Base64 => encode_base64(&bytes),
EncodingFormat::Bits => encode_bits(&bytes),
EncodingFormat::Int => self.id.to_string(),
_ => self.canonical(),
}
}
}
pub fn is_snowflake(input: &str) -> bool {
let input = input.trim();
if input.is_empty() {
return false;
}
if !input.chars().all(|c| c.is_ascii_digit()) {
return false;
}
let len = input.len();
(15..=19).contains(&len) && input.parse::<u64>().is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate() {
let generator = SnowflakeGenerator::new();
let id = generator.generate().unwrap();
assert!(is_snowflake(&id));
}
#[test]
fn test_generate_twitter() {
let generator = SnowflakeGenerator::twitter().with_machine_id(1);
let id = generator.generate().unwrap();
let parsed = ParsedSnowflake::parse_twitter(&id).unwrap();
assert_eq!(parsed.machine_id(), 1);
}
#[test]
fn test_parse_components() {
let id = "1234567890123456789";
let parsed = ParsedSnowflake::parse(id).unwrap();
assert!(parsed.timestamp_ms() > 0);
assert!(parsed.datacenter_id() < 32);
assert!(parsed.machine_id() < 32);
assert!(parsed.sequence() < 4096);
}
}