amalgam_codegen/
resolver.rs

1//! Extensible type reference resolution system
2//! 
3//! Based on compiler design principles for name resolution with pluggable strategies.
4//! New resolution strategies can be added without modifying existing code.
5
6use std::collections::HashMap;
7use amalgam_core::ir::{Module, Import};
8
9/// Result of attempting to resolve a type reference
10#[derive(Debug, Clone)]
11pub struct Resolution {
12    /// The resolved reference to use in generated code
13    pub resolved_name: String,
14    /// The import that provides this type (if any)
15    pub required_import: Option<Import>,
16}
17
18/// Trait for implementing type resolution strategies
19/// 
20/// Each implementation handles a specific pattern of imports/references
21/// (e.g., Kubernetes, Crossplane, custom CRDs, etc.)
22pub trait ReferenceResolver: Send + Sync {
23    /// Check if this resolver can handle the given reference
24    fn can_resolve(&self, reference: &str) -> bool;
25    
26    /// Try to resolve a type reference given the current imports
27    fn resolve(
28        &self,
29        reference: &str,
30        imports: &[Import],
31        context: &ResolutionContext,
32    ) -> Option<Resolution>;
33    
34    /// Extract type information from an import path
35    /// Returns (group, version, kind) if applicable
36    fn parse_import_path(&self, path: &str) -> Option<ImportMetadata>;
37    
38    /// Get a human-readable name for this resolver (for debugging)
39    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    /// Current module's group (e.g., "apiextensions.crossplane.io")
53    pub current_group: Option<String>,
54    /// Current module's version (e.g., "v1beta1")
55    pub current_version: Option<String>,
56    /// Current module's kind (e.g., "composition")
57    pub current_kind: Option<String>,
58}
59
60/// Main resolver that delegates to registered strategies
61pub struct TypeResolver {
62    /// Registered resolution strategies
63    resolvers: Vec<Box<dyn ReferenceResolver>>,
64    /// Cache of resolved references for performance
65    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        // Register default resolvers
76        resolver.register(Box::new(KubernetesResolver::new()));
77        resolver.register(Box::new(LocalTypeResolver::new()));
78        // More resolvers can be added here as they're implemented
79        
80        resolver
81    }
82    
83    /// Register a new resolution strategy
84    pub fn register(&mut self, resolver: Box<dyn ReferenceResolver>) {
85        self.resolvers.push(resolver);
86    }
87    
88    /// Resolve a type reference using registered strategies
89    pub fn resolve(
90        &mut self,
91        reference: &str,
92        module: &Module,
93        context: &ResolutionContext,
94    ) -> String {
95        // Check cache first
96        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        // Try each resolver in order
104        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        // No resolver could handle it - return as-is
117        reference.to_string()
118    }
119    
120    /// Clear the resolution cache (useful when context changes)
121    pub fn clear_cache(&mut self) {
122        self.cache.clear();
123    }
124}
125
126// ============================================================================
127// Kubernetes Resolution Strategy
128// ============================================================================
129
130struct KubernetesResolver {
131    /// Known k8s type mappings
132    known_types: HashMap<String, String>,
133}
134
135impl KubernetesResolver {
136    fn new() -> Self {
137        let mut known_types = HashMap::new();
138        
139        // Register common k8s types and their canonical names
140        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        // Add more as needed
145        
146        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        // First, normalize the reference if it's a short name
164        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        // Look for a matching import
173        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                // Check if this import could provide the type
179                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        // Parse k8s import paths like "../../k8s_io/v1/objectmeta.ncl"
201        if !path.contains("k8s_io") && !path.contains("k8s.io") {
202            return None;
203        }
204        
205        let parts: Vec<&str> = path.split('/').collect();
206        
207        // Find k8s_io in the path
208        if let Some(k8s_idx) = parts.iter().position(|&p| p == "k8s_io" || p == "k8s.io") {
209            // For k8s_io paths, structure is: k8s_io/version/kind.ncl
210            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(),  // Simplified - k8s_io maps to k8s.io
223                    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        // Check if the import metadata matches the reference
241        if let Some(ref kind) = metadata.kind {
242            // Case-insensitive comparison for the kind name
243            // The reference might have "ObjectMeta" while the file is "objectmeta.ncl"
244            let ref_kind = reference.split('.').last().unwrap_or("");
245            ref_kind.eq_ignore_ascii_case(kind)
246        } else {
247            // Module import - check if version matches
248            reference.contains(&metadata.version)
249        }
250    }
251}
252
253// ============================================================================
254// Local Type Resolution Strategy
255// ============================================================================
256
257struct 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        // Handle simple, unqualified type names that might be local
268        !reference.contains('.') && !reference.contains('/')
269    }
270    
271    fn resolve(
272        &self,
273        reference: &str,
274        _imports: &[Import],
275        _context: &ResolutionContext,
276    ) -> Option<Resolution> {
277        // Local types are used as-is
278        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 // Local resolver doesn't parse imports
286    }
287    
288    fn name(&self) -> &str {
289        "LocalTypeResolver"
290    }
291}
292
293// ============================================================================
294// Future resolvers can be added here:
295// - CrossplaneResolver
296// - OpenAPIResolver
297// - CustomCRDResolver
298// - ProtoResolver
299// etc.
300// ============================================================================
301
302/// Helper function to capitalize first letter
303fn 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}