1use nanoid::nanoid;
2use regex::Regex;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum IdType {
7 Event,
8 Deployment,
9 DeploymentGroup,
10 Release,
11 Command,
12 Token,
13}
14
15#[derive(Debug, Clone)]
17pub struct IdConfig {
18 prefix: &'static str,
19 lowercase: bool,
20 length: usize,
21}
22
23impl IdConfig {
24 const fn new(prefix: &'static str, lowercase: bool, length: usize) -> Self {
25 Self {
26 prefix,
27 lowercase,
28 length,
29 }
30 }
31}
32
33const fn get_id_config(id_type: IdType) -> IdConfig {
35 match id_type {
36 IdType::Event => IdConfig::new("event", false, 28),
37 IdType::Deployment => IdConfig::new("ag", true, 28),
38 IdType::DeploymentGroup => IdConfig::new("dg", true, 28),
39 IdType::Release => IdConfig::new("rel", false, 28),
40 IdType::Command => IdConfig::new("cmd", true, 28),
41 IdType::Token => IdConfig::new("tok", true, 28),
42 }
43}
44
45pub const ALPHABET_LOWERCASE: &[char] = &[
46 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
47 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
48];
49
50pub const ALPHABET_MIXED_CASE: &[char] = &[
51 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
52 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
53 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
54 'v', 'w', 'x', 'y', 'z',
55];
56
57pub fn new_id(id_type: IdType) -> String {
59 let config = get_id_config(id_type);
60 let alphabet = if config.lowercase {
61 ALPHABET_LOWERCASE
62 } else {
63 ALPHABET_MIXED_CASE
64 };
65
66 let length = config.length;
67
68 let id = nanoid!(length, &alphabet);
69
70 format!("{}_{}", config.prefix, id)
71}
72
73pub fn generate_id_example(id_type: IdType) -> String {
75 let config = get_id_config(id_type);
76
77 let char_set = if config.lowercase {
79 ALPHABET_LOWERCASE
80 } else {
81 ALPHABET_MIXED_CASE
82 };
83
84 let suffix_length = config.length;
86
87 let mut seed = 0u32;
89 for c in config.prefix.chars() {
90 seed = (seed.wrapping_mul(31) + c as u32) % 4_294_967_295;
91 }
92
93 let mut suffix = String::with_capacity(suffix_length);
95 for _ in 0..suffix_length {
96 seed = (seed.wrapping_mul(1_664_525) + 1_013_904_223) % 4_294_967_295;
98 let random_value = seed as f64 / 4_294_967_295.0;
99
100 let index = (random_value * char_set.len() as f64).floor() as usize;
101 suffix.push(char_set[index]);
102 }
103
104 format!("{}_{}", config.prefix, suffix)
106}
107
108fn convert_camel_to_spaces(input: &str) -> String {
110 let re = Regex::new(r"([a-z])([A-Z])").unwrap();
111 re.replace_all(input, "$1 $2").to_lowercase()
112}
113
114fn capitalize_first_letter(input: &str) -> String {
116 let mut chars = input.chars();
117 match chars.next() {
118 None => String::new(),
119 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
120 }
121}
122
123pub fn id_regex_pattern(id_type: IdType) -> String {
125 let config = get_id_config(id_type);
126 let alphabet_regex = if config.lowercase {
127 "0-9a-z"
128 } else {
129 "0-9a-zA-Z"
130 };
131
132 format!(
133 "^{}_[{}]{{{}}}$",
134 config.prefix, alphabet_regex, config.length
135 )
136}
137
138pub fn id_error_message(id_type: IdType) -> String {
140 let config = get_id_config(id_type);
141 let type_name = convert_camel_to_spaces(&format!("{:?}", id_type));
142 let alphabet_desc = if config.lowercase {
143 "lowercase letters and numbers"
144 } else {
145 "alphanumeric characters"
146 };
147
148 format!(
149 "{} ID must start with {}_ and can only contain {}.",
150 capitalize_first_letter(&type_name),
151 config.prefix,
152 alphabet_desc
153 )
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn deployment_ids_match_platform_schema_shape() {
162 let id = new_id(IdType::Deployment);
163 let pattern = Regex::new(&id_regex_pattern(IdType::Deployment)).unwrap();
164 assert!(pattern.is_match(&id), "unexpected deployment id: {id}");
165 assert!(id.starts_with("ag_"));
166 }
167}