Rustelo/crates/templates/core-lib-template/src/ids.rs
Jesús Pérez d3a47108af
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
chore: update gitignore and fix content
2026-02-08 20:07:09 +00:00

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());
}
}