1#![expect(
2 clippy::indexing_slicing,
3 reason = "Random indices are generated from the exact lengths of the static slug tables."
4)]
5
6use rand::RngExt;
14
15const 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
22const 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
29pub 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
48pub 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
69pub fn create_with_prefix(prefix: &str) -> String {
80 format!("{}-{}", prefix, create())
81}
82
83pub fn is_humanized_codename(title: &str) -> bool {
91 let words: Vec<&str> = title.split_whitespace().collect();
92 if words.len() != 2 {
93 return false;
94 }
95 let adjective = words[0].to_ascii_lowercase();
96 let noun = words[1].to_ascii_lowercase();
97 ADJECTIVES.contains(&adjective.as_str()) && NOUNS.contains(&noun.as_str())
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_create_slug() {
106 let slug = create();
107 assert!(slug.contains('-'));
108 let parts: Vec<&str> = slug.split('-').collect();
109 assert_eq!(parts.len(), 2);
110 assert!(ADJECTIVES.contains(&parts[0]));
111 assert!(NOUNS.contains(&parts[1]));
112 }
113
114 #[test]
115 fn test_create_timestamped() {
116 let slug = create_timestamped();
117 let parts: Vec<&str> = slug.split('-').collect();
118 assert_eq!(parts.len(), 3);
119 assert!(parts[0].parse::<u128>().is_ok());
120 }
121
122 #[test]
123 fn test_create_with_prefix() {
124 let slug = create_with_prefix("plan");
125 assert!(slug.starts_with("plan-"));
126 let parts: Vec<&str> = slug.split('-').collect();
127 assert_eq!(parts.len(), 3);
128 assert_eq!(parts[0], "plan");
129 }
130
131 #[test]
132 fn test_uniqueness() {
133 let slugs: Vec<String> = (0..100).map(|_| create()).collect();
134 let unique_count = slugs.iter().collect::<hashbrown::HashSet<_>>().len();
135 assert!(unique_count > 50, "Expected mostly unique slugs");
136 }
137
138 #[test]
139 fn test_is_humanized_codename_detects_generated_names() {
140 assert!(is_humanized_codename("Jolly Forest"));
141 assert!(is_humanized_codename("Kind Lagoon"));
142 assert!(!is_humanized_codename("Release"));
143 assert!(!is_humanized_codename("Release Notes"));
144 assert!(!is_humanized_codename("Refine README"));
145 assert!(!is_humanized_codename(""));
146 }
147}