Some checks failed
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
CI/CD Pipeline / Cleanup (push) Has been cancelled
330 lines
No EOL
9.5 KiB
Rust
330 lines
No EOL
9.5 KiB
Rust
//! ID generation utilities (enabled with "ids" feature)
|
|
|
|
#[cfg(feature = "ids")]
|
|
use uuid::Uuid;
|
|
|
|
/// UUID utilities
|
|
#[cfg(feature = "ids")]
|
|
pub mod uuid {
|
|
use super::*;
|
|
|
|
/// Generate new UUID v4
|
|
pub fn generate() -> String {
|
|
Uuid::new_v4().to_string()
|
|
}
|
|
|
|
/// Generate UUID v4 in simple format (no hyphens)
|
|
pub fn generate_simple() -> String {
|
|
Uuid::new_v4().simple().to_string()
|
|
}
|
|
|
|
/// Generate short ID (first 8 characters of UUID)
|
|
pub fn generate_short() -> String {
|
|
Uuid::new_v4().simple().to_string()[..8].to_string()
|
|
}
|
|
|
|
/// Generate very short ID (first 4 characters of UUID)
|
|
pub fn generate_tiny() -> String {
|
|
Uuid::new_v4().simple().to_string()[..4].to_string()
|
|
}
|
|
|
|
/// Validate UUID string
|
|
pub fn is_valid(id: &str) -> bool {
|
|
Uuid::parse_str(id).is_ok()
|
|
}
|
|
|
|
/// Parse UUID from string
|
|
pub fn parse(id: &str) -> Result<Uuid, uuid::Error> {
|
|
Uuid::parse_str(id)
|
|
}
|
|
|
|
/// Convert UUID to different formats
|
|
pub fn to_simple(id: &str) -> Result<String, uuid::Error> {
|
|
let uuid = Uuid::parse_str(id)?;
|
|
Ok(uuid.simple().to_string())
|
|
}
|
|
|
|
/// Convert simple UUID back to hyphenated format
|
|
pub fn to_hyphenated(simple_id: &str) -> Result<String, uuid::Error> {
|
|
if simple_id.len() != 32 {
|
|
return Err(uuid::Error::InvalidLength(simple_id.len()));
|
|
}
|
|
|
|
let formatted = format!(
|
|
"{}-{}-{}-{}-{}",
|
|
&simple_id[0..8],
|
|
&simple_id[8..12],
|
|
&simple_id[12..16],
|
|
&simple_id[16..20],
|
|
&simple_id[20..32]
|
|
);
|
|
|
|
Uuid::parse_str(&formatted)?;
|
|
Ok(formatted)
|
|
}
|
|
}
|
|
|
|
/// Slug utilities for human-readable IDs
|
|
pub mod slug {
|
|
use crate::utils::string;
|
|
|
|
/// Generate slug from title
|
|
pub fn from_title(title: &str) -> String {
|
|
string::to_kebab_case(title)
|
|
.chars()
|
|
.filter(|c| c.is_alphanumeric() || *c == '-')
|
|
.collect::<String>()
|
|
.trim_matches('-')
|
|
.to_string()
|
|
}
|
|
|
|
/// Generate unique slug by adding suffix if needed
|
|
pub fn make_unique<F>(base_slug: &str, exists_fn: F) -> String
|
|
where
|
|
F: Fn(&str) -> bool,
|
|
{
|
|
if !exists_fn(base_slug) {
|
|
return base_slug.to_string();
|
|
}
|
|
|
|
for i in 1..1000 {
|
|
let candidate = format!("{}-{}", base_slug, i);
|
|
if !exists_fn(&candidate) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
// Fallback: add UUID suffix
|
|
#[cfg(feature = "ids")]
|
|
{
|
|
format!("{}-{}", base_slug, uuid::generate_short())
|
|
}
|
|
#[cfg(not(feature = "ids"))]
|
|
{
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let timestamp = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis();
|
|
format!("{}-{}", base_slug, timestamp)
|
|
}
|
|
}
|
|
|
|
/// Validate slug format
|
|
pub fn is_valid(slug: &str) -> bool {
|
|
!slug.is_empty()
|
|
&& slug.len() <= 100
|
|
&& slug.chars().all(|c| c.is_alphanumeric() || c == '-')
|
|
&& !slug.starts_with('-')
|
|
&& !slug.ends_with('-')
|
|
&& !slug.contains("--")
|
|
}
|
|
|
|
/// Clean up slug to make it valid
|
|
pub fn clean(input: &str) -> String {
|
|
let mut cleaned = from_title(input);
|
|
|
|
// Remove multiple consecutive hyphens
|
|
while cleaned.contains("--") {
|
|
cleaned = cleaned.replace("--", "-");
|
|
}
|
|
|
|
// Trim hyphens from start and end
|
|
cleaned = cleaned.trim_matches('-').to_string();
|
|
|
|
// Truncate if too long
|
|
if cleaned.len() > 100 {
|
|
cleaned = cleaned[..100].trim_end_matches('-').to_string();
|
|
}
|
|
|
|
// Ensure not empty
|
|
if cleaned.is_empty() {
|
|
cleaned = "untitled".to_string();
|
|
}
|
|
|
|
cleaned
|
|
}
|
|
}
|
|
|
|
/// Content ID utilities
|
|
pub mod content {
|
|
use super::*;
|
|
|
|
/// Generate content ID from type and title
|
|
pub fn generate_id(content_type: &str, title: &str) -> String {
|
|
let slug = slug::clean(title);
|
|
format!("{}:{}", content_type, slug)
|
|
}
|
|
|
|
/// Generate content ID with timestamp
|
|
pub fn generate_id_with_timestamp(content_type: &str, title: &str) -> String {
|
|
let slug = slug::clean(title);
|
|
|
|
#[cfg(feature = "time")]
|
|
{
|
|
use crate::utils::time;
|
|
let timestamp = time::now_iso();
|
|
let date_part = timestamp.split('T').next().unwrap_or("unknown");
|
|
format!("{}:{}:{}", content_type, date_part, slug)
|
|
}
|
|
|
|
#[cfg(not(feature = "time"))]
|
|
{
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let timestamp = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs();
|
|
format!("{}:{}:{}", content_type, timestamp, slug)
|
|
}
|
|
}
|
|
|
|
/// Parse content ID into parts
|
|
pub fn parse_id(id: &str) -> Option<(String, String)> {
|
|
let parts: Vec<&str> = id.splitn(2, ':').collect();
|
|
if parts.len() == 2 {
|
|
Some((parts[0].to_string(), parts[1].to_string()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Validate content ID format
|
|
pub fn is_valid_id(id: &str) -> bool {
|
|
if let Some((content_type, slug_part)) = parse_id(id) {
|
|
!content_type.is_empty()
|
|
&& !slug_part.is_empty()
|
|
&& content_type.chars().all(|c| c.is_alphanumeric() || c == '_')
|
|
&& slug::is_valid(&slug_part.split(':').last().unwrap_or(""))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
// No-op implementations when ids feature is not enabled
|
|
#[cfg(not(feature = "ids"))]
|
|
pub mod uuid {
|
|
pub fn generate() -> String {
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let timestamp = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis();
|
|
format!("id-{}", timestamp)
|
|
}
|
|
|
|
pub fn generate_simple() -> String {
|
|
generate()
|
|
}
|
|
|
|
pub fn generate_short() -> String {
|
|
generate()[..8].to_string()
|
|
}
|
|
|
|
pub fn generate_tiny() -> String {
|
|
generate()[..4].to_string()
|
|
}
|
|
|
|
pub fn is_valid(_id: &str) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_slug_generation() {
|
|
assert_eq!(slug::from_title("Hello World"), "hello-world");
|
|
assert_eq!(slug::from_title("My Blog Post!"), "my-blog-post");
|
|
assert_eq!(slug::from_title("CamelCase Title"), "camel-case-title");
|
|
assert_eq!(slug::from_title("Test Spaces"), "test-spaces");
|
|
}
|
|
|
|
#[test]
|
|
fn test_slug_validation() {
|
|
assert!(slug::is_valid("hello-world"));
|
|
assert!(slug::is_valid("my-post-123"));
|
|
assert!(!slug::is_valid(""));
|
|
assert!(!slug::is_valid("-invalid"));
|
|
assert!(!slug::is_valid("invalid-"));
|
|
assert!(!slug::is_valid("double--hyphen"));
|
|
assert!(!slug::is_valid("with spaces"));
|
|
assert!(!slug::is_valid("with/slash"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_slug_cleaning() {
|
|
assert_eq!(slug::clean("--bad--start--"), "bad-start");
|
|
assert_eq!(slug::clean(""), "untitled");
|
|
assert_eq!(slug::clean("Good Title"), "good-title");
|
|
|
|
// Test truncation
|
|
let long_title = "a".repeat(150);
|
|
let cleaned = slug::clean(&long_title);
|
|
assert!(cleaned.len() <= 100);
|
|
assert!(!cleaned.ends_with('-'));
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_id_generation() {
|
|
let id = content::generate_id("blog", "My First Post");
|
|
assert_eq!(id, "blog:my-first-post");
|
|
|
|
let id_with_timestamp = content::generate_id_with_timestamp("blog", "My Post");
|
|
assert!(id_with_timestamp.starts_with("blog:"));
|
|
assert!(id_with_timestamp.ends_with(":my-post"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_id_parsing() {
|
|
let (content_type, slug) = content::parse_id("blog:my-post").unwrap();
|
|
assert_eq!(content_type, "blog");
|
|
assert_eq!(slug, "my-post");
|
|
|
|
let (content_type, slug) = content::parse_id("blog:2023-01-01:my-post").unwrap();
|
|
assert_eq!(content_type, "blog");
|
|
assert_eq!(slug, "2023-01-01:my-post");
|
|
|
|
assert!(content::parse_id("invalid-id").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_id_validation() {
|
|
assert!(content::is_valid_id("blog:my-post"));
|
|
assert!(content::is_valid_id("news_article:breaking-news"));
|
|
assert!(!content::is_valid_id(""));
|
|
assert!(!content::is_valid_id("blog"));
|
|
assert!(!content::is_valid_id(":my-post"));
|
|
assert!(!content::is_valid_id("blog:"));
|
|
}
|
|
|
|
#[cfg(feature = "ids")]
|
|
#[test]
|
|
fn test_uuid_generation() {
|
|
let id = uuid::generate();
|
|
assert!(uuid::is_valid(&id));
|
|
assert_eq!(id.len(), 36); // Standard UUID format with hyphens
|
|
|
|
let simple_id = uuid::generate_simple();
|
|
assert_eq!(simple_id.len(), 32); // No hyphens
|
|
|
|
let short_id = uuid::generate_short();
|
|
assert_eq!(short_id.len(), 8);
|
|
|
|
let tiny_id = uuid::generate_tiny();
|
|
assert_eq!(tiny_id.len(), 4);
|
|
}
|
|
|
|
#[cfg(feature = "ids")]
|
|
#[test]
|
|
fn test_uuid_conversion() {
|
|
let id = uuid::generate();
|
|
let simple = uuid::to_simple(&id).unwrap();
|
|
let back_to_hyphenated = uuid::to_hyphenated(&simple).unwrap();
|
|
|
|
assert_eq!(id.to_lowercase(), back_to_hyphenated.to_lowercase());
|
|
}
|
|
} |