use chrono::{DateTime, Utc};
use sha2::{Digest, Sha256};
pub fn generate_hash_id_with_collision_check<F>(
prefix: &str,
title: &str,
description: &str,
timestamp: DateTime<Utc>,
estimated_db_size: usize,
mut collision_check: F,
) -> anyhow::Result<String>
where
F: FnMut(&str) -> bool,
{
let creator = "user";
let initial_length = if estimated_db_size < 10 {
4
} else if estimated_db_size < 100 {
5
} else if estimated_db_size < 1000 {
6
} else if estimated_db_size < 10000 {
7
} else {
8
};
for length in initial_length..=8 {
for nonce in 0..10 {
let candidate = generate_hash_id(
prefix,
title,
description,
creator,
timestamp,
length,
nonce,
);
if !collision_check(&candidate) {
return Ok(candidate);
}
}
}
anyhow::bail!(
"Failed to generate unique hash ID after trying all lengths and nonces (database has ~{} issues)",
estimated_db_size
)
}
pub fn generate_hash_id(
prefix: &str,
title: &str,
description: &str,
creator: &str,
timestamp: DateTime<Utc>,
length: usize,
nonce: u32,
) -> String {
let content = format!(
"{}|{}|{}|{}|{}",
title,
description,
creator,
timestamp.timestamp_nanos_opt().unwrap_or(0),
nonce
);
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
let hash_result = hasher.finalize();
let short_hash = match length {
4 => {
format!("{:02x}{:02x}", hash_result[0], hash_result[1])
}
5 => {
let three_byte_hex = format!(
"{:02x}{:02x}{:02x}",
hash_result[0], hash_result[1], hash_result[2]
);
three_byte_hex[..5].to_string()
}
6 => {
format!(
"{:02x}{:02x}{:02x}",
hash_result[0], hash_result[1], hash_result[2]
)
}
7 => {
let four_byte_hex = format!(
"{:02x}{:02x}{:02x}{:02x}",
hash_result[0], hash_result[1], hash_result[2], hash_result[3]
);
four_byte_hex[..7].to_string()
}
8 => {
format!(
"{:02x}{:02x}{:02x}{:02x}",
hash_result[0], hash_result[1], hash_result[2], hash_result[3]
)
}
_ => {
format!(
"{:02x}{:02x}{:02x}",
hash_result[0], hash_result[1], hash_result[2]
)
}
};
format!("{}-{}", prefix, short_hash)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_generate_hash_id_basic() {
let timestamp = Utc.with_ymd_and_hms(2025, 10, 31, 12, 0, 0).unwrap();
let id = generate_hash_id(
"test",
"First issue",
"Test description",
"user",
timestamp,
4,
0,
);
assert!(id.starts_with("test-"));
assert_eq!(id.len(), "test-".len() + 4); }
#[test]
fn test_generate_hash_id_deterministic() {
let timestamp = Utc.with_ymd_and_hms(2025, 10, 31, 12, 0, 0).unwrap();
let id1 = generate_hash_id(
"test",
"First issue",
"Test description",
"user",
timestamp,
4,
0,
);
let id2 = generate_hash_id(
"test",
"First issue",
"Test description",
"user",
timestamp,
4,
0,
);
assert_eq!(id1, id2);
}
#[test]
fn test_generate_hash_id_different_nonce() {
let timestamp = Utc.with_ymd_and_hms(2025, 10, 31, 12, 0, 0).unwrap();
let id1 = generate_hash_id(
"test",
"First issue",
"Test description",
"user",
timestamp,
4,
0,
);
let id2 = generate_hash_id(
"test",
"First issue",
"Test description",
"user",
timestamp,
4,
1,
);
assert_ne!(id1, id2);
}
#[test]
fn test_generate_hash_id_different_lengths() {
let timestamp = Utc.with_ymd_and_hms(2025, 10, 31, 12, 0, 0).unwrap();
for length in 4..=8 {
let id = generate_hash_id(
"test",
"First issue",
"Test description",
"user",
timestamp,
length,
0,
);
assert!(id.starts_with("test-"));
assert_eq!(id.len(), "test-".len() + length);
}
}
#[test]
fn test_generate_hash_id_different_inputs() {
let timestamp = Utc.with_ymd_and_hms(2025, 10, 31, 12, 0, 0).unwrap();
let id1 = generate_hash_id(
"test",
"First issue",
"Test description",
"user",
timestamp,
4,
0,
);
let id2 = generate_hash_id(
"test",
"Second issue", "Test description",
"user",
timestamp,
4,
0,
);
assert_ne!(id1, id2);
}
}