#![deny(unsafe_code)]
#![warn(rust_2018_idioms)]
#![warn(missing_docs)]
#![warn(clippy::all)]
use std::borrow::Cow;
#[must_use]
pub fn normalize_package_separator(module_name: &str) -> Cow<'_, str> {
if module_name.contains('\'') {
Cow::Owned(module_name.replace('\'', "::"))
} else {
Cow::Borrowed(module_name)
}
}
#[must_use]
pub fn legacy_package_separator(module_name: &str) -> Cow<'_, str> {
if module_name.contains("::") {
Cow::Owned(module_name.replace("::", "'"))
} else {
Cow::Borrowed(module_name)
}
}
#[must_use]
pub fn module_variant_pairs(old_module: &str, new_module: &str) -> Vec<(String, String)> {
let canonical_old = normalize_package_separator(old_module).into_owned();
let canonical_new = normalize_package_separator(new_module).into_owned();
let canonical = (canonical_old.clone(), canonical_new.clone());
let legacy = (
legacy_package_separator(&canonical_old).into_owned(),
legacy_package_separator(&canonical_new).into_owned(),
);
if legacy == canonical { vec![canonical] } else { vec![canonical, legacy] }
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::{legacy_package_separator, module_variant_pairs, normalize_package_separator};
#[test]
fn normalizes_legacy_separator() {
assert_eq!(normalize_package_separator("Foo'Bar"), "Foo::Bar");
assert_eq!(normalize_package_separator("Foo::Bar"), "Foo::Bar");
}
#[test]
fn projects_canonical_separator_to_legacy() {
assert_eq!(legacy_package_separator("Foo::Bar"), "Foo'Bar");
assert_eq!(legacy_package_separator("Foo'Bar"), "Foo'Bar");
}
#[test]
fn builds_canonical_and_legacy_variant_pairs() {
assert_eq!(
module_variant_pairs("Foo::Bar", "New::Path"),
vec![
("Foo::Bar".to_string(), "New::Path".to_string()),
("Foo'Bar".to_string(), "New'Path".to_string()),
]
);
}
#[test]
fn deduplicates_pair_when_no_separator_variants_exist() {
assert_eq!(module_variant_pairs("strict", "warnings").len(), 1);
}
#[test]
fn normalize_simple_bare_name_foo() {
let result = normalize_package_separator("Foo");
assert_eq!(result, "Foo");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn normalize_simple_two_segment_foo_bar() {
assert_eq!(normalize_package_separator("Foo'Bar"), "Foo::Bar");
}
#[test]
fn legacy_simple_two_segment_foo_bar() {
assert_eq!(legacy_package_separator("Foo::Bar"), "Foo'Bar");
}
#[test]
fn legacy_simple_bare_name_foo() {
let result = legacy_package_separator("Foo");
assert_eq!(result, "Foo");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn normalize_four_segments_deeply_nested() {
assert_eq!(normalize_package_separator("Foo'Bar'Baz'Qux"), "Foo::Bar::Baz::Qux");
}
#[test]
fn legacy_four_segments_deeply_nested() {
assert_eq!(legacy_package_separator("Foo::Bar::Baz::Qux"), "Foo'Bar'Baz'Qux");
}
#[test]
fn variant_pairs_four_segments_produces_both_forms() {
let pairs = module_variant_pairs("Foo::Bar::Baz::Qux", "One::Two::Three::Four");
assert_eq!(pairs.len(), 2);
assert_eq!(
pairs[0],
("Foo::Bar::Baz::Qux".to_string(), "One::Two::Three::Four".to_string())
);
assert_eq!(pairs[1], ("Foo'Bar'Baz'Qux".to_string(), "One'Two'Three'Four".to_string()));
}
#[test]
fn roundtrip_four_segment_normalize_then_legacy() {
let input = "Foo'Bar'Baz'Qux";
let canonical = normalize_package_separator(input);
assert_eq!(canonical, "Foo::Bar::Baz::Qux");
let back = legacy_package_separator(&canonical);
assert_eq!(back, input);
}
#[test]
fn roundtrip_four_segment_legacy_then_normalize() {
let input = "Foo::Bar::Baz::Qux";
let legacy = legacy_package_separator(input);
assert_eq!(legacy, "Foo'Bar'Baz'Qux");
let back = normalize_package_separator(&legacy);
assert_eq!(back, input);
}
#[test]
fn normalize_preserves_content_with_no_separators() {
for name in &["DBI", "strict", "warnings", "utf8", "POSIX", "Carp"] {
let result = normalize_package_separator(name);
assert_eq!(result.as_ref(), *name);
assert!(matches!(result, Cow::Borrowed(_)));
}
}
#[test]
fn legacy_preserves_content_with_no_separators() {
for name in &["DBI", "strict", "warnings", "utf8", "POSIX", "Carp"] {
let result = legacy_package_separator(name);
assert_eq!(result.as_ref(), *name);
assert!(matches!(result, Cow::Borrowed(_)));
}
}
#[test]
fn normalize_handles_only_identifier_chars() {
assert_eq!(normalize_package_separator("_Private'_Internal"), "_Private::_Internal");
}
#[test]
fn normalize_handles_numeric_only_segments() {
assert_eq!(normalize_package_separator("V5'V6"), "V5::V6");
}
#[test]
fn components_after_normalize_two_segments() {
let canonical = normalize_package_separator("Foo'Bar");
let components: Vec<&str> = canonical.split("::").collect();
assert_eq!(components, vec!["Foo", "Bar"]);
}
#[test]
fn components_after_normalize_four_segments() {
let canonical = normalize_package_separator("Foo'Bar'Baz'Qux");
let components: Vec<&str> = canonical.split("::").collect();
assert_eq!(components, vec!["Foo", "Bar", "Baz", "Qux"]);
}
#[test]
fn components_after_normalize_single_segment() {
let canonical = normalize_package_separator("strict");
let components: Vec<&str> = canonical.split("::").collect();
assert_eq!(components, vec!["strict"]);
}
#[test]
fn components_after_normalize_already_canonical() {
let canonical = normalize_package_separator("File::Spec::Functions");
let components: Vec<&str> = canonical.split("::").collect();
assert_eq!(components, vec!["File", "Spec", "Functions"]);
}
#[test]
fn components_after_normalize_mixed_separators() {
let canonical = normalize_package_separator("A'B::C'D");
let components: Vec<&str> = canonical.split("::").collect();
assert_eq!(components, vec!["A", "B", "C", "D"]);
}
#[test]
fn normalized_name_converts_to_file_path_two_segments() {
let canonical = normalize_package_separator("Foo'Bar");
let path = canonical.replace("::", "/") + ".pm";
assert_eq!(path, "Foo/Bar.pm");
}
#[test]
fn normalized_name_converts_to_file_path_four_segments() {
let canonical = normalize_package_separator("Foo'Bar'Baz'Qux");
let path = canonical.replace("::", "/") + ".pm";
assert_eq!(path, "Foo/Bar/Baz/Qux.pm");
}
#[test]
fn normalized_name_converts_to_file_path_single_segment() {
let canonical = normalize_package_separator("strict");
let path = canonical.replace("::", "/") + ".pm";
assert_eq!(path, "strict.pm");
}
#[test]
fn normalized_name_converts_to_file_path_cpan_style() {
let canonical = normalize_package_separator("File'Spec'Functions");
let path = canonical.replace("::", "/") + ".pm";
assert_eq!(path, "File/Spec/Functions.pm");
}
#[test]
fn file_path_roundtrip_through_normalize() {
let module = "DateTime::Format::Strptime";
let path = module.replace("::", "/") + ".pm";
assert_eq!(path, "DateTime/Format/Strptime.pm");
let recovered = path.trim_end_matches(".pm").replace('/', "::");
assert_eq!(recovered, module);
}
#[test]
fn single_word_normalize_is_identity() {
let result = normalize_package_separator("Moose");
assert_eq!(result, "Moose");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn single_word_legacy_is_identity() {
let result = legacy_package_separator("Moose");
assert_eq!(result, "Moose");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn single_word_variant_pairs_produces_one_pair() {
let pairs = module_variant_pairs("Moose", "Moo");
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0], ("Moose".to_string(), "Moo".to_string()));
}
#[test]
fn normalize_trailing_double_colon_passthrough() {
let result = normalize_package_separator("Foo::Bar::");
assert_eq!(result, "Foo::Bar::");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn legacy_trailing_double_colon_converts() {
assert_eq!(legacy_package_separator("Foo::Bar::"), "Foo'Bar'");
}
#[test]
fn variant_pairs_trailing_separator() {
let pairs = module_variant_pairs("Foo::Bar::", "Baz::Qux::");
assert_eq!(pairs.len(), 2);
assert_eq!(pairs[0], ("Foo::Bar::".to_string(), "Baz::Qux::".to_string()));
assert_eq!(pairs[1], ("Foo'Bar'".to_string(), "Baz'Qux'".to_string()));
}
#[test]
fn normalize_leading_double_colon_passthrough() {
let result = normalize_package_separator("::Foo::Bar");
assert_eq!(result, "::Foo::Bar");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn legacy_leading_double_colon_converts() {
assert_eq!(legacy_package_separator("::Foo::Bar"), "'Foo'Bar");
}
#[test]
fn normalize_cpan_deeply_nested_catalyst() {
assert_eq!(
normalize_package_separator("Catalyst'Plugin'Authentication'Store"),
"Catalyst::Plugin::Authentication::Store"
);
}
#[test]
fn variant_pairs_cpan_moosex_types_rename() {
let pairs = module_variant_pairs("MooseX::Types::Structured", "Type::Tiny::Structured");
assert_eq!(pairs.len(), 2);
assert_eq!(
pairs[0],
("MooseX::Types::Structured".to_string(), "Type::Tiny::Structured".to_string())
);
}
#[test]
fn variant_pairs_cpan_deeply_nested_six_segments() {
let pairs = module_variant_pairs(
"App::Prove::State::Result::Test",
"TAP::Harness::Result::Test::V2",
);
assert_eq!(pairs.len(), 2);
assert!(!pairs[0].0.contains('\''));
assert!(!pairs[0].1.contains('\''));
assert!(!pairs[1].0.contains("::"));
assert!(!pairs[1].1.contains("::"));
}
}