use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
#[cfg_attr(feature = "diagnostics", derive(miette::Diagnostic))]
pub enum IdError {
#[error("Invalid ID format: {format}")]
#[cfg_attr(
feature = "diagnostics",
diagnostic(code(weavegraph::id::invalid_format))
)]
InvalidFormat { format: String },
#[error("ID generation failed: {reason}")]
#[cfg_attr(
feature = "diagnostics",
diagnostic(code(weavegraph::id::generation_failed))
)]
GenerationFailed { reason: String },
}
#[derive(Debug, Clone, Default)]
pub struct IdConfig {
pub seed: Option<u64>,
pub prefix: Option<String>,
pub include_timestamp: bool,
pub use_counter: bool,
}
impl fmt::Display for IdConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"IdConfig {{ seed: {:?}, prefix: {:?}, timestamp: {}, counter: {} }}",
self.seed, self.prefix, self.include_timestamp, self.use_counter
)
}
}
#[derive(Debug)]
pub struct IdGenerator {
config: IdConfig,
counter: AtomicU64,
}
impl IdGenerator {
#[must_use]
pub fn new() -> Self {
Self {
config: IdConfig::default(),
counter: AtomicU64::new(0),
}
}
#[must_use]
pub fn with_config(config: IdConfig) -> Self {
Self {
config,
counter: AtomicU64::new(0),
}
}
#[must_use]
pub fn generate_id(&self) -> String {
let base_id = if let Some(seed) = self.config.seed {
if self.config.use_counter {
let counter = self.counter.fetch_add(1, Ordering::Relaxed);
format!("seeded-{}-{}", seed, counter)
} else {
format!("seeded-{}", seed)
}
} else if self.config.use_counter {
let counter = self.counter.fetch_add(1, Ordering::Relaxed);
format!("counter-{}", counter)
} else {
self.generate_uuid()
};
let mut final_id = base_id;
if self.config.include_timestamp {
let timestamp = chrono::Utc::now().timestamp();
final_id = format!("{}-t{}", final_id, timestamp);
}
if let Some(prefix) = &self.config.prefix {
final_id = format!("{}-{}", prefix, final_id);
}
final_id
}
#[must_use]
pub fn generate_uuid(&self) -> String {
Uuid::new_v4().to_string()
}
#[must_use]
pub fn generate_id_with_prefix(&self, prefix: &str) -> String {
format!("{}-{}", prefix, self.generate_base_id())
}
#[must_use]
pub fn generate_run_id(&self) -> String {
self.generate_id_with_prefix("run")
}
#[must_use]
pub fn generate_step_id(&self) -> String {
self.generate_id_with_prefix("step")
}
#[must_use]
pub fn generate_node_id(&self) -> String {
self.generate_id_with_prefix("node")
}
#[must_use]
pub fn generate_session_id(&self) -> String {
self.generate_id_with_prefix("session")
}
#[must_use]
pub fn generate_random_id(&self) -> String {
self.generate_uuid()
}
pub fn parse_id(&self, id: &str) -> Result<ParsedId, IdError> {
if id.is_empty() {
return Err(IdError::InvalidFormat {
format: "empty string".into(),
});
}
let parts: Vec<&str> = id.split('-').collect();
if parts.len() < 2 {
return Err(IdError::InvalidFormat {
format: "missing separator".into(),
});
}
Ok(ParsedId {
prefix: parts[0].to_string(),
base: parts[1..].join("-"),
original: id.to_string(),
})
}
#[must_use]
pub fn current_counter(&self) -> u64 {
self.counter.load(Ordering::Relaxed)
}
pub fn reset_counter(&self) {
self.counter.store(0, Ordering::Relaxed);
}
fn generate_base_id(&self) -> String {
if let Some(seed) = self.config.seed {
if self.config.use_counter {
let counter = self.counter.fetch_add(1, Ordering::Relaxed);
format!("seeded-{}-{}", seed, counter)
} else {
format!("seeded-{}", seed)
}
} else if self.config.use_counter {
let counter = self.counter.fetch_add(1, Ordering::Relaxed);
format!("counter-{}", counter)
} else {
self.generate_uuid()
}
}
}
impl Default for IdGenerator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedId {
pub prefix: String,
pub base: String,
pub original: String,
}
impl ParsedId {
#[must_use]
pub fn has_prefix(&self, expected_prefix: &str) -> bool {
self.prefix == expected_prefix
}
#[must_use]
pub fn extract_timestamp(&self) -> Option<i64> {
if let Some(timestamp_part) = self.base.split('-').find(|part| part.starts_with('t')) {
timestamp_part[1..].parse().ok()
} else {
None
}
}
}
pub mod id_utils {
use super::*;
#[must_use]
pub fn create_test_generator(seed: u64) -> IdGenerator {
IdGenerator::with_config(IdConfig {
seed: Some(seed),
use_counter: true,
..Default::default()
})
}
#[must_use]
pub fn create_production_generator() -> IdGenerator {
IdGenerator::with_config(IdConfig {
include_timestamp: true,
..Default::default()
})
}
pub fn is_valid_id(id: &str, expected_prefix: Option<&str>) -> bool {
if id.is_empty() {
return false;
}
let parts: Vec<&str> = id.split('-').collect();
if parts.len() < 2 {
return false;
}
if let Some(prefix) = expected_prefix {
parts[0] == prefix
} else {
true
}
}
#[must_use]
pub fn get_id_type(id: &str) -> String {
id.split('-')
.next()
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".to_string())
}
}