use std::str::FromStr;
use nu_protocol::{Record, Span, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
pub const ULID_STRING_LENGTH: usize = 26;
pub const MAX_BULK_GENERATION: usize = 10_000;
pub const CROCKFORD_BASE32_CHARSET: &str = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
pub const NANOS_PER_MILLI: u64 = 1_000_000;
pub const MS_PER_SECOND: u64 = 1_000;
pub const ULID_TIMESTAMP_CHARS: usize = 10;
pub const ULID_RANDOMNESS_CHARS: usize = 16;
const ULID_RANDOMNESS_MASK: u128 = 0xFFFF_FFFF_FFFF_FFFF_FFFF;
pub struct UlidEngine;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UlidComponents {
pub ulid: String,
pub timestamp_ms: u64,
pub randomness_hex: String,
pub valid: bool,
}
impl UlidEngine {
pub fn generate() -> Result<Ulid, UlidError> {
Ok(Ulid::new())
}
pub fn generate_with_timestamp(timestamp_ms: u64) -> Result<Ulid, UlidError> {
let ulid = Ulid::from_parts(timestamp_ms, rand::random::<u128>() & ULID_RANDOMNESS_MASK);
Ok(ulid)
}
pub fn generate_bulk(count: usize) -> Result<Vec<Ulid>, UlidError> {
if count == 0 {
return Ok(Vec::new());
}
if count > MAX_BULK_GENERATION {
return Err(UlidError::InvalidInput {
message: "Bulk generation limited to 10,000 ULIDs per request for performance"
.to_string(),
});
}
let mut result = Vec::with_capacity(count);
for _ in 0..count {
result.push(Ulid::new());
}
Ok(result)
}
pub fn parse(ulid_str: &str) -> Result<UlidComponents, UlidError> {
match Ulid::from_str(ulid_str) {
Ok(ulid) => {
let components = UlidComponents {
ulid: ulid_str.to_string(),
timestamp_ms: ulid.timestamp_ms(),
randomness_hex: format!("{:x}", ulid.random()),
valid: true,
};
Ok(components)
}
Err(e) => Err(UlidError::InvalidFormat {
input: ulid_str.to_string(),
reason: format!("Parse error: {}", e),
}),
}
}
#[must_use]
pub fn validate(ulid_str: &str) -> bool {
Ulid::from_str(ulid_str).is_ok()
}
pub fn extract_timestamp(ulid_str: &str) -> Result<u64, UlidError> {
match Ulid::from_str(ulid_str) {
Ok(ulid) => Ok(ulid.timestamp_ms()),
Err(e) => Err(UlidError::InvalidFormat {
input: ulid_str.to_string(),
reason: format!("Cannot extract timestamp: {}", e),
}),
}
}
pub fn extract_randomness(ulid_str: &str) -> Result<u128, UlidError> {
match Ulid::from_str(ulid_str) {
Ok(ulid) => Ok(ulid.random()),
Err(e) => Err(UlidError::InvalidFormat {
input: ulid_str.to_string(),
reason: format!("Cannot extract randomness: {}", e),
}),
}
}
pub fn to_bytes(ulid: &Ulid) -> Vec<u8> {
ulid.to_bytes().to_vec()
}
pub fn components_to_value(components: &UlidComponents, span: Span) -> Value {
let mut record = Record::new();
record.push("ulid", Value::string(components.ulid.clone(), span));
let mut timestamp_record = Record::new();
timestamp_record.push("ms", Value::int(components.timestamp_ms as i64, span));
let timestamp_secs = components.timestamp_ms / MS_PER_SECOND;
let timestamp_nanos = (components.timestamp_ms % MS_PER_SECOND) * NANOS_PER_MILLI;
if let Some(datetime) =
chrono::DateTime::from_timestamp(timestamp_secs as i64, timestamp_nanos as u32)
{
timestamp_record.push(
"iso8601",
Value::string(datetime.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), span),
);
timestamp_record.push("unix", Value::int(timestamp_secs as i64, span));
}
record.push("timestamp", Value::record(timestamp_record, span));
let mut randomness_record = Record::new();
randomness_record.push(
"hex",
Value::string(components.randomness_hex.clone(), span),
);
record.push("randomness", Value::record(randomness_record, span));
record.push("valid", Value::bool(components.valid, span));
Value::record(record, span)
}
}
#[derive(Debug, Clone)]
pub enum UlidError {
InvalidFormat {
input: String,
reason: String,
},
InvalidInput {
message: String,
},
TimestampOutOfRange {
timestamp: u64,
max_timestamp: u64,
},
GenerationError {
reason: String,
},
}
impl std::fmt::Display for UlidError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UlidError::InvalidFormat { input, reason } => {
write!(f, "Invalid ULID format '{}': {}", input, reason)
}
UlidError::InvalidInput { message } => {
write!(f, "Invalid input: {}", message)
}
UlidError::TimestampOutOfRange {
timestamp,
max_timestamp,
} => {
write!(
f,
"Timestamp {} is out of range (max: {})",
timestamp, max_timestamp
)
}
UlidError::GenerationError { reason } => {
write!(f, "ULID generation error: {}", reason)
}
}
}
}
impl std::error::Error for UlidError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ulid_generation() {
let ulid = UlidEngine::generate().unwrap();
assert_eq!(ulid.to_string().len(), ULID_STRING_LENGTH);
}
#[test]
fn test_ulid_validation() {
assert!(UlidEngine::validate("01AN4Z07BY79KA1307SR9X4MV3"));
assert!(!UlidEngine::validate("invalid"));
assert!(!UlidEngine::validate("01AN4Z07BY79KA1307SR9X4MV")); assert!(!UlidEngine::validate("01AN4Z07BY79KA1307SR9X4MV34")); }
#[test]
fn test_ulid_parsing() {
let ulid_str = "01AN4Z07BY79KA1307SR9X4MV3";
let components = UlidEngine::parse(ulid_str).unwrap();
assert_eq!(components.ulid, ulid_str);
assert!(components.valid);
assert_eq!(components.timestamp_ms, 1465824320894);
}
#[test]
fn test_bulk_generation() {
let ulids = UlidEngine::generate_bulk(10).unwrap();
assert_eq!(ulids.len(), 10);
let unique_count = ulids
.iter()
.map(|u| u.to_string())
.collect::<std::collections::HashSet<_>>()
.len();
assert_eq!(unique_count, 10);
}
#[test]
fn test_timestamp_extraction() {
let ulid_str = "01AN4Z07BY79KA1307SR9X4MV3";
let timestamp = UlidEngine::extract_timestamp(ulid_str).unwrap();
assert_eq!(timestamp, 1465824320894);
}
#[test]
fn test_bulk_generation_limit() {
let result = UlidEngine::generate_bulk(10_001);
assert!(result.is_err());
if let Err(UlidError::InvalidInput { message }) = result {
assert!(message.contains("10,000"));
}
}
}