apcore_toolkit/
resolve_target.rs1use regex::Regex;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ResolvedTarget {
13 pub module_path: String,
15 pub qualname: String,
17}
18
19pub fn resolve_target(target: &str) -> Result<ResolvedTarget, String> {
48 let last_colon = target.rfind(':').ok_or_else(|| {
49 format!("Invalid target format: \"{target}\". Expected \"module_path:qualname\".")
50 })?;
51
52 let module_path = &target[..last_colon];
53 let qualname = &target[last_colon + 1..];
54
55 if module_path.is_empty() {
56 return Err(format!(
57 "Invalid target format: \"{target}\". Module path is empty."
58 ));
59 }
60
61 if qualname.is_empty() {
62 return Err(format!(
63 "Invalid target format: \"{target}\". Qualified name is empty."
64 ));
65 }
66
67 let ident_re = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap();
69 if !ident_re.is_match(qualname) {
70 return Err(format!(
71 "Invalid qualname \"{qualname}\" in target \"{target}\". \
72 Must be a valid identifier."
73 ));
74 }
75
76 Ok(ResolvedTarget {
77 module_path: module_path.to_string(),
78 qualname: qualname.to_string(),
79 })
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_resolve_target_python_style() {
88 let result = resolve_target("my_package.my_module:MyClass").unwrap();
89 assert_eq!(result.module_path, "my_package.my_module");
90 assert_eq!(result.qualname, "MyClass");
91 }
92
93 #[test]
94 fn test_resolve_target_rust_style() {
95 let result = resolve_target("my_crate::handlers::task:create_task").unwrap();
96 assert_eq!(result.module_path, "my_crate::handlers::task");
97 assert_eq!(result.qualname, "create_task");
98 }
99
100 #[test]
101 fn test_resolve_target_simple() {
102 let result = resolve_target("app:handler").unwrap();
103 assert_eq!(result.module_path, "app");
104 assert_eq!(result.qualname, "handler");
105 }
106
107 #[test]
108 fn test_resolve_target_typescript_style() {
109 let result = resolve_target("./handlers/task:createTask").unwrap();
110 assert_eq!(result.module_path, "./handlers/task");
111 assert_eq!(result.qualname, "createTask");
112 }
113
114 #[test]
115 fn test_resolve_target_no_colon() {
116 let result = resolve_target("no_colon_here");
117 assert!(result.is_err());
118 assert!(result.unwrap_err().contains("Invalid target format"));
119 }
120
121 #[test]
122 fn test_resolve_target_empty_module() {
123 let result = resolve_target(":qualname");
124 assert!(result.is_err());
125 assert!(result.unwrap_err().contains("Module path is empty"));
126 }
127
128 #[test]
129 fn test_resolve_target_empty_qualname() {
130 let result = resolve_target("module:");
131 assert!(result.is_err());
132 assert!(result.unwrap_err().contains("Qualified name is empty"));
133 }
134
135 #[test]
136 fn test_resolve_target_invalid_qualname() {
137 let result = resolve_target("module:123invalid");
138 assert!(result.is_err());
139 assert!(result.unwrap_err().contains("Must be a valid identifier"));
140 }
141
142 #[test]
143 fn test_resolve_target_qualname_with_spaces() {
144 let result = resolve_target("module:has spaces");
145 assert!(result.is_err());
146 }
147
148 #[test]
149 fn test_resolve_target_node_prefix() {
150 let result = resolve_target("node:path:join").unwrap();
152 assert_eq!(result.module_path, "node:path");
153 assert_eq!(result.qualname, "join");
154 }
155
156 #[test]
157 fn test_resolve_target_underscore_qualname() {
158 let result = resolve_target("mod:_private_func").unwrap();
159 assert_eq!(result.qualname, "_private_func");
160 }
161}