1use std::collections::HashMap;
7use amalgam_core::ir::{Module, Import};
8
9#[derive(Debug, Clone)]
11pub struct Resolution {
12 pub resolved_name: String,
14 pub required_import: Option<Import>,
16}
17
18pub trait ReferenceResolver: Send + Sync {
23 fn can_resolve(&self, reference: &str) -> bool;
25
26 fn resolve(
28 &self,
29 reference: &str,
30 imports: &[Import],
31 context: &ResolutionContext,
32 ) -> Option<Resolution>;
33
34 fn parse_import_path(&self, path: &str) -> Option<ImportMetadata>;
37
38 fn name(&self) -> &str;
40}
41
42#[derive(Debug, Clone)]
43pub struct ImportMetadata {
44 pub group: String,
45 pub version: String,
46 pub kind: Option<String>,
47 pub is_module: bool,
48}
49
50#[derive(Debug, Clone)]
51pub struct ResolutionContext {
52 pub current_group: Option<String>,
54 pub current_version: Option<String>,
56 pub current_kind: Option<String>,
58}
59
60pub struct TypeResolver {
62 resolvers: Vec<Box<dyn ReferenceResolver>>,
64 cache: HashMap<String, Resolution>,
66}
67
68impl TypeResolver {
69 pub fn new() -> Self {
70 let mut resolver = Self {
71 resolvers: Vec::new(),
72 cache: HashMap::new(),
73 };
74
75 resolver.register(Box::new(KubernetesResolver::new()));
77 resolver.register(Box::new(LocalTypeResolver::new()));
78 resolver
81 }
82
83 pub fn register(&mut self, resolver: Box<dyn ReferenceResolver>) {
85 self.resolvers.push(resolver);
86 }
87
88 pub fn resolve(
90 &mut self,
91 reference: &str,
92 module: &Module,
93 context: &ResolutionContext,
94 ) -> String {
95 if let Some(cached) = self.cache.get(reference) {
97 tracing::trace!("TypeResolver: cache hit for '{}'", reference);
98 return cached.resolved_name.clone();
99 }
100
101 tracing::trace!("TypeResolver: resolving '{}' with {} imports", reference, module.imports.len());
102
103 for resolver in &self.resolvers {
105 if resolver.can_resolve(reference) {
106 tracing::trace!(" Trying resolver: {}", resolver.name());
107 if let Some(resolution) = resolver.resolve(reference, &module.imports, context) {
108 tracing::debug!("TypeResolver: resolved '{}' -> '{}'", reference, resolution.resolved_name);
109 self.cache.insert(reference.to_string(), resolution.clone());
110 return resolution.resolved_name;
111 }
112 }
113 }
114
115 tracing::trace!("TypeResolver: no resolver handled '{}', returning as-is", reference);
116 reference.to_string()
118 }
119
120 pub fn clear_cache(&mut self) {
122 self.cache.clear();
123 }
124}
125
126struct KubernetesResolver {
131 known_types: HashMap<String, String>,
133}
134
135impl KubernetesResolver {
136 fn new() -> Self {
137 let mut known_types = HashMap::new();
138
139 known_types.insert("ObjectMeta".to_string(), "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta".to_string());
141 known_types.insert("ListMeta".to_string(), "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta".to_string());
142 known_types.insert("TypeMeta".to_string(), "io.k8s.apimachinery.pkg.apis.meta.v1.TypeMeta".to_string());
143 known_types.insert("LabelSelector".to_string(), "io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector".to_string());
144 Self { known_types }
147 }
148}
149
150impl ReferenceResolver for KubernetesResolver {
151 fn can_resolve(&self, reference: &str) -> bool {
152 reference.starts_with("io.k8s.")
153 || reference.contains("k8s.io")
154 || self.known_types.contains_key(reference)
155 }
156
157 fn resolve(
158 &self,
159 reference: &str,
160 imports: &[Import],
161 _context: &ResolutionContext,
162 ) -> Option<Resolution> {
163 let full_reference = if let Some(full_name) = self.known_types.get(reference) {
165 full_name.clone()
166 } else {
167 reference.to_string()
168 };
169
170 tracing::trace!("KubernetesResolver: resolving '{}' (full: '{}')", reference, full_reference);
171
172 for import in imports {
174 tracing::trace!(" Checking import: path='{}', alias={:?}", import.path, import.alias);
175 if let Some(metadata) = self.parse_import_path(&import.path) {
176 tracing::trace!(" Parsed metadata: group={}, version={}, kind={:?}",
177 metadata.group, metadata.version, metadata.kind);
178 if self.import_provides_type(&metadata, &full_reference) {
180 let alias = import.alias.as_ref().unwrap_or(&metadata.group);
181 let type_name = full_reference.split('.').last().unwrap_or(&full_reference);
182
183 tracing::debug!(" Resolved '{}' to '{}.{}'", reference, alias, type_name);
184 return Some(Resolution {
185 resolved_name: format!("{}.{}", alias, type_name),
186 required_import: Some(import.clone()),
187 });
188 } else {
189 tracing::trace!(" No match (import_provides_type returned false)");
190 }
191 } else {
192 tracing::trace!(" Could not parse import path");
193 }
194 }
195
196 None
197 }
198
199 fn parse_import_path(&self, path: &str) -> Option<ImportMetadata> {
200 if !path.contains("k8s_io") && !path.contains("k8s.io") {
202 return None;
203 }
204
205 let parts: Vec<&str> = path.split('/').collect();
206
207 if let Some(k8s_idx) = parts.iter().position(|&p| p == "k8s_io" || p == "k8s.io") {
209 if k8s_idx + 2 < parts.len() {
211 let version = parts[k8s_idx + 1].to_string();
212 let filename = parts[k8s_idx + 2];
213
214 let (kind, is_module) = if filename == "mod.ncl" {
215 (None, true)
216 } else {
217 let kind_name = filename.strip_suffix(".ncl")?;
218 (Some(capitalize_first(kind_name)), false)
219 };
220
221 return Some(ImportMetadata {
222 group: "k8s.io".to_string(), version,
224 kind,
225 is_module,
226 });
227 }
228 }
229
230 None
231 }
232
233 fn name(&self) -> &str {
234 "KubernetesResolver"
235 }
236}
237
238impl KubernetesResolver {
239 fn import_provides_type(&self, metadata: &ImportMetadata, reference: &str) -> bool {
240 if let Some(ref kind) = metadata.kind {
242 let ref_kind = reference.split('.').last().unwrap_or("");
245 ref_kind.eq_ignore_ascii_case(kind)
246 } else {
247 reference.contains(&metadata.version)
249 }
250 }
251}
252
253struct LocalTypeResolver;
258
259impl LocalTypeResolver {
260 fn new() -> Self {
261 Self
262 }
263}
264
265impl ReferenceResolver for LocalTypeResolver {
266 fn can_resolve(&self, reference: &str) -> bool {
267 !reference.contains('.') && !reference.contains('/')
269 }
270
271 fn resolve(
272 &self,
273 reference: &str,
274 _imports: &[Import],
275 _context: &ResolutionContext,
276 ) -> Option<Resolution> {
277 Some(Resolution {
279 resolved_name: reference.to_string(),
280 required_import: None,
281 })
282 }
283
284 fn parse_import_path(&self, _path: &str) -> Option<ImportMetadata> {
285 None }
287
288 fn name(&self) -> &str {
289 "LocalTypeResolver"
290 }
291}
292
293fn capitalize_first(s: &str) -> String {
304 let mut chars = s.chars();
305 match chars.next() {
306 None => String::new(),
307 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
308 }
309}
310
311impl Default for TypeResolver {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_kubernetes_resolution() {
323 let mut resolver = TypeResolver::new();
324 let module = Module {
325 name: "test".to_string(),
326 imports: vec![Import {
327 path: "../../../k8s.io/apimachinery/v1/mod.ncl".to_string(),
328 alias: Some("k8s_v1".to_string()),
329 items: vec![],
330 }],
331 types: vec![],
332 constants: vec![],
333 metadata: Default::default(),
334 };
335
336 let context = ResolutionContext {
337 current_group: None,
338 current_version: None,
339 current_kind: None,
340 };
341
342 let resolved = resolver.resolve("io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", &module, &context);
343 assert_eq!(resolved, "k8s_v1.ObjectMeta");
344 }
345
346 #[test]
347 fn test_local_type_resolution() {
348 let mut resolver = TypeResolver::new();
349 let module = Module {
350 name: "test".to_string(),
351 imports: vec![],
352 types: vec![],
353 constants: vec![],
354 metadata: Default::default(),
355 };
356
357 let context = ResolutionContext {
358 current_group: None,
359 current_version: None,
360 current_kind: None,
361 };
362
363 let resolved = resolver.resolve("MyLocalType", &module, &context);
364 assert_eq!(resolved, "MyLocalType");
365 }
366}