Skip to main content

perl_uri_classify/
lib.rs

1//! URI classification and key normalization helpers.
2//!
3//! This crate centralizes URI helpers that are frequently reused by LSP-facing
4//! crates while keeping filesystem URI conversion concerns in `perl-uri`.
5
6#![deny(unsafe_code)]
7#![warn(missing_docs)]
8#![warn(clippy::all)]
9
10use url::Url;
11
12/// Normalize a URI to a consistent key for lookups.
13///
14/// This function handles platform-specific differences to ensure consistent
15/// lookups across different systems, particularly for Windows drive letters.
16#[must_use]
17pub fn uri_key(uri: &str) -> String {
18    if let Ok(parsed) = Url::parse(uri) {
19        let value = parsed.as_str().to_string();
20        if let Some(rest) = value.strip_prefix("file:///")
21            && rest.len() > 1
22            && rest.as_bytes()[1] == b':'
23            && rest.as_bytes()[0].is_ascii_alphabetic()
24        {
25            return format!("file:///{}{}", rest[0..1].to_ascii_lowercase(), &rest[1..]);
26        }
27        value
28    } else {
29        uri.to_string()
30    }
31}
32
33/// Check if a URI uses the `file://` scheme.
34#[must_use]
35pub fn is_file_uri(uri: &str) -> bool {
36    uri.starts_with("file://")
37}
38
39/// Check if a URI uses a special scheme (not `file://`).
40#[must_use]
41pub fn is_special_scheme(uri: &str) -> bool {
42    if let Ok(url) = Url::parse(uri) {
43        url.scheme() != "file"
44    } else {
45        uri.starts_with("untitled:")
46            || uri.starts_with("git:")
47            || uri.starts_with("vscode-notebook:")
48            || uri.starts_with("vscode-vfs:")
49    }
50}
51
52/// Extract the file extension from a URI-like string.
53#[must_use]
54pub fn uri_extension(uri: &str) -> Option<&str> {
55    let path_part = uri.rsplit('/').next()?;
56    let path_part = path_part.split('?').next()?;
57    let path_part = path_part.split('#').next()?;
58    let dot_pos = path_part.rfind('.')?;
59    let ext = &path_part[dot_pos + 1..];
60    if ext.is_empty() { None } else { Some(ext) }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::{is_file_uri, is_special_scheme, uri_extension, uri_key};
66
67    #[test]
68    fn normalizes_uri_keys() {
69        assert_eq!(uri_key("file:///tmp/test.pl"), "file:///tmp/test.pl");
70        assert_eq!(uri_key("file:///C:/Users/test.pl"), "file:///c:/Users/test.pl");
71    }
72
73    #[test]
74    fn preserves_invalid_uri_values() {
75        assert_eq!(uri_key("not-a-uri"), "not-a-uri");
76    }
77
78    #[test]
79    fn detects_file_uris() {
80        assert!(is_file_uri("file:///tmp/test.pl"));
81        assert!(!is_file_uri("https://example.com"));
82    }
83
84    #[test]
85    fn detects_special_schemes() {
86        assert!(is_special_scheme("untitled:Untitled-1"));
87        assert!(is_special_scheme("git:/foo/bar"));
88        assert!(!is_special_scheme("file:///tmp/test.pl"));
89    }
90
91    #[test]
92    fn extracts_extensions() {
93        assert_eq!(uri_extension("file:///tmp/test.pl"), Some("pl"));
94        assert_eq!(uri_extension("file:///tmp/file.pl?query=1"), Some("pl"));
95        assert_eq!(uri_extension("file:///tmp/no-extension"), None);
96    }
97}