1use amalgam_core::ir::{Import, Module};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct Resolution {
12 pub resolved_name: String,
14 pub required_import: Option<Import>,
16}
17
18#[derive(Debug, Clone, Default)]
20pub struct ResolutionContext {
21 pub current_group: Option<String>,
22 pub current_version: Option<String>,
23 pub current_kind: Option<String>,
24}
25
26pub struct TypeResolver {
28 cache: HashMap<String, Resolution>,
30 type_registry: HashMap<String, String>,
32}
33
34impl Default for TypeResolver {
35 fn default() -> Self {
36 Self::new()
37 }
38}
39
40impl TypeResolver {
41 pub fn new() -> Self {
42 let mut resolver = Self {
43 cache: HashMap::new(),
44 type_registry: HashMap::new(),
45 };
46
47 resolver.register_common_types();
49 resolver
50 }
51
52 fn register_common_types(&mut self) {
53 self.type_registry.insert(
55 "ObjectMeta".to_string(),
56 "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta".to_string(),
57 );
58 self.type_registry.insert(
59 "LabelSelector".to_string(),
60 "io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector".to_string(),
61 );
62 self.type_registry.insert(
63 "Time".to_string(),
64 "io.k8s.apimachinery.pkg.apis.meta.v1.Time".to_string(),
65 );
66
67 }
69
70 pub fn resolve(
72 &mut self,
73 reference: &str,
74 module: &Module,
75 _context: &ResolutionContext,
76 ) -> String {
77 if let Some(cached) = self.cache.get(reference) {
79 return cached.resolved_name.clone();
80 }
81
82 let full_reference = self
84 .type_registry
85 .get(reference)
86 .cloned()
87 .unwrap_or_else(|| reference.to_string());
88
89 for import in &module.imports {
91 if let Some(resolved) = self.try_resolve_with_import(&full_reference, import) {
92 self.cache.insert(
93 reference.to_string(),
94 Resolution {
95 resolved_name: resolved.clone(),
96 required_import: Some(import.clone()),
97 },
98 );
99 return resolved;
100 }
101 }
102
103 for type_def in &module.types {
105 if type_def.name == reference {
106 let resolution = Resolution {
107 resolved_name: reference.to_string(),
108 required_import: None,
109 };
110 self.cache.insert(reference.to_string(), resolution.clone());
111 return resolution.resolved_name;
112 }
113 }
114
115 reference.to_string()
117 }
118
119 fn try_resolve_with_import(&self, reference: &str, import: &Import) -> Option<String> {
121 let type_name = reference.split('/').next_back()?.split('.').next_back()?;
124
125 let import_info = self.parse_import_path(&import.path)?;
127
128 if self.import_matches_reference(&import_info, reference) {
130 let prefix = import.alias.as_ref().unwrap_or(&import_info.module_name);
132
133 return Some(format!("{}.{}", prefix, type_name));
134 }
135
136 None
137 }
138
139 fn parse_import_path(&self, path: &str) -> Option<ImportInfo> {
141 let path = path.trim_end_matches(".ncl");
143
144 let parts: Vec<&str> = path.split('/').collect();
146 if parts.is_empty() {
147 return None;
148 }
149
150 let clean_parts: Vec<&str> = parts
152 .iter()
153 .filter(|&&p| !p.is_empty() && p != ".." && p != ".")
154 .cloned()
155 .collect();
156
157 if clean_parts.is_empty() {
158 return None;
159 }
160
161 let module_name = if clean_parts.last() == Some(&"mod") && clean_parts.len() > 1 {
163 clean_parts[clean_parts.len() - 2]
165 } else {
166 clean_parts.last()?
167 };
168
169 let namespace = if clean_parts.len() > 1 {
171 clean_parts[..clean_parts.len() - 1].join(".")
172 } else {
173 String::new()
174 };
175
176 Some(ImportInfo {
177 module_name: module_name.to_string(),
178 namespace,
179 full_path: path.to_string(),
180 })
181 }
182
183 fn import_matches_reference(&self, import_info: &ImportInfo, reference: &str) -> bool {
185 if import_info.namespace.is_empty() {
193 return false;
194 }
195
196 let namespace_parts: Vec<&str> = import_info.namespace.split('.').collect();
198 namespace_parts.iter().any(|&part| reference.contains(part))
199 }
200}
201
202#[derive(Debug)]
203struct ImportInfo {
204 module_name: String,
205 namespace: String,
206 #[allow(dead_code)]
207 full_path: String,
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use amalgam_core::ir::Metadata;
214 use std::collections::BTreeMap;
215
216 fn create_test_module(name: &str, imports: Vec<Import>) -> Module {
217 Module {
218 name: name.to_string(),
219 imports,
220 types: vec![],
221 constants: vec![],
222 metadata: Metadata {
223 source_language: None,
224 source_file: None,
225 version: None,
226 generated_at: None,
227 custom: BTreeMap::new(),
228 },
229 }
230 }
231
232 #[test]
233 fn test_kubernetes_resolution() {
234 let mut resolver = TypeResolver::new();
235 let module = create_test_module(
236 "test",
237 vec![Import {
238 path: "../../../k8s.io/apimachinery/v1/mod.ncl".to_string(),
239 alias: Some("k8s_v1".to_string()),
240 items: vec![],
241 }],
242 );
243
244 let resolved = resolver.resolve(
245 "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
246 &module,
247 &ResolutionContext::default(),
248 );
249 assert_eq!(resolved, "k8s_v1.ObjectMeta");
250 }
251
252 #[test]
253 fn test_short_name_resolution() {
254 let mut resolver = TypeResolver::new();
255 let module = create_test_module(
256 "test",
257 vec![Import {
258 path: "../../../k8s.io/apimachinery/v1/mod.ncl".to_string(),
259 alias: Some("k8s_v1".to_string()),
260 items: vec![],
261 }],
262 );
263
264 let resolved = resolver.resolve("ObjectMeta", &module, &ResolutionContext::default());
266 assert_eq!(resolved, "k8s_v1.ObjectMeta");
267 }
268
269 #[test]
270 fn test_local_type_resolution() {
271 let mut resolver = TypeResolver::new();
272 let mut module = create_test_module("test", vec![]);
273
274 module.types.push(amalgam_core::ir::TypeDefinition {
276 name: "MyType".to_string(),
277 ty: amalgam_core::types::Type::String,
278 documentation: None,
279 annotations: BTreeMap::new(),
280 });
281
282 let resolved = resolver.resolve("MyType", &module, &ResolutionContext::default());
283 assert_eq!(resolved, "MyType");
284 }
285
286 #[test]
287 fn test_crossplane_resolution() {
288 let mut resolver = TypeResolver::new();
289 let module = create_test_module(
290 "test",
291 vec![Import {
292 path: "../../apiextensions.crossplane.io/v1/composition.ncl".to_string(),
293 alias: Some("crossplane_v1".to_string()),
294 items: vec![],
295 }],
296 );
297
298 let resolved = resolver.resolve(
299 "apiextensions.crossplane.io/v1/Composition",
300 &module,
301 &ResolutionContext::default(),
302 );
303
304 eprintln!("Crossplane resolution result: '{}'", resolved);
306
307 assert!(resolved.ends_with("Composition"));
310 assert!(resolved.contains("crossplane"));
311 }
312
313 #[test]
314 fn test_unresolved_type() {
315 let mut resolver = TypeResolver::new();
316 let module = create_test_module("test", vec![]);
317
318 let resolved = resolver.resolve("UnknownType", &module, &ResolutionContext::default());
320 assert_eq!(resolved, "UnknownType");
321 }
322}