gossan_cloud/
permutations.rs1use serde::Deserialize;
6use std::sync::OnceLock;
7
8#[derive(Debug, Clone, Deserialize)]
10struct PermutationConfig {
11 suffixes: StringList,
12 prefixes: StringList,
13 transforms: Transforms,
14}
15
16#[derive(Debug, Clone, Deserialize)]
17struct StringList {
18 values: Vec<String>,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22struct Transforms {
23 #[serde(rename = "dot_to_hyphen")]
24 dot_to_hyphen: bool,
25 #[serde(rename = "hyphen_to_dot")]
26 hyphen_to_dot: bool,
27}
28
29const BUILTIN_PERMUTATIONS: &str = include_str!("../rules/permutations.toml");
31
32static PERMUTATIONS: OnceLock<PermutationConfig> = OnceLock::new();
34
35fn builtin_permutations() -> &'static PermutationConfig {
37 PERMUTATIONS.get_or_init(|| {
38 match toml::from_str::<PermutationConfig>(BUILTIN_PERMUTATIONS) {
39 Ok(config) => config,
40 Err(e) => {
41 tracing::error!(error = %e, "failed to parse built-in permutations.toml");
42 PermutationConfig {
44 suffixes: StringList {
45 values: vec![
46 "".to_string(),
47 "-assets".to_string(),
48 "-static".to_string(),
49 "-dev".to_string(),
50 "-prod".to_string(),
51 ],
52 },
53 prefixes: StringList {
54 values: vec!["".to_string(), "assets-".to_string(), "dev-".to_string()],
55 },
56 transforms: Transforms {
57 dot_to_hyphen: true,
58 hyphen_to_dot: true,
59 },
60 }
61 }
62 }
63 })
64}
65
66pub fn generate(org: &str) -> Vec<String> {
68 let o = org.to_lowercase();
69 let config = builtin_permutations();
70 let suffixes = &config.suffixes.values;
71 let prefixes = &config.prefixes.values;
72
73 let mut candidates = std::collections::HashSet::new();
74
75 for suffix in suffixes {
76 for prefix in prefixes {
77 let name = format!("{}{}{}", prefix, o, suffix);
78 if name.len() >= 3 && name.len() <= 63 {
80 candidates.insert(name);
81 }
82 }
83 }
84
85 if config.transforms.dot_to_hyphen {
87 let transformed = o.replace('.', "-");
88 if transformed.len() >= 3 && transformed.len() <= 63 {
89 candidates.insert(transformed);
90 }
91 }
92 if config.transforms.hyphen_to_dot {
93 let transformed = o.replace('-', ".");
94 if transformed.len() >= 3 && transformed.len() <= 63 {
95 candidates.insert(transformed);
96 }
97 }
98
99 candidates.into_iter().collect()
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn generate_includes_expected_prefix_and_suffix_forms() {
108 let candidates = generate("example");
109 assert!(candidates.contains(&"example-assets".to_string()));
110 assert!(candidates.contains(&"assets-example".to_string()));
111 assert!(candidates.contains(&"example".to_string()));
112 }
113
114 #[test]
115 fn generate_deduplicates_candidates() {
116 let candidates = generate("example");
117 let unique = candidates.iter().collect::<std::collections::HashSet<_>>();
118 assert_eq!(unique.len(), candidates.len());
119 }
120
121 #[test]
122 fn generate_normalizes_case_and_preserves_valid_lengths() {
123 let candidates = generate("ExAmPlE");
124 assert!(candidates.iter().all(|name| name == &name.to_lowercase()));
125 assert!(candidates.iter().all(|name| (3..=63).contains(&name.len())));
126 }
127
128 #[test]
129 fn permutations_load_from_toml() {
130 let config = builtin_permutations();
131 assert!(
132 !config.suffixes.values.is_empty(),
133 "should have suffixes from TOML"
134 );
135 assert!(
136 !config.prefixes.values.is_empty(),
137 "should have prefixes from TOML"
138 );
139 }
140
141 #[test]
142 fn permutations_include_expected_suffixes() {
143 let config = builtin_permutations();
144 let suffixes = &config.suffixes.values;
145
146 assert!(
148 suffixes.contains(&"".to_string()),
149 "should include empty suffix"
150 );
151 assert!(
152 suffixes.contains(&"-assets".to_string()),
153 "should include -assets"
154 );
155 assert!(
156 suffixes.contains(&"-prod".to_string()),
157 "should include -prod"
158 );
159 assert!(
160 suffixes.contains(&"-backup".to_string()),
161 "should include -backup"
162 );
163 }
164
165 #[test]
166 fn permutations_include_expected_prefixes() {
167 let config = builtin_permutations();
168 let prefixes = &config.prefixes.values;
169
170 assert!(
172 prefixes.contains(&"".to_string()),
173 "should include empty prefix"
174 );
175 assert!(
176 prefixes.contains(&"assets-".to_string()),
177 "should include assets-"
178 );
179 assert!(
180 prefixes.contains(&"dev-".to_string()),
181 "should include dev-"
182 );
183 }
184
185 #[test]
186 fn transforms_are_enabled() {
187 let config = builtin_permutations();
188 assert!(
189 config.transforms.dot_to_hyphen,
190 "dot_to_hyphen should be enabled"
191 );
192 assert!(
193 config.transforms.hyphen_to_dot,
194 "hyphen_to_dot should be enabled"
195 );
196 }
197}