perl_qualified_name/
lib.rs1#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11use core::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum QualifiedNameError {
16 EmptyName,
18 LeadingSigil(char),
20 EmptySegment {
22 index: usize,
24 },
25 InvalidSegment {
27 index: usize,
29 },
30}
31
32impl fmt::Display for QualifiedNameError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::EmptyName => write!(f, "name is empty"),
36 Self::LeadingSigil(sigil) => {
37 write!(f, "qualified name cannot start with sigil '{sigil}'")
38 }
39 Self::EmptySegment { index } => {
40 write!(f, "segment {index} is empty (leading/trailing/double separator)")
41 }
42 Self::InvalidSegment { index } => {
43 write!(f, "segment {index} is not a valid identifier")
44 }
45 }
46 }
47}
48
49impl std::error::Error for QualifiedNameError {}
50
51#[must_use]
56pub fn split_qualified_name(name: &str) -> (Option<&str>, &str) {
57 if let Some(idx) = name.rfind("::") {
58 (Some(&name[..idx]), &name[idx + 2..])
59 } else {
60 (None, name)
61 }
62}
63
64#[must_use]
69pub fn container_name(name: &str) -> Option<&str> {
70 split_qualified_name(name).0
71}
72
73pub fn validate_perl_qualified_name(name: &str) -> Result<(), QualifiedNameError> {
80 if name.is_empty() {
81 return Err(QualifiedNameError::EmptyName);
82 }
83
84 if name.starts_with(['$', '@', '%', '&', '*']) {
85 let sigil = name.chars().next().unwrap_or_default();
86 return Err(QualifiedNameError::LeadingSigil(sigil));
87 }
88
89 for (idx, part) in name.split("::").enumerate() {
90 if part.is_empty() {
91 return Err(QualifiedNameError::EmptySegment { index: idx });
92 }
93
94 if !is_valid_identifier_part(part) {
95 return Err(QualifiedNameError::InvalidSegment { index: idx });
96 }
97 }
98
99 Ok(())
100}
101
102#[must_use]
104pub fn is_valid_identifier_part(s: &str) -> bool {
105 let mut chars = s.chars();
106 match chars.next() {
107 Some(c) if c.is_alphabetic() || c == '_' => chars.all(|c| c.is_alphanumeric() || c == '_'),
108 _ => false,
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::{
115 container_name, is_valid_identifier_part, split_qualified_name,
116 validate_perl_qualified_name,
117 };
118
119 #[test]
120 fn splits_qualified_and_unqualified_names() {
121 assert_eq!(split_qualified_name("Foo::Bar"), (Some("Foo"), "Bar"));
122 assert_eq!(split_qualified_name("process"), (None, "process"));
123 }
124
125 #[test]
126 fn extracts_container_name() {
127 assert_eq!(container_name("Foo::Bar::baz"), Some("Foo::Bar"));
128 assert_eq!(container_name("MyClass::new"), Some("MyClass"));
129 assert_eq!(container_name("toplevel"), None);
130 assert_eq!(container_name(""), None);
131 assert_eq!(container_name("Package::"), Some("Package"));
132 }
133
134 #[test]
135 fn validates_simple_qualified_names() {
136 assert!(validate_perl_qualified_name("Package").is_ok());
137 assert!(validate_perl_qualified_name("Package::Sub").is_ok());
138 assert!(validate_perl_qualified_name("Müller::Util").is_ok());
139 assert!(validate_perl_qualified_name("日本::パッケージ").is_ok());
140 }
141
142 #[test]
143 fn rejects_invalid_qualified_names() {
144 assert!(validate_perl_qualified_name("").is_err());
145 assert!(validate_perl_qualified_name("Package::").is_err());
146 assert!(validate_perl_qualified_name("Foo::::Bar").is_err());
147 assert!(validate_perl_qualified_name("$foo").is_err());
148 }
149
150 #[test]
151 fn validates_identifier_part_syntax() {
152 assert!(is_valid_identifier_part("MyPkg"));
153 assert!(is_valid_identifier_part("π"));
154 assert!(!is_valid_identifier_part("1abc"));
155 assert!(!is_valid_identifier_part(""));
156 }
157}