Skip to main content

alien_core/
id_utils.rs

1use nanoid::nanoid;
2use regex::Regex;
3
4/// Defines different ID types with their configuration
5#[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/// Configuration for ID generation
16#[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
33/// Get configuration for a specific ID type
34const 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("dep", 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
57/// Generate a new ID for the specified type
58pub 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
73/// Generate a deterministic ID example for a given type
74pub fn generate_id_example(id_type: IdType) -> String {
75    let config = get_id_config(id_type);
76
77    // Determine character set based on lowercase flag
78    let char_set = if config.lowercase {
79        ALPHABET_LOWERCASE
80    } else {
81        ALPHABET_MIXED_CASE
82    };
83
84    // Length of the suffix
85    let suffix_length = config.length;
86
87    // Seed generation from the prefix
88    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    // Generate the suffix using the pseudo-random function
94    let mut suffix = String::with_capacity(suffix_length);
95    for _ in 0..suffix_length {
96        // Pseudo-random number generator
97        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    // Return the complete ID
105    format!("{}_{}", config.prefix, suffix)
106}
107
108/// Convert camel case to spaced lowercase
109fn 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
114/// Capitalize the first letter of a string
115fn 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
123/// Generate a regex pattern for validating IDs of a specific type
124pub 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
138/// Generate an error message for invalid IDs
139pub 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("dep_"));
166    }
167}