1use amalgam_core::types::Type;
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct TypeReference {
9 pub group: String,
11 pub version: String,
13 pub kind: String,
15}
16
17impl TypeReference {
18 pub fn new(group: String, version: String, kind: String) -> Self {
19 Self {
20 group,
21 version,
22 kind,
23 }
24 }
25
26 pub fn from_qualified_name(name: &str) -> Option<Self> {
28 if name.starts_with("io.k8s.") {
34 let parts: Vec<&str> = name.split('.').collect();
38
39 if name.starts_with("io.k8s.apimachinery.pkg.apis.meta.") && parts.len() >= 8 {
40 let version = parts[parts.len() - 2].to_string();
42 let kind = parts[parts.len() - 1].to_string();
43 return Some(Self::new("k8s.io".to_string(), version, kind));
44 } else if name.starts_with("io.k8s.api.") && parts.len() >= 5 {
45 let group = if parts[3] == "core" {
47 "k8s.io".to_string()
48 } else {
49 format!("{}.k8s.io", parts[3])
50 };
51 let version = parts[parts.len() - 2].to_string();
52 let kind = parts[parts.len() - 1].to_string();
53 return Some(Self::new(group, version, kind));
54 }
55 } else if name.contains('/') {
56 let parts: Vec<&str> = name.split('/').collect();
58 if let Some(last) = parts.last() {
59 let type_parts: Vec<&str> = last.split('.').collect();
60 if type_parts.len() == 2 {
61 let version = type_parts[0].to_string();
62 let kind = type_parts[1].to_string();
63 let group = parts[0].to_string();
64 return Some(Self::new(group, version, kind));
65 }
66 }
67 } else if name.starts_with("v1.")
68 || name.starts_with("v1beta1.")
69 || name.starts_with("v1alpha1.")
70 {
71 let parts: Vec<&str> = name.split('.').collect();
73 if parts.len() == 2 {
74 return Some(Self::new(
75 "k8s.io".to_string(),
76 parts[0].to_string(),
77 parts[1].to_string(),
78 ));
79 }
80 }
81
82 None
83 }
84
85 pub fn import_path(&self, from_group: &str, from_version: &str) -> String {
87 let group_to_package = |group: &str| -> String {
97 let sanitized = group.replace('.', "_");
102
103 if group.contains('.') {
105 let parts: Vec<&str> = group.split('.').collect();
108 if parts.len() >= 2
109 && (parts.last() == Some(&"io")
110 || parts.last() == Some(&"com")
111 || parts.last() == Some(&"org"))
112 {
113 if parts.len() == 2 {
115 sanitized
117 } else if parts.len() >= 3 {
118 parts[parts.len() - 2].to_string()
121 } else {
122 sanitized
123 }
124 } else {
125 sanitized
126 }
127 } else {
128 sanitized
129 }
130 };
131
132 let needs_group_subdir = |group: &str, package: &str| -> bool {
134 let sanitized = group.replace('.', "_");
137 sanitized != package && group.contains('.')
138 };
139
140 let from_package = group_to_package(from_group);
142 let mut from_components: Vec<String> = Vec::new();
143 from_components.push(from_package.clone());
144
145 if needs_group_subdir(from_group, &from_package) {
146 from_components.push(from_group.to_string());
147 }
148 from_components.push(from_version.to_string());
149
150 let target_package = group_to_package(&self.group);
152 let mut to_components: Vec<String> = Vec::new();
153 to_components.push(target_package.clone());
154
155 if needs_group_subdir(&self.group, &target_package) {
156 to_components.push(self.group.clone());
157 }
158 to_components.push(self.version.clone());
159 to_components.push(format!("{}.ncl", self.kind.to_lowercase()));
160
161 let up_count = from_components.len();
167 let up_dirs = "../".repeat(up_count);
168 let down_path = to_components.join("/");
169
170 format!("{}{}", up_dirs, down_path)
171 }
172
173 pub fn module_alias(&self) -> String {
175 format!(
176 "{}_{}",
177 self.group.replace(['.', '-'], "_"),
178 self.version.replace('-', "_")
179 )
180 }
181}
182
183pub struct ImportResolver {
185 references: HashSet<TypeReference>,
187 local_types: HashSet<String>,
189}
190
191impl Default for ImportResolver {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197impl ImportResolver {
198 pub fn new() -> Self {
199 Self {
200 references: HashSet::new(),
201 local_types: HashSet::new(),
202 }
203 }
204
205 pub fn add_local_type(&mut self, name: &str) {
207 self.local_types.insert(name.to_string());
208 }
209
210 pub fn analyze_type(&mut self, ty: &Type) {
212 match ty {
213 Type::Reference(name) => {
214 if !self.local_types.contains(name) {
216 if let Some(type_ref) = TypeReference::from_qualified_name(name) {
217 tracing::trace!("ImportResolver: found external reference: {:?}", type_ref);
218 self.references.insert(type_ref);
219 } else {
220 tracing::trace!("ImportResolver: could not parse reference: {}", name);
221 }
222 }
223 }
224 Type::Array(inner) => self.analyze_type(inner),
225 Type::Optional(inner) => self.analyze_type(inner),
226 Type::Map { value, .. } => self.analyze_type(value),
227 Type::Record { fields, .. } => {
228 for field in fields.values() {
229 self.analyze_type(&field.ty);
230 }
231 }
232 Type::Union(types) => {
233 for ty in types {
234 self.analyze_type(ty);
235 }
236 }
237 Type::TaggedUnion { variants, .. } => {
238 for ty in variants.values() {
239 self.analyze_type(ty);
240 }
241 }
242 Type::Contract { base, .. } => self.analyze_type(base),
243 _ => {}
244 }
245 }
246
247 pub fn references(&self) -> &HashSet<TypeReference> {
249 &self.references
250 }
251
252 pub fn generate_imports(&self, from_group: &str, from_version: &str) -> Vec<String> {
254 let mut imports = Vec::new();
255
256 let mut by_module: HashMap<String, Vec<&TypeReference>> = HashMap::new();
258 for type_ref in &self.references {
259 let module_key = format!("{}/{}", type_ref.group, type_ref.version);
260 by_module.entry(module_key).or_default().push(type_ref);
261 }
262
263 for (_module, refs) in by_module {
265 let first_ref = refs[0];
266 let import_path = first_ref.import_path(from_group, from_version);
267 let alias = first_ref.module_alias();
268
269 imports.push(format!("let {} = import \"{}\" in", alias, import_path));
270 }
271
272 imports.sort();
273 imports
274 }
275}
276
277pub fn common_k8s_types() -> Vec<TypeReference> {
279 vec![
280 TypeReference::new(
281 "k8s.io".to_string(),
282 "v1".to_string(),
283 "ObjectMeta".to_string(),
284 ),
285 TypeReference::new(
286 "k8s.io".to_string(),
287 "v1".to_string(),
288 "ListMeta".to_string(),
289 ),
290 TypeReference::new(
291 "k8s.io".to_string(),
292 "v1".to_string(),
293 "TypeMeta".to_string(),
294 ),
295 TypeReference::new(
296 "k8s.io".to_string(),
297 "v1".to_string(),
298 "LabelSelector".to_string(),
299 ),
300 TypeReference::new("k8s.io".to_string(), "v1".to_string(), "Volume".to_string()),
301 TypeReference::new(
302 "k8s.io".to_string(),
303 "v1".to_string(),
304 "VolumeMount".to_string(),
305 ),
306 TypeReference::new(
307 "k8s.io".to_string(),
308 "v1".to_string(),
309 "Container".to_string(),
310 ),
311 TypeReference::new(
312 "k8s.io".to_string(),
313 "v1".to_string(),
314 "PodSpec".to_string(),
315 ),
316 TypeReference::new(
317 "k8s.io".to_string(),
318 "v1".to_string(),
319 "ResourceRequirements".to_string(),
320 ),
321 TypeReference::new(
322 "k8s.io".to_string(),
323 "v1".to_string(),
324 "Affinity".to_string(),
325 ),
326 TypeReference::new(
327 "k8s.io".to_string(),
328 "v1".to_string(),
329 "Toleration".to_string(),
330 ),
331 TypeReference::new("k8s.io".to_string(), "v1".to_string(), "EnvVar".to_string()),
332 TypeReference::new(
333 "k8s.io".to_string(),
334 "v1".to_string(),
335 "ConfigMapKeySelector".to_string(),
336 ),
337 TypeReference::new(
338 "k8s.io".to_string(),
339 "v1".to_string(),
340 "SecretKeySelector".to_string(),
341 ),
342 ]
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_parse_qualified_name() {
351 let ref1 = TypeReference::from_qualified_name("io.k8s.api.core.v1.ObjectMeta");
352 assert!(ref1.is_some());
353 let ref1 = ref1.unwrap();
354 assert_eq!(ref1.group, "k8s.io");
355 assert_eq!(ref1.version, "v1");
356 assert_eq!(ref1.kind, "ObjectMeta");
357
358 let ref2 = TypeReference::from_qualified_name("v1.Volume");
359 assert!(ref2.is_some());
360 let ref2 = ref2.unwrap();
361 assert_eq!(ref2.group, "k8s.io");
362 assert_eq!(ref2.version, "v1");
363 assert_eq!(ref2.kind, "Volume");
364 }
365
366 #[test]
367 fn test_import_path() {
368 let type_ref = TypeReference::new(
369 "k8s.io".to_string(),
370 "v1".to_string(),
371 "ObjectMeta".to_string(),
372 );
373
374 let path = type_ref.import_path("apiextensions.crossplane.io", "v1");
376 assert_eq!(path, "../../../k8s_io/v1/objectmeta.ncl");
377
378 let path2 = type_ref.import_path("example.io", "v1");
380 assert_eq!(path2, "../../k8s_io/v1/objectmeta.ncl");
381 }
382}