Skip to main content

vtcode_commons/
slug.rs

1#![expect(
2    clippy::indexing_slicing,
3    reason = "Random indices are generated from the exact lengths of the static slug tables."
4)]
5
6//! Human-readable slug generator for plan file names
7//!
8//! Generates memorable identifiers by combining random adjectives and nouns,
9//! producing slugs like "gentle-harbor" or "cosmic-wizard".
10//!
11//! Based on OpenCode's slug utility pattern for planning workflow file naming.
12
13use rand::RngExt;
14
15/// Adjectives for slug generation (30 options)
16const ADJECTIVES: &[&str] = &[
17    "brave", "calm", "clever", "cosmic", "crisp", "curious", "eager", "gentle", "glowing", "happy", "hidden", "jolly",
18    "kind", "lucky", "mighty", "misty", "neon", "nimble", "playful", "proud", "quick", "quiet", "shiny", "silent",
19    "stellar", "sunny", "swift", "tidy", "witty", "bright",
20];
21
22/// Nouns for slug generation (32 options)
23const NOUNS: &[&str] = &[
24    "cabin", "cactus", "canyon", "circuit", "comet", "eagle", "engine", "falcon", "forest", "garden", "harbor",
25    "island", "knight", "lagoon", "meadow", "moon", "mountain", "nebula", "orchid", "otter", "panda", "pixel",
26    "planet", "river", "rocket", "sailor", "squid", "star", "tiger", "wizard", "wolf", "stream",
27];
28
29/// Create a human-readable slug by combining a random adjective with a random noun.
30///
31/// # Examples
32///
33/// ```
34/// use vtcode_commons::slug;
35///
36/// let slug = slug::create();
37/// // Returns something like "gentle-harbor", "cosmic-wizard", etc.
38/// assert!(slug.contains('-'));
39/// ```
40pub fn create() -> String {
41    let mut rng = rand::rng();
42    let adj_idx = rng.random_range(0..ADJECTIVES.len());
43    let noun_idx = rng.random_range(0..NOUNS.len());
44
45    format!("{}-{}", ADJECTIVES[adj_idx], NOUNS[noun_idx])
46}
47
48/// Create a timestamped slug with a human-readable suffix.
49///
50/// Format: `{timestamp_millis}-{adjective}-{noun}`
51///
52/// # Examples
53///
54/// ```
55/// use vtcode_commons::slug;
56///
57/// let slug = slug::create_timestamped();
58/// // Returns something like "1768330644696-gentle-harbor"
59/// ```
60pub fn create_timestamped() -> String {
61    let timestamp = std::time::SystemTime::now()
62        .duration_since(std::time::UNIX_EPOCH)
63        .map(|d| d.as_millis())
64        .unwrap_or(0);
65
66    format!("{}-{}", timestamp, create())
67}
68
69/// Create a slug with a custom prefix.
70///
71/// # Examples
72///
73/// ```
74/// use vtcode_commons::slug;
75///
76/// let slug = slug::create_with_prefix("plan");
77/// // Returns something like "plan-gentle-harbor"
78/// ```
79pub fn create_with_prefix(prefix: &str) -> String {
80    format!("{}-{}", prefix, create())
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_create_slug() {
89        let slug = create();
90        assert!(slug.contains('-'));
91        let parts: Vec<&str> = slug.split('-').collect();
92        assert_eq!(parts.len(), 2);
93        assert!(ADJECTIVES.contains(&parts[0]));
94        assert!(NOUNS.contains(&parts[1]));
95    }
96
97    #[test]
98    fn test_create_timestamped() {
99        let slug = create_timestamped();
100        let parts: Vec<&str> = slug.split('-').collect();
101        assert_eq!(parts.len(), 3);
102        assert!(parts[0].parse::<u128>().is_ok());
103    }
104
105    #[test]
106    fn test_create_with_prefix() {
107        let slug = create_with_prefix("plan");
108        assert!(slug.starts_with("plan-"));
109        let parts: Vec<&str> = slug.split('-').collect();
110        assert_eq!(parts.len(), 3);
111        assert_eq!(parts[0], "plan");
112    }
113
114    #[test]
115    fn test_uniqueness() {
116        let slugs: Vec<String> = (0..100).map(|_| create()).collect();
117        let unique_count = slugs.iter().collect::<hashbrown::HashSet<_>>().len();
118        assert!(unique_count > 50, "Expected mostly unique slugs");
119    }
120}