1#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11use std::borrow::Cow;
12
13#[must_use]
24pub fn normalize_package_separator(module_name: &str) -> Cow<'_, str> {
25 if module_name.contains('\'') {
26 Cow::Owned(module_name.replace('\'', "::"))
27 } else {
28 Cow::Borrowed(module_name)
29 }
30}
31
32#[must_use]
43pub fn legacy_package_separator(module_name: &str) -> Cow<'_, str> {
44 if module_name.contains("::") {
45 Cow::Owned(module_name.replace("::", "'"))
46 } else {
47 Cow::Borrowed(module_name)
48 }
49}
50
51#[must_use]
71pub fn module_variant_pairs(old_module: &str, new_module: &str) -> Vec<(String, String)> {
72 let canonical_old = normalize_package_separator(old_module).into_owned();
73 let canonical_new = normalize_package_separator(new_module).into_owned();
74
75 let canonical = (canonical_old.clone(), canonical_new.clone());
76 let legacy = (
77 legacy_package_separator(&canonical_old).into_owned(),
78 legacy_package_separator(&canonical_new).into_owned(),
79 );
80
81 if legacy == canonical { vec![canonical] } else { vec![canonical, legacy] }
82}
83
84#[cfg(test)]
85mod tests {
86 use std::borrow::Cow;
87
88 use super::{legacy_package_separator, module_variant_pairs, normalize_package_separator};
89
90 #[test]
91 fn normalizes_legacy_separator() {
92 assert_eq!(normalize_package_separator("Foo'Bar"), "Foo::Bar");
93 assert_eq!(normalize_package_separator("Foo::Bar"), "Foo::Bar");
94 }
95
96 #[test]
97 fn projects_canonical_separator_to_legacy() {
98 assert_eq!(legacy_package_separator("Foo::Bar"), "Foo'Bar");
99 assert_eq!(legacy_package_separator("Foo'Bar"), "Foo'Bar");
100 }
101
102 #[test]
103 fn builds_canonical_and_legacy_variant_pairs() {
104 assert_eq!(
105 module_variant_pairs("Foo::Bar", "New::Path"),
106 vec![
107 ("Foo::Bar".to_string(), "New::Path".to_string()),
108 ("Foo'Bar".to_string(), "New'Path".to_string()),
109 ]
110 );
111 }
112
113 #[test]
114 fn deduplicates_pair_when_no_separator_variants_exist() {
115 assert_eq!(module_variant_pairs("strict", "warnings").len(), 1);
116 }
117
118 #[test]
121 fn normalize_simple_bare_name_foo() {
122 let result = normalize_package_separator("Foo");
123 assert_eq!(result, "Foo");
124 assert!(matches!(result, Cow::Borrowed(_)));
125 }
126
127 #[test]
128 fn normalize_simple_two_segment_foo_bar() {
129 assert_eq!(normalize_package_separator("Foo'Bar"), "Foo::Bar");
130 }
131
132 #[test]
133 fn legacy_simple_two_segment_foo_bar() {
134 assert_eq!(legacy_package_separator("Foo::Bar"), "Foo'Bar");
135 }
136
137 #[test]
138 fn legacy_simple_bare_name_foo() {
139 let result = legacy_package_separator("Foo");
140 assert_eq!(result, "Foo");
141 assert!(matches!(result, Cow::Borrowed(_)));
142 }
143
144 #[test]
147 fn normalize_four_segments_deeply_nested() {
148 assert_eq!(normalize_package_separator("Foo'Bar'Baz'Qux"), "Foo::Bar::Baz::Qux");
149 }
150
151 #[test]
152 fn legacy_four_segments_deeply_nested() {
153 assert_eq!(legacy_package_separator("Foo::Bar::Baz::Qux"), "Foo'Bar'Baz'Qux");
154 }
155
156 #[test]
157 fn variant_pairs_four_segments_produces_both_forms() {
158 let pairs = module_variant_pairs("Foo::Bar::Baz::Qux", "One::Two::Three::Four");
159 assert_eq!(pairs.len(), 2);
160 assert_eq!(
161 pairs[0],
162 ("Foo::Bar::Baz::Qux".to_string(), "One::Two::Three::Four".to_string())
163 );
164 assert_eq!(pairs[1], ("Foo'Bar'Baz'Qux".to_string(), "One'Two'Three'Four".to_string()));
165 }
166
167 #[test]
168 fn roundtrip_four_segment_normalize_then_legacy() {
169 let input = "Foo'Bar'Baz'Qux";
170 let canonical = normalize_package_separator(input);
171 assert_eq!(canonical, "Foo::Bar::Baz::Qux");
172 let back = legacy_package_separator(&canonical);
173 assert_eq!(back, input);
174 }
175
176 #[test]
177 fn roundtrip_four_segment_legacy_then_normalize() {
178 let input = "Foo::Bar::Baz::Qux";
179 let legacy = legacy_package_separator(input);
180 assert_eq!(legacy, "Foo'Bar'Baz'Qux");
181 let back = normalize_package_separator(&legacy);
182 assert_eq!(back, input);
183 }
184
185 #[test]
188 fn normalize_preserves_content_with_no_separators() {
189 for name in &["DBI", "strict", "warnings", "utf8", "POSIX", "Carp"] {
191 let result = normalize_package_separator(name);
192 assert_eq!(result.as_ref(), *name);
193 assert!(matches!(result, Cow::Borrowed(_)));
194 }
195 }
196
197 #[test]
198 fn legacy_preserves_content_with_no_separators() {
199 for name in &["DBI", "strict", "warnings", "utf8", "POSIX", "Carp"] {
200 let result = legacy_package_separator(name);
201 assert_eq!(result.as_ref(), *name);
202 assert!(matches!(result, Cow::Borrowed(_)));
203 }
204 }
205
206 #[test]
207 fn normalize_handles_only_identifier_chars() {
208 assert_eq!(normalize_package_separator("_Private'_Internal"), "_Private::_Internal");
210 }
211
212 #[test]
213 fn normalize_handles_numeric_only_segments() {
214 assert_eq!(normalize_package_separator("V5'V6"), "V5::V6");
215 }
216
217 #[test]
222 fn components_after_normalize_two_segments() {
223 let canonical = normalize_package_separator("Foo'Bar");
224 let components: Vec<&str> = canonical.split("::").collect();
225 assert_eq!(components, vec!["Foo", "Bar"]);
226 }
227
228 #[test]
229 fn components_after_normalize_four_segments() {
230 let canonical = normalize_package_separator("Foo'Bar'Baz'Qux");
231 let components: Vec<&str> = canonical.split("::").collect();
232 assert_eq!(components, vec!["Foo", "Bar", "Baz", "Qux"]);
233 }
234
235 #[test]
236 fn components_after_normalize_single_segment() {
237 let canonical = normalize_package_separator("strict");
238 let components: Vec<&str> = canonical.split("::").collect();
239 assert_eq!(components, vec!["strict"]);
240 }
241
242 #[test]
243 fn components_after_normalize_already_canonical() {
244 let canonical = normalize_package_separator("File::Spec::Functions");
245 let components: Vec<&str> = canonical.split("::").collect();
246 assert_eq!(components, vec!["File", "Spec", "Functions"]);
247 }
248
249 #[test]
250 fn components_after_normalize_mixed_separators() {
251 let canonical = normalize_package_separator("A'B::C'D");
252 let components: Vec<&str> = canonical.split("::").collect();
253 assert_eq!(components, vec!["A", "B", "C", "D"]);
254 }
255
256 #[test]
261 fn normalized_name_converts_to_file_path_two_segments() {
262 let canonical = normalize_package_separator("Foo'Bar");
263 let path = canonical.replace("::", "/") + ".pm";
264 assert_eq!(path, "Foo/Bar.pm");
265 }
266
267 #[test]
268 fn normalized_name_converts_to_file_path_four_segments() {
269 let canonical = normalize_package_separator("Foo'Bar'Baz'Qux");
270 let path = canonical.replace("::", "/") + ".pm";
271 assert_eq!(path, "Foo/Bar/Baz/Qux.pm");
272 }
273
274 #[test]
275 fn normalized_name_converts_to_file_path_single_segment() {
276 let canonical = normalize_package_separator("strict");
277 let path = canonical.replace("::", "/") + ".pm";
278 assert_eq!(path, "strict.pm");
279 }
280
281 #[test]
282 fn normalized_name_converts_to_file_path_cpan_style() {
283 let canonical = normalize_package_separator("File'Spec'Functions");
284 let path = canonical.replace("::", "/") + ".pm";
285 assert_eq!(path, "File/Spec/Functions.pm");
286 }
287
288 #[test]
289 fn file_path_roundtrip_through_normalize() {
290 let module = "DateTime::Format::Strptime";
292 let path = module.replace("::", "/") + ".pm";
293 assert_eq!(path, "DateTime/Format/Strptime.pm");
294
295 let recovered = path.trim_end_matches(".pm").replace('/', "::");
297 assert_eq!(recovered, module);
298 }
299
300 #[test]
303 fn single_word_normalize_is_identity() {
304 let result = normalize_package_separator("Moose");
305 assert_eq!(result, "Moose");
306 assert!(matches!(result, Cow::Borrowed(_)));
307 }
308
309 #[test]
310 fn single_word_legacy_is_identity() {
311 let result = legacy_package_separator("Moose");
312 assert_eq!(result, "Moose");
313 assert!(matches!(result, Cow::Borrowed(_)));
314 }
315
316 #[test]
317 fn single_word_variant_pairs_produces_one_pair() {
318 let pairs = module_variant_pairs("Moose", "Moo");
319 assert_eq!(pairs.len(), 1);
320 assert_eq!(pairs[0], ("Moose".to_string(), "Moo".to_string()));
321 }
322
323 #[test]
326 fn normalize_trailing_double_colon_passthrough() {
327 let result = normalize_package_separator("Foo::Bar::");
329 assert_eq!(result, "Foo::Bar::");
330 assert!(matches!(result, Cow::Borrowed(_)));
331 }
332
333 #[test]
334 fn legacy_trailing_double_colon_converts() {
335 assert_eq!(legacy_package_separator("Foo::Bar::"), "Foo'Bar'");
336 }
337
338 #[test]
339 fn variant_pairs_trailing_separator() {
340 let pairs = module_variant_pairs("Foo::Bar::", "Baz::Qux::");
341 assert_eq!(pairs.len(), 2);
342 assert_eq!(pairs[0], ("Foo::Bar::".to_string(), "Baz::Qux::".to_string()));
343 assert_eq!(pairs[1], ("Foo'Bar'".to_string(), "Baz'Qux'".to_string()));
344 }
345
346 #[test]
349 fn normalize_leading_double_colon_passthrough() {
350 let result = normalize_package_separator("::Foo::Bar");
351 assert_eq!(result, "::Foo::Bar");
352 assert!(matches!(result, Cow::Borrowed(_)));
353 }
354
355 #[test]
356 fn legacy_leading_double_colon_converts() {
357 assert_eq!(legacy_package_separator("::Foo::Bar"), "'Foo'Bar");
358 }
359
360 #[test]
363 fn normalize_cpan_deeply_nested_catalyst() {
364 assert_eq!(
365 normalize_package_separator("Catalyst'Plugin'Authentication'Store"),
366 "Catalyst::Plugin::Authentication::Store"
367 );
368 }
369
370 #[test]
371 fn variant_pairs_cpan_moosex_types_rename() {
372 let pairs = module_variant_pairs("MooseX::Types::Structured", "Type::Tiny::Structured");
373 assert_eq!(pairs.len(), 2);
374 assert_eq!(
375 pairs[0],
376 ("MooseX::Types::Structured".to_string(), "Type::Tiny::Structured".to_string())
377 );
378 }
379
380 #[test]
381 fn variant_pairs_cpan_deeply_nested_six_segments() {
382 let pairs = module_variant_pairs(
383 "App::Prove::State::Result::Test",
384 "TAP::Harness::Result::Test::V2",
385 );
386 assert_eq!(pairs.len(), 2);
387 assert!(!pairs[0].0.contains('\''));
389 assert!(!pairs[0].1.contains('\''));
390 assert!(!pairs[1].0.contains("::"));
392 assert!(!pairs[1].1.contains("::"));
393 }
394}