1use crate::{
4 crd::{CRDParser, CRD},
5 imports::{ImportResolver, TypeReference},
6 ParserError,
7};
8use amalgam_codegen::{
9 nickel_package::{NickelPackageConfig, NickelPackageGenerator, PackageDependency},
10 Codegen,
11};
12use amalgam_core::{
13 ir::{Import, Module, TypeDefinition, IR},
14 types::Type,
15};
16use std::collections::HashMap;
17use std::path::PathBuf;
18
19pub struct PackageGenerator {
20 crds: Vec<CRD>,
21 package_name: String,
22 _base_path: PathBuf,
23}
24
25impl PackageGenerator {
26 pub fn new(package_name: String, base_path: PathBuf) -> Self {
27 Self {
28 crds: Vec::new(),
29 package_name,
30 _base_path: base_path,
31 }
32 }
33
34 pub fn add_crd(&mut self, crd: CRD) {
35 self.crds.push(crd);
36 }
37
38 pub fn add_crds(&mut self, crds: Vec<CRD>) {
39 self.crds.extend(crds);
40 }
41
42 pub fn generate_package(&self) -> Result<NamespacedPackage, ParserError> {
59 let mut package = NamespacedPackage::new(self.package_name.clone());
60
61 for crd in &self.crds {
63 let group = &crd.spec.group;
64 let kind_lowercase = crd.spec.names.kind.to_lowercase();
65 let _kind_original = crd.spec.names.kind.clone();
66
67 for version in &crd.spec.versions {
68 if !version.served {
69 continue; }
71
72 let parser = CRDParser::new();
74 let ir = parser.parse_version(crd, &version.name)?;
75
76 if let Some(module) = ir.modules.first() {
78 for type_def in &module.types {
79 package.add_type(
82 group.clone(),
83 version.name.clone(),
84 kind_lowercase.clone(),
85 type_def.clone(),
86 );
87 }
88 }
89 }
90 }
91
92 Ok(package)
93 }
94}
95
96pub struct NamespacedPackage {
98 pub name: String,
99 pub types: HashMap<String, HashMap<String, HashMap<String, TypeDefinition>>>,
101}
102
103impl NamespacedPackage {
104 pub fn new(name: String) -> Self {
105 Self {
106 name,
107 types: HashMap::new(),
108 }
109 }
110
111 pub fn add_type(
112 &mut self,
113 group: String,
114 version: String,
115 kind: String,
116 type_def: TypeDefinition,
117 ) {
118 self.types
119 .entry(group)
120 .or_default()
121 .entry(version)
122 .or_default()
123 .insert(kind, type_def);
124 }
125
126 pub fn generate_main_module(&self) -> String {
128 let mut content = String::new();
129 content.push_str(&format!("# {} - Kubernetes CRD types\n", self.name));
130 content.push_str("# Auto-generated by amalgam\n");
131 content.push_str("# Structure: group/version/kind\n\n");
132 content.push_str("{\n");
133
134 let mut groups: Vec<_> = self.types.keys().collect();
136 groups.sort();
137
138 for group in groups {
139 let safe_group = group.replace(['.', '-'], "_");
140 content.push_str(&format!(
141 " {} = import \"./{}/mod.ncl\",\n",
142 safe_group, group
143 ));
144 }
145
146 content.push_str("}\n");
147 content
148 }
149
150 pub fn generate_group_module(&self, group: &str) -> Option<String> {
152 self.types.get(group).map(|versions| {
153 let mut content = String::new();
154 content.push_str(&format!("# {} group\n", group));
155 content.push_str("# Auto-generated by amalgam\n\n");
156 content.push_str("{\n");
157
158 let mut version_list: Vec<_> = versions.keys().collect();
160 version_list.sort();
161
162 for version in version_list {
163 content.push_str(&format!(
164 " {} = import \"./{}/mod.ncl\",\n",
165 version, version
166 ));
167 }
168
169 content.push_str("}\n");
170 content
171 })
172 }
173
174 pub fn generate_version_module(&self, group: &str, version: &str) -> Option<String> {
176 self.types.get(group).and_then(|versions| {
177 versions.get(version).map(|kinds| {
178 let mut content = String::new();
179 content.push_str(&format!("# {}/{} types\n", group, version));
180 content.push_str("# Auto-generated by amalgam\n\n");
181 content.push_str("{\n");
182
183 let mut kind_list: Vec<_> = kinds.keys().collect();
185 kind_list.sort();
186
187 for kind in kind_list {
188 let type_name = if let Some(type_def) = kinds.get(kind) {
190 type_def.name.clone()
191 } else {
192 capitalize_first(kind)
193 };
194 content.push_str(&format!(" {} = import \"./{}.ncl\",\n", type_name, kind));
195 }
196
197 content.push_str("}\n");
198 content
199 })
200 })
201 }
202
203 pub fn generate_kind_file(&self, group: &str, version: &str, kind: &str) -> Option<String> {
205 self.types.get(group).and_then(|versions| {
206 versions.get(version).and_then(|kinds| {
207 kinds.get(kind).map(|type_def| {
208 let mut ir = IR::new();
210 let mut module = Module {
211 name: format!("{}.{}", kind, group),
212 imports: Vec::new(),
213 types: vec![type_def.clone()],
214 constants: Vec::new(),
215 metadata: Default::default(),
216 };
217
218 let mut import_resolver = ImportResolver::new();
220 import_resolver.analyze_type(&type_def.ty);
221
222 let mut reference_mappings: HashMap<String, String> = HashMap::new();
224
225 let mut imports_by_path: HashMap<String, Vec<TypeReference>> = HashMap::new();
227
228 for type_ref in import_resolver.references() {
229 let import_path = type_ref.import_path(group, version);
230 imports_by_path
231 .entry(import_path)
232 .or_default()
233 .push(type_ref.clone());
234 }
235
236 for (import_path, type_refs) in imports_by_path {
238 let alias = if import_path.contains("k8s_io") {
240 let filename = import_path
242 .trim_end_matches(".ncl")
243 .split('/')
244 .next_back()
245 .unwrap_or("unknown");
246 format!("k8s_io_{}", filename)
247 } else {
248 format!("import_{}", module.imports.len())
249 };
250
251 for type_ref in &type_refs {
253 let full_name = if type_ref.group == "k8s.io" {
255 if type_ref.kind == "ObjectMeta" || type_ref.kind == "ListMeta" {
257 format!(
258 "io.k8s.apimachinery.pkg.apis.meta.{}.{}",
259 type_ref.version, type_ref.kind
260 )
261 } else {
262 format!(
263 "io.k8s.api.core.{}.{}",
264 type_ref.version, type_ref.kind
265 )
266 }
267 } else {
268 format!("{}/{}.{}", type_ref.group, type_ref.version, type_ref.kind)
270 };
271
272 let mapped_name = format!("{}.{}", alias, type_ref.kind);
274 reference_mappings.insert(full_name, mapped_name);
275 }
276
277 tracing::debug!(
278 "Adding import: path={}, alias={}, types={:?}",
279 import_path,
280 alias,
281 type_refs.iter().map(|t| &t.kind).collect::<Vec<_>>()
282 );
283
284 module.imports.push(Import {
285 path: import_path,
286 alias: Some(alias),
287 items: vec![], });
289 }
290
291 let mut transformed_type_def = type_def.clone();
293 transform_type_references(&mut transformed_type_def.ty, &reference_mappings);
294
295 module.types = vec![transformed_type_def];
297
298 tracing::debug!(
299 "Module {} has {} imports",
300 module.name,
301 module.imports.len()
302 );
303 ir.add_module(module);
304
305 use amalgam_codegen::package_mode::PackageMode;
307 use std::path::PathBuf;
308
309 let manifest_path = PathBuf::from(".amalgam-manifest.toml");
311 let manifest = if manifest_path.exists() {
312 Some(&manifest_path)
313 } else {
314 None
315 };
316
317 let mut package_mode = PackageMode::new_with_analyzer(manifest);
318
319 let mut all_types: Vec<amalgam_core::types::Type> = Vec::new();
321 for module in &ir.modules {
322 for type_def in &module.types {
323 all_types.push(type_def.ty.clone());
324 }
325 }
326 package_mode.analyze_and_update_dependencies(&all_types, group);
327
328 let mut codegen = amalgam_codegen::nickel::NickelCodegen::new()
329 .with_package_mode(package_mode);
330 let mut generated = codegen
331 .generate(&ir)
332 .unwrap_or_else(|e| format!("# Error generating type: {}\n", e));
333
334 if group == "k8s.io" || group.starts_with("io.k8s") {
336 use crate::k8s_imports::{find_k8s_type_references, fix_k8s_imports};
337 let type_refs = find_k8s_type_references(&type_def.ty);
338 if !type_refs.is_empty() {
339 generated = fix_k8s_imports(&generated, &type_refs, version);
340 }
341 }
342
343 generated
344 })
345 })
346 })
347 }
348
349 pub fn groups(&self) -> Vec<String> {
351 let mut groups: Vec<_> = self.types.keys().cloned().collect();
352 groups.sort();
353 groups
354 }
355
356 pub fn versions(&self, group: &str) -> Vec<String> {
358 self.types
359 .get(group)
360 .map(|versions| {
361 let mut version_list: Vec<_> = versions.keys().cloned().collect();
362 version_list.sort();
363 version_list
364 })
365 .unwrap_or_default()
366 }
367
368 pub fn kinds(&self, group: &str, version: &str) -> Vec<String> {
370 self.types
371 .get(group)
372 .and_then(|versions| {
373 versions.get(version).map(|kinds| {
374 let mut kind_list: Vec<_> = kinds.keys().cloned().collect();
375 kind_list.sort();
376 kind_list
377 })
378 })
379 .unwrap_or_default()
380 }
381
382 pub fn generate_nickel_manifest(&self, config: Option<NickelPackageConfig>) -> String {
384 let config = config.unwrap_or_else(|| NickelPackageConfig {
385 name: self.name.clone(),
386 description: format!("Generated type definitions for {}", self.name),
387 version: "0.1.0".to_string(),
388 minimal_nickel_version: "1.9.0".to_string(),
389 authors: vec!["amalgam".to_string()],
390 license: "Apache-2.0".to_string(),
391 keywords: {
392 let mut keywords = vec!["kubernetes".to_string(), "types".to_string()];
393 for group in self.groups() {
395 keywords.push(group.replace('.', "-"));
396 }
397 keywords
398 },
399 });
400
401 let generator = NickelPackageGenerator::new(config);
402
403 let mut dependencies = HashMap::new();
405 if self.has_k8s_references() {
406 dependencies.insert(
408 "k8s_io".to_string(),
409 PackageDependency::Path(PathBuf::from("../k8s_io")),
410 );
411 }
412
413 let modules: Vec<Module> = self
415 .groups()
416 .into_iter()
417 .flat_map(|group| {
418 self.versions(&group)
419 .into_iter()
420 .map(move |version| Module {
421 name: format!("{}.{}", group, version),
422 imports: Vec::new(),
423 types: Vec::new(),
424 constants: Vec::new(),
425 metadata: Default::default(),
426 })
427 })
428 .collect();
429
430 generator
431 .generate_manifest(&modules, dependencies)
432 .unwrap_or_else(|e| format!("# Error generating manifest: {}\n", e))
433 }
434
435 fn has_k8s_references(&self) -> bool {
437 for versions in self.types.values() {
438 for kinds in versions.values() {
439 for type_def in kinds.values() {
440 if needs_k8s_imports(&type_def.ty) {
441 return true;
442 }
443 }
444 }
445 }
446 false
447 }
448}
449
450#[allow(dead_code)]
451fn sanitize_name(name: &str) -> String {
452 name.replace(['-', '.'], "_")
453 .to_lowercase()
454 .chars()
455 .map(|c| {
456 if c.is_alphanumeric() || c == '_' {
457 c
458 } else {
459 '_'
460 }
461 })
462 .collect::<String>()
463}
464
465fn capitalize_first(s: &str) -> String {
466 let mut chars = s.chars();
467 match chars.next() {
468 None => String::new(),
469 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
470 }
471}
472
473fn transform_type_references(ty: &mut Type, mappings: &HashMap<String, String>) {
475 match ty {
476 Type::Reference(name) => {
477 if let Some(mapped) = mappings.get(name) {
479 *name = mapped.clone();
480 }
481 }
482 Type::Array(inner) => transform_type_references(inner, mappings),
483 Type::Optional(inner) => transform_type_references(inner, mappings),
484 Type::Map { value, .. } => transform_type_references(value, mappings),
485 Type::Record { fields, .. } => {
486 for field in fields.values_mut() {
487 transform_type_references(&mut field.ty, mappings);
488 }
489 }
490 Type::Union(types) => {
491 for ty in types {
492 transform_type_references(ty, mappings);
493 }
494 }
495 Type::TaggedUnion { variants, .. } => {
496 for variant_type in variants.values_mut() {
497 transform_type_references(variant_type, mappings);
498 }
499 }
500 _ => {} }
502}
503
504#[allow(dead_code)]
506fn capitalize(s: &str) -> String {
507 capitalize_first(s)
508}
509
510#[allow(dead_code)]
511fn needs_k8s_imports(ty: &Type) -> bool {
512 match ty {
515 Type::Reference(name) => name.contains("k8s.io") || name.contains("ObjectMeta"),
516 Type::Record { fields, .. } => fields.values().any(|field| needs_k8s_imports(&field.ty)),
517 Type::Array(inner) => needs_k8s_imports(inner),
518 Type::Optional(inner) => needs_k8s_imports(inner),
519 Type::Union(types) => types.iter().any(needs_k8s_imports),
520 _ => false,
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use crate::crd::{CRDMetadata, CRDNames, CRDSchema, CRDSpec, CRDVersion};
528 use pretty_assertions::assert_eq;
529
530 fn sample_crd(group: &str, version: &str, kind: &str) -> CRD {
531 CRD {
532 api_version: "apiextensions.k8s.io/v1".to_string(),
533 kind: "CustomResourceDefinition".to_string(),
534 metadata: CRDMetadata {
535 name: format!("{}.{}", kind.to_lowercase(), group),
536 },
537 spec: CRDSpec {
538 group: group.to_string(),
539 names: CRDNames {
540 kind: kind.to_string(),
541 plural: format!("{}s", kind.to_lowercase()),
542 singular: kind.to_lowercase(),
543 },
544 versions: vec![CRDVersion {
545 name: version.to_string(),
546 served: true,
547 storage: true,
548 schema: Some(CRDSchema {
549 openapi_v3_schema: serde_json::json!({
550 "type": "object",
551 "properties": {
552 "spec": {
553 "type": "object",
554 "properties": {
555 "field1": {"type": "string"},
556 "field2": {"type": "integer"}
557 }
558 }
559 }
560 }),
561 }),
562 }],
563 },
564 }
565 }
566
567 #[test]
568 fn test_package_generator_basic() {
569 let mut generator =
570 PackageGenerator::new("test-package".to_string(), PathBuf::from("/tmp/test"));
571
572 generator.add_crd(sample_crd("example.io", "v1", "Widget"));
573
574 let package = generator.generate_package().unwrap();
575
576 assert_eq!(package.name, "test-package");
577 assert!(package.groups().contains(&"example.io".to_string()));
578 }
579
580 #[test]
581 fn test_sanitize_name_function() {
582 assert_eq!(super::sanitize_name("some-name"), "some_name");
583 assert_eq!(super::sanitize_name("name.with.dots"), "name_with_dots");
584 assert_eq!(super::sanitize_name("UPPERCASE"), "uppercase");
585 }
586
587 #[test]
588 fn test_capitalize_function() {
589 assert_eq!(super::capitalize("widget"), "Widget");
590 assert_eq!(super::capitalize(""), "");
591 }
592}