use chrono::{TimeZone, Utc};
use rand::Rng;
use regex::Regex;
use std::sync::OnceLock;
use uuid::Uuid;
struct Patterns {
uuid: Regex,
int_range: Regex,
float_range: Regex,
ts_range: Regex,
ts_simple: Regex,
datetime_simple: Regex,
datetime_range: Regex,
}
static PATTERNS: OnceLock<Patterns> = OnceLock::new();
impl Patterns {
fn new() -> Self {
Self {
uuid: Regex::new(r"<rnd:uuid>").expect("Invalid UUID regex"),
int_range: Regex::new(r"<rnd:int\[\s*(-?\d+)\s*,\s*(-?\d+)\s*\]>")
.expect("Invalid int range regex"),
float_range: Regex::new(
r"<rnd:float\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]>",
)
.expect("Invalid float range regex"),
ts_range: Regex::new(r"<rnd:ts\[\s*(\d+)\s*,\s*(\d+)\s*\]>")
.expect("Invalid timestamp range regex"),
ts_simple: Regex::new(r"<rnd:ts>").expect("Invalid timestamp simple regex"),
datetime_simple: Regex::new(r"<rnd:datetime>").expect("Invalid datetime simple regex"),
datetime_range: Regex::new(r"<rnd:datetime\[([^,]+),\s*([^\]]+)\]>")
.expect("Invalid datetime range regex"),
}
}
fn get() -> &'static Patterns {
PATTERNS.get_or_init(Patterns::new)
}
}
pub fn resolve(input: &str) -> String {
let patterns = Patterns::get();
let mut result = input.to_string();
result = patterns
.uuid
.replace_all(&result, |_: ®ex::Captures| Uuid::new_v4().to_string())
.to_string();
result = patterns
.int_range
.replace_all(&result, |caps: ®ex::Captures| {
let min = caps[1].trim().parse::<i64>().unwrap_or(0);
let max = caps[2].trim().parse::<i64>().unwrap_or(100);
let (actual_min, actual_max) = if min <= max { (min, max) } else { (max, min) };
rand::rng()
.random_range(actual_min..=actual_max)
.to_string()
})
.to_string();
result = patterns
.float_range
.replace_all(&result, |caps: ®ex::Captures| {
let min_str = caps[1].trim();
let max_str = caps[2].trim();
let min = min_str.parse::<f64>().unwrap_or(0.0);
let max = max_str.parse::<f64>().unwrap_or(1.0);
let precision = if min_str.contains('.') {
min_str.split('.').nth(1).map(|s| s.len()).unwrap_or(1)
} else {
0
};
let (actual_min, actual_max) = if min <= max { (min, max) } else { (max, min) };
let random_value = rand::rng().random_range(actual_min..=actual_max);
format!("{:.precision$}", random_value, precision = precision)
})
.to_string();
result = patterns
.ts_range
.replace_all(&result, |caps: ®ex::Captures| {
let start_ts = caps[1].trim().parse::<i64>().unwrap_or(0);
let end_ts = caps[2]
.trim()
.parse::<i64>()
.unwrap_or_else(|_| Utc::now().timestamp());
let (actual_start, actual_end) = if start_ts <= end_ts {
(start_ts, end_ts)
} else {
(end_ts, start_ts)
};
let random_ts = rand::rng().random_range(actual_start..=actual_end);
random_ts.to_string()
})
.to_string();
result = patterns
.ts_simple
.replace_all(&result, |_: ®ex::Captures| {
let now = Utc::now().timestamp();
let thirty_days_ago = now - (30 * 24 * 60 * 60); let random_ts = rand::rng().random_range(thirty_days_ago..=now);
random_ts.to_string()
})
.to_string();
result = patterns
.datetime_simple
.replace_all(&result, |_: ®ex::Captures| {
let now = Utc::now().timestamp();
let thirty_days_ago = now - (30 * 24 * 60 * 60); let random_ts = rand::rng().random_range(thirty_days_ago..=now);
match Utc.timestamp_opt(random_ts, 0) {
chrono::LocalResult::Single(dt) => dt.to_rfc3339(),
_ => format!("{{datetime_error: invalid_timestamp_{}}}", random_ts),
}
})
.to_string();
result = patterns
.datetime_range
.replace_all(&result, |caps: ®ex::Captures| {
let start_str = caps[1].trim();
let end_str = caps[2].trim();
let start_ts = chrono::DateTime::parse_from_rfc3339(start_str)
.map(|dt| dt.timestamp())
.unwrap_or_else(|_| {
start_str.parse::<i64>().unwrap_or(0)
});
let end_ts = chrono::DateTime::parse_from_rfc3339(end_str)
.map(|dt| dt.timestamp())
.unwrap_or_else(|_| {
end_str
.parse::<i64>()
.unwrap_or_else(|_| Utc::now().timestamp())
});
let (actual_start, actual_end) = if start_ts <= end_ts {
(start_ts, end_ts)
} else {
(end_ts, start_ts)
};
let random_ts = rand::rng().random_range(actual_start..=actual_end);
match Utc.timestamp_opt(random_ts, 0) {
chrono::LocalResult::Single(dt) => dt.to_rfc3339(),
_ => format!("{{datetime_error: invalid_timestamp_{}}}", random_ts),
}
})
.to_string();
result
}
#[allow(dead_code)]
pub fn uuid() -> String {
Uuid::new_v4().to_string()
}
#[allow(dead_code)]
pub fn int_range(min: i64, max: i64) -> i64 {
let (actual_min, actual_max) = if min <= max { (min, max) } else { (max, min) };
rand::rng().random_range(actual_min..=actual_max)
}
#[allow(dead_code)]
pub fn float_range(min: f64, max: f64, precision: usize) -> String {
let (actual_min, actual_max) = if min <= max { (min, max) } else { (max, min) };
let value = rand::rng().random_range(actual_min..=actual_max);
format!("{:.precision$}", value, precision = precision)
}
#[allow(dead_code)]
pub fn timestamp_range(start: i64, end: i64) -> i64 {
let (actual_start, actual_end) = if start <= end {
(start, end)
} else {
(end, start)
};
rand::rng().random_range(actual_start..=actual_end)
}
#[allow(dead_code)]
pub fn timestamp() -> i64 {
let now = Utc::now().timestamp();
let thirty_days_ago = now - (30 * 24 * 60 * 60);
rand::rng().random_range(thirty_days_ago..=now)
}
#[allow(dead_code)]
pub fn datetime() -> String {
let ts = timestamp();
match Utc.timestamp_opt(ts, 0) {
chrono::LocalResult::Single(dt) => dt.to_rfc3339(),
_ => format!("{{datetime_error: invalid_timestamp_{}}}", ts),
}
}
#[allow(dead_code)]
pub fn datetime_range(start: i64, end: i64) -> String {
let ts = timestamp_range(start, end);
match Utc.timestamp_opt(ts, 0) {
chrono::LocalResult::Single(dt) => dt.to_rfc3339(),
_ => format!("{{datetime_error: invalid_timestamp_{}}}", ts),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_uuid() {
let input = "test-<rnd:uuid>-end";
let result = resolve(input);
assert!(result.starts_with("test-"));
assert!(result.ends_with("-end"));
assert_ne!(result, input);
let uuid_part = &result[5..41]; assert_eq!(uuid_part.len(), 36);
assert_eq!(uuid_part.matches('-').count(), 4);
}
#[test]
fn test_resolve_int_range() {
let input = "value: <rnd:int[10, 20]>";
let result = resolve(input);
assert!(result.starts_with("value: "));
let value_str = result.strip_prefix("value: ").unwrap();
let value: i32 = value_str.parse().expect("Should be a valid integer");
assert!((10..=20).contains(&value));
}
#[test]
fn test_resolve_int_range_reversed() {
let input = "value: <rnd:int[20, 10]>";
let result = resolve(input);
let value_str = result.strip_prefix("value: ").unwrap();
let value: i32 = value_str.parse().expect("Should be a valid integer");
assert!(
(10..=20).contains(&value),
"Value {} should be between 10 and 20",
value
);
}
#[test]
fn test_resolve_float_range() {
let input = "score: <rnd:float[0.0, 1.0]>";
let result = resolve(input);
let value_str = result.strip_prefix("score: ").unwrap();
let value: f64 = value_str.parse().expect("Should be a valid float");
assert!((0.0..=1.0).contains(&value));
}
#[test]
fn test_resolve_timestamp_simple() {
let input = "ts: <rnd:ts>";
let result = resolve(input);
let ts_str = result.strip_prefix("ts: ").unwrap();
let ts: i64 = ts_str.parse().expect("Should be a valid timestamp");
let now = Utc::now().timestamp();
let thirty_days_ago = now - (30 * 24 * 60 * 60);
assert!(ts >= thirty_days_ago && ts <= now);
}
#[test]
fn test_resolve_datetime_simple() {
let input = "dt: <rnd:datetime>";
let result = resolve(input);
assert!(result.starts_with("dt: "));
let dt_str = result.strip_prefix("dt: ").unwrap();
assert!(chrono::DateTime::parse_from_rfc3339(dt_str).is_ok());
}
#[test]
fn test_resolve_multiple_patterns() {
let input =
r#"{"id": "<rnd:uuid>", "value": <rnd:int[1, 10]>, "score": <rnd:float[0.0, 1.0]>}"#;
let result = resolve(input);
let json: serde_json::Value = serde_json::from_str(&result).expect("Should be valid JSON");
assert!(json["id"].is_string());
assert!(json["value"].is_number());
assert!(json["score"].is_number());
let value = json["value"].as_i64().unwrap();
assert!((1..=10).contains(&value));
let score = json["score"].as_f64().unwrap();
assert!((0.0..=1.0).contains(&score));
}
#[test]
fn test_no_patterns_unchanged() {
let input = "plain text with no patterns";
let result = resolve(input);
assert_eq!(result, input);
}
#[test]
fn test_malformed_patterns_ignored() {
let input = "test <rnd:invalid> <rnd> <rnd:>";
let result = resolve(input);
assert_eq!(result, input); }
#[test]
fn test_helper_functions() {
let uuid = uuid();
assert_eq!(uuid.len(), 36);
assert_eq!(uuid.matches('-').count(), 4);
let int_val = int_range(5, 15);
assert!((5..=15).contains(&int_val));
let float_val = float_range(0.0, 1.0, 2);
assert!(float_val.contains('.'));
let parsed: f64 = float_val.parse().unwrap();
assert!((0.0..=1.0).contains(&parsed));
let ts = timestamp();
let now = Utc::now().timestamp();
let thirty_days_ago = now - (30 * 24 * 60 * 60);
assert!(ts >= thirty_days_ago && ts <= now);
let dt = datetime();
assert!(chrono::DateTime::parse_from_rfc3339(&dt).is_ok());
}
}