Skip to main content

perl_module_path/
lib.rs

1//! Perl module name/path conversion helpers.
2//!
3//! This crate provides a small, focused API for converting between canonical
4//! Perl module names (for example, `Foo::Bar`) and module file paths
5//! (for example, `Foo/Bar.pm`).
6
7use std::borrow::Cow;
8
9/// Normalize legacy package separator `'` to canonical `::`.
10///
11/// # Examples
12///
13/// ```
14/// use perl_module_path::normalize_package_separator;
15///
16/// assert_eq!(normalize_package_separator("Foo'Bar"), "Foo::Bar");
17/// assert_eq!(normalize_package_separator("Foo::Bar"), "Foo::Bar");
18/// ```
19#[must_use]
20pub fn normalize_package_separator(module_name: &str) -> Cow<'_, str> {
21    perl_module_name::normalize_package_separator(module_name)
22}
23
24/// Convert a module name into a relative Perl module path.
25///
26/// # Examples
27///
28/// ```
29/// use perl_module_path::module_name_to_path;
30///
31/// assert_eq!(module_name_to_path("Foo::Bar"), "Foo/Bar.pm");
32/// assert_eq!(module_name_to_path("strict"), "strict.pm");
33/// ```
34#[must_use]
35pub fn module_name_to_path(module_name: &str) -> String {
36    let normalized = normalize_package_separator(module_name);
37    format!("{}.pm", normalized.replace("::", "/"))
38}
39
40/// Convert a module path/key into a module name.
41///
42/// Handles both `/` and `\\` separators and strips `.pm`/`.pl` suffixes.
43///
44/// # Examples
45///
46/// ```
47/// use perl_module_path::module_path_to_name;
48///
49/// assert_eq!(module_path_to_name("Foo/Bar.pm"), "Foo::Bar");
50/// assert_eq!(module_path_to_name(r"Foo\Bar.pm"), "Foo::Bar");
51/// assert_eq!(module_path_to_name("script.pl"), "script");
52/// ```
53#[must_use]
54pub fn module_path_to_name(module_path: &str) -> String {
55    let normalized = module_path.replace('\\', "/");
56    let without_ext = strip_perl_extension(&normalized);
57    without_ext.replace('/', "::")
58}
59
60/// Convert a filesystem source path into a likely module name.
61///
62/// This is intended for file-rename workflows where a concrete source path
63/// needs to map back to the module import name. It follows these rules:
64///
65/// 1. Strip `.pm` or `.pl` suffix
66/// 2. If a `lib/` segment exists, use everything after the last `lib/`
67/// 3. Otherwise, fall back to the file stem
68///
69/// # Examples
70///
71/// ```
72/// use perl_module_path::file_path_to_module_name;
73///
74/// assert_eq!(file_path_to_module_name("/workspace/lib/Foo/Bar.pm"), "Foo::Bar");
75/// assert_eq!(file_path_to_module_name("/workspace/script.pl"), "script");
76/// assert_eq!(file_path_to_module_name(r"C:\workspace\lib\Foo\Bar.pm"), "Foo::Bar");
77/// ```
78#[must_use]
79pub fn file_path_to_module_name(file_path: &str) -> String {
80    let normalized = file_path.replace('\\', "/");
81    let without_ext = strip_perl_extension(&normalized);
82
83    if let Some(relative_module_path) = strip_to_lib_relative_path(without_ext) {
84        return module_path_to_name(relative_module_path);
85    }
86
87    without_ext
88        .rsplit('/')
89        .next()
90        .filter(|segment| !segment.is_empty())
91        .unwrap_or(without_ext)
92        .to_string()
93}
94
95fn strip_to_lib_relative_path(path: &str) -> Option<&str> {
96    if let Some(stripped) = path.strip_prefix("lib/") {
97        return Some(stripped);
98    }
99
100    path.rfind("/lib/").map(|lib_idx| &path[lib_idx + "/lib/".len()..])
101}
102
103fn strip_perl_extension(path: &str) -> &str {
104    if let Some(stripped) = path.strip_suffix(".pm") {
105        stripped
106    } else if let Some(stripped) = path.strip_suffix(".pl") {
107        stripped
108    } else {
109        path
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::{
116        file_path_to_module_name, module_name_to_path, module_path_to_name,
117        normalize_package_separator,
118    };
119
120    #[test]
121    fn normalizes_legacy_package_separator() {
122        assert_eq!(normalize_package_separator("Foo'Bar"), "Foo::Bar");
123        assert_eq!(normalize_package_separator("Foo::Bar"), "Foo::Bar");
124    }
125
126    #[test]
127    fn converts_module_name_to_path() {
128        assert_eq!(module_name_to_path("Foo::Bar"), "Foo/Bar.pm");
129        assert_eq!(module_name_to_path("App::Config::Loader"), "App/Config/Loader.pm");
130        assert_eq!(module_name_to_path("Legacy'Package"), "Legacy/Package.pm");
131    }
132
133    #[test]
134    fn converts_module_path_to_name() {
135        assert_eq!(module_path_to_name("Foo/Bar.pm"), "Foo::Bar");
136        assert_eq!(module_path_to_name("lib/Foo/Bar.pm"), "lib::Foo::Bar");
137    }
138
139    #[test]
140    fn converts_windows_module_path_to_name() {
141        assert_eq!(module_path_to_name(r"Foo\Bar.pm"), "Foo::Bar");
142        assert_eq!(module_path_to_name(r"lib\Foo\Bar.pm"), "lib::Foo::Bar");
143    }
144
145    #[test]
146    fn strips_perl_extensions() {
147        assert_eq!(module_path_to_name("Foo/Bar.pm"), "Foo::Bar");
148        assert_eq!(module_path_to_name("script.pl"), "script");
149    }
150
151    #[test]
152    fn round_trips_common_module_name() {
153        let module = "MyApp::Service::Email";
154        let path = module_name_to_path(module);
155        assert_eq!(module_path_to_name(&path), module);
156    }
157
158    #[test]
159    fn converts_filesystem_path_with_lib_segment_to_module_name() {
160        assert_eq!(file_path_to_module_name("/workspace/lib/Foo/Bar.pm"), "Foo::Bar");
161        assert_eq!(file_path_to_module_name("lib/My/App.pm"), "My::App");
162    }
163
164    #[test]
165    fn converts_windows_filesystem_path_with_lib_segment_to_module_name() {
166        assert_eq!(file_path_to_module_name(r"C:\workspace\lib\Foo\Bar.pm"), "Foo::Bar");
167    }
168
169    #[test]
170    fn falls_back_to_file_stem_when_lib_segment_missing() {
171        assert_eq!(file_path_to_module_name("/workspace/scripts/sync_worker.pl"), "sync_worker");
172        assert_eq!(file_path_to_module_name("MyModule.pm"), "MyModule");
173    }
174}