use crate::schema::AutoGenerationRule;
use crate::{Error, Result};
use rand::Rng;
use regex::Regex;
use serde_json::Value;
use uuid::Uuid;
pub fn generate_id(
rule: &AutoGenerationRule,
entity_name: &str,
field_name: &str,
counter: Option<u64>,
) -> Result<String> {
match rule {
AutoGenerationRule::Uuid => Ok(Uuid::new_v4().to_string()),
AutoGenerationRule::Timestamp => Ok(chrono::Utc::now().timestamp().to_string()),
AutoGenerationRule::Date => Ok(chrono::Utc::now().date_naive().to_string()),
AutoGenerationRule::Pattern(pattern) => {
generate_pattern_id(pattern, entity_name, field_name, counter)
}
AutoGenerationRule::Realistic { prefix, length } => generate_realistic_id(prefix, *length),
AutoGenerationRule::AutoIncrement => {
Err(Error::internal("AutoIncrement should be handled by database".to_string()))
}
AutoGenerationRule::Custom(expr) => evaluate_custom_expression(expr),
}
}
fn generate_pattern_id(
pattern: &str,
_entity_name: &str,
_field_name: &str,
counter: Option<u64>,
) -> Result<String> {
let mut result = pattern.to_string();
let increment_re = Regex::new(r"\{increment(?::(\d+))?\}").unwrap();
if increment_re.is_match(&result) {
let increment_value = counter.unwrap_or(1);
result = increment_re
.replace_all(&result, |caps: ®ex::Captures| {
if let Some(padding_str) = caps.get(1) {
let padding: usize = padding_str.as_str().parse().unwrap_or(6);
format!("{:0width$}", increment_value, width = padding)
} else {
increment_value.to_string()
}
})
.to_string();
}
if result.contains("{timestamp}") {
let timestamp = chrono::Utc::now().timestamp();
result = result.replace("{timestamp}", ×tamp.to_string());
}
let random_re = Regex::new(r"\{random(?::(\d+))?\}").unwrap();
if random_re.is_match(&result) {
result = random_re
.replace_all(&result, |caps: ®ex::Captures| {
let length: usize = caps.get(1).and_then(|m| m.as_str().parse().ok()).unwrap_or(8);
generate_random_string(length)
})
.to_string();
}
if result.contains("{uuid}") {
result = result.replace("{uuid}", &Uuid::new_v4().to_string());
}
Ok(result)
}
fn generate_realistic_id(prefix: &str, length: usize) -> Result<String> {
let random_part = generate_random_string(length);
Ok(format!("{}_{}", prefix, random_part))
}
fn generate_random_string(length: usize) -> String {
const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rand::rng();
(0..length)
.map(|_| {
let idx = rng.random_range(0..CHARS.len());
CHARS[idx] as char
})
.collect()
}
pub async fn get_and_increment_counter(
database: &dyn crate::database::VirtualDatabase,
entity_name: &str,
field_name: &str,
) -> Result<u64> {
let counter_table = "_vbr_counters";
let key = format!("{}:{}", entity_name, field_name);
if !database.table_exists(counter_table).await? {
let create_table = format!(
"CREATE TABLE IF NOT EXISTS {} (key TEXT PRIMARY KEY, value INTEGER NOT NULL DEFAULT 0)",
counter_table
);
database.create_table(&create_table).await?;
}
let query = format!("SELECT value FROM {} WHERE key = ?", counter_table);
let results = database.query(&query, &[Value::String(key.clone())]).await?;
let current_value = if let Some(row) = results.first() {
row.get("value").and_then(|v| v.as_u64()).unwrap_or(0)
} else {
0
};
let new_value = current_value + 1;
let update_query = format!(
"INSERT INTO {} (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",
counter_table
);
database
.execute(
&update_query,
&[
Value::String(key),
Value::Number(new_value.into()),
Value::Number(new_value.into()),
],
)
.await?;
Ok(new_value)
}
fn evaluate_custom_expression(expr: &str) -> Result<String> {
let normalized = expr.trim().to_uppercase();
match normalized.as_str() {
"NOW()" | "CURRENT_TIMESTAMP" | "CURRENT_TIMESTAMP()" => {
Ok(chrono::Utc::now().to_rfc3339())
}
"UUID()" | "GEN_RANDOM_UUID()" => Ok(Uuid::new_v4().to_string()),
"RANDOM()" => {
let mut rng = rand::rng();
Ok(rng.random::<u32>().to_string())
}
_ => {
Ok(expr.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_pattern_id() {
let pattern = "USR-{increment:06}";
let result = generate_pattern_id(pattern, "User", "id", Some(1)).unwrap();
assert_eq!(result, "USR-000001");
let pattern = "ORD-{timestamp}";
let result = generate_pattern_id(pattern, "Order", "id", None).unwrap();
assert!(result.starts_with("ORD-"));
let pattern = "TXN-{random:12}";
let result = generate_pattern_id(pattern, "Transaction", "id", None).unwrap();
assert!(result.starts_with("TXN-"));
assert_eq!(result.len(), 16); }
#[test]
fn test_generate_pattern_id_increment_no_padding() {
let pattern = "ID-{increment}";
let result = generate_pattern_id(pattern, "Test", "id", Some(42)).unwrap();
assert_eq!(result, "ID-42");
}
#[test]
fn test_generate_pattern_id_increment_with_padding() {
let pattern = "NUM-{increment:08}";
let result = generate_pattern_id(pattern, "Test", "id", Some(123)).unwrap();
assert_eq!(result, "NUM-00000123");
}
#[test]
fn test_generate_pattern_id_increment_default_counter() {
let pattern = "SEQ-{increment:04}";
let result = generate_pattern_id(pattern, "Test", "id", None).unwrap();
assert_eq!(result, "SEQ-0001");
}
#[test]
fn test_generate_pattern_id_uuid() {
let pattern = "ITEM-{uuid}";
let result = generate_pattern_id(pattern, "Test", "id", None).unwrap();
assert!(result.starts_with("ITEM-"));
assert_eq!(result.len(), 41); }
#[test]
fn test_generate_pattern_id_random_default_length() {
let pattern = "RND-{random}";
let result = generate_pattern_id(pattern, "Test", "id", None).unwrap();
assert!(result.starts_with("RND-"));
assert_eq!(result.len(), 12); }
#[test]
fn test_generate_pattern_id_random_custom_length() {
let pattern = "KEY-{random:20}";
let result = generate_pattern_id(pattern, "Test", "id", None).unwrap();
assert!(result.starts_with("KEY-"));
assert_eq!(result.len(), 24); }
#[test]
fn test_generate_pattern_id_multiple_placeholders() {
let pattern = "{increment:03}-{random:4}";
let result = generate_pattern_id(pattern, "Test", "id", Some(5)).unwrap();
assert!(result.starts_with("005-"));
assert_eq!(result.len(), 8); }
#[test]
fn test_generate_pattern_id_plain_text() {
let pattern = "STATIC-ID";
let result = generate_pattern_id(pattern, "Test", "id", None).unwrap();
assert_eq!(result, "STATIC-ID");
}
#[test]
fn test_generate_realistic_id() {
let result = generate_realistic_id("cus", 14).unwrap();
assert!(result.starts_with("cus_"));
assert_eq!(result.len(), 18); }
#[test]
fn test_generate_realistic_id_different_prefix() {
let result = generate_realistic_id("ord", 10).unwrap();
assert!(result.starts_with("ord_"));
assert_eq!(result.len(), 14); }
#[test]
fn test_generate_realistic_id_long_prefix() {
let result = generate_realistic_id("subscription", 8).unwrap();
assert!(result.starts_with("subscription_"));
assert_eq!(result.len(), 21); }
#[test]
fn test_generate_realistic_id_uniqueness() {
let id1 = generate_realistic_id("test", 12).unwrap();
let id2 = generate_realistic_id("test", 12).unwrap();
assert_ne!(id1, id2);
}
#[test]
fn test_generate_random_string() {
let s1 = generate_random_string(8);
let s2 = generate_random_string(8);
assert_eq!(s1.len(), 8);
assert_eq!(s2.len(), 8);
assert_ne!(s1, s2);
}
#[test]
fn test_generate_random_string_various_lengths() {
assert_eq!(generate_random_string(1).len(), 1);
assert_eq!(generate_random_string(5).len(), 5);
assert_eq!(generate_random_string(16).len(), 16);
assert_eq!(generate_random_string(32).len(), 32);
}
#[test]
fn test_generate_random_string_valid_chars() {
let s = generate_random_string(100);
for c in s.chars() {
assert!(c.is_ascii_lowercase() || c.is_ascii_digit());
}
}
#[test]
fn test_generate_random_string_empty() {
let s = generate_random_string(0);
assert!(s.is_empty());
}
#[test]
fn test_generate_id_uuid() {
let rule = AutoGenerationRule::Uuid;
let result = generate_id(&rule, "Entity", "id", None).unwrap();
assert_eq!(result.len(), 36);
assert!(result.contains('-'));
}
#[test]
fn test_generate_id_timestamp() {
let rule = AutoGenerationRule::Timestamp;
let result = generate_id(&rule, "Entity", "created_at", None).unwrap();
let _timestamp: i64 = result.parse().expect("Should be a valid timestamp");
}
#[test]
fn test_generate_id_date() {
let rule = AutoGenerationRule::Date;
let result = generate_id(&rule, "Entity", "date", None).unwrap();
assert_eq!(result.len(), 10);
assert!(result.contains('-'));
}
#[test]
fn test_generate_id_pattern() {
let rule = AutoGenerationRule::Pattern("PREFIX-{increment:04}".to_string());
let result = generate_id(&rule, "Entity", "id", Some(7)).unwrap();
assert_eq!(result, "PREFIX-0007");
}
#[test]
fn test_generate_id_realistic() {
let rule = AutoGenerationRule::Realistic {
prefix: "inv".to_string(),
length: 10,
};
let result = generate_id(&rule, "Invoice", "id", None).unwrap();
assert!(result.starts_with("inv_"));
assert_eq!(result.len(), 14);
}
#[test]
fn test_generate_id_auto_increment_error() {
let rule = AutoGenerationRule::AutoIncrement;
let result = generate_id(&rule, "Entity", "id", None);
assert!(result.is_err());
}
#[test]
fn test_generate_id_custom_now() {
let rule = AutoGenerationRule::Custom("NOW()".to_string());
let result = generate_id(&rule, "Entity", "id", None);
assert!(result.is_ok());
let value = result.unwrap();
assert!(value.contains('T'), "Expected timestamp format, got: {}", value);
}
#[test]
fn test_generate_id_custom_uuid() {
let rule = AutoGenerationRule::Custom("UUID()".to_string());
let result = generate_id(&rule, "Entity", "id", None).unwrap();
assert_eq!(result.len(), 36); assert!(result.contains('-'));
}
#[test]
fn test_generate_id_custom_literal() {
let rule = AutoGenerationRule::Custom("default_value".to_string());
let result = generate_id(&rule, "Entity", "id", None).unwrap();
assert_eq!(result, "default_value");
}
}