amalgam_parser/
package.rs

1//! Package-based CRD importing (similar to CUE's approach)
2
3use 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    /// Generate a package structure similar to CUE's approach
43    /// Creates a directory structure like:
44    /// ```text
45    /// package_name/
46    ///   ├── mod.ncl                                    # Main module
47    ///   ├── apiextensions.crossplane.io/
48    ///   │   ├── mod.ncl                               # Group module
49    ///   │   ├── v1/
50    ///   │   │   ├── mod.ncl                           # Version module
51    ///   │   │   ├── composition.ncl                   # Type definition
52    ///   │   │   └── compositeresourcedefinition.ncl
53    ///   │   └── v1beta1/
54    ///   │       └── ...
55    ///   └── pkg.crossplane.io/
56    ///       └── ...
57    /// ```
58    pub fn generate_package(&self) -> Result<NamespacedPackage, ParserError> {
59        let mut package = NamespacedPackage::new(self.package_name.clone());
60
61        // Group CRDs by group, version, and kind
62        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; // Skip non-served versions
70                }
71
72                // Parse the CRD for this specific version
73                let parser = CRDParser::new();
74                let ir = parser.parse_version(crd, &version.name)?;
75
76                // Extract the type definition for this version
77                if let Some(module) = ir.modules.first() {
78                    for type_def in &module.types {
79                        // Store by group/version/kind structure with lowercase key
80                        // but preserve original casing in the type definition
81                        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
96/// Represents a package organized by group/version/kind
97pub struct NamespacedPackage {
98    pub name: String,
99    /// group -> version -> kind -> TypeDefinition
100    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    /// Generate the main module file
127    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        // Sort groups for consistent output
135        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    /// Generate a group-level module file
151    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            // Sort versions for consistent output
159            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    /// Generate a version-level module file for a group
175    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                // Sort kinds for consistent output
184                let mut kind_list: Vec<_> = kinds.keys().collect();
185                kind_list.sort();
186
187                for kind in kind_list {
188                    // Use the actual type name from the TypeDefinition
189                    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    /// Generate a kind-specific file
204    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                    // Use the nickel codegen to generate the type
209                    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                    // Analyze the type for external references and add imports
219                    let mut import_resolver = ImportResolver::new();
220                    import_resolver.analyze_type(&type_def.ty);
221
222                    // Build a mapping from full qualified names to alias.TypeName
223                    let mut reference_mappings: HashMap<String, String> = HashMap::new();
224
225                    // Group references by their import path to avoid duplicates
226                    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                    // Generate a single import for each unique path and build mappings
237                    for (import_path, type_refs) in imports_by_path {
238                        // Generate a proper alias for this import
239                        let alias = if import_path.contains("k8s_io") {
240                            // For k8s types, extract the filename as the basis for the alias
241                            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                        // Create mappings for all types from this import
252                        for type_ref in &type_refs {
253                            // Build the full qualified name that appears in Type::Reference
254                            let full_name = if type_ref.group == "k8s.io" {
255                                // For k8s types, construct the full io.k8s... name
256                                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                                // For other types, use a simpler format
269                                format!("{}/{}.{}", type_ref.group, type_ref.version, type_ref.kind)
270                            };
271
272                            // Map to alias.TypeName
273                            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![], // Empty items means import the whole module
288                        });
289                    }
290
291                    // Transform the type definition to use the mapped references
292                    let mut transformed_type_def = type_def.clone();
293                    transform_type_references(&mut transformed_type_def.ty, &reference_mappings);
294
295                    // Use the transformed type definition
296                    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                    // Generate the Nickel code with package mode
306                    use amalgam_codegen::package_mode::PackageMode;
307                    use std::path::PathBuf;
308
309                    // Use analyzer-based package mode for automatic dependency detection
310                    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                    // Analyze types to detect dependencies
320                    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                    // For k8s.io packages, check for missing internal imports
335                    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    /// Get all groups in the package
350    pub fn groups(&self) -> Vec<String> {
351        let mut groups: Vec<_> = self.types.keys().cloned().collect();
352        groups.sort();
353        groups
354    }
355
356    /// Get all versions for a group
357    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    /// Get all kinds for a group/version
369    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    /// Generate a Nickel package manifest (Nickel-pkg.ncl)
383    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                // Add groups as keywords
394                for group in self.groups() {
395                    keywords.push(group.replace('.', "-"));
396                }
397                keywords
398            },
399        });
400
401        let generator = NickelPackageGenerator::new(config);
402
403        // Detect if we need k8s.io as a dependency
404        let mut dependencies = HashMap::new();
405        if self.has_k8s_references() {
406            // Add k8s.io as a path dependency (assuming it's in a sibling directory)
407            dependencies.insert(
408                "k8s_io".to_string(),
409                PackageDependency::Path(PathBuf::from("../k8s_io")),
410            );
411        }
412
413        // Convert our types to modules for the generator
414        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    /// Check if any types reference k8s.io types
436    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
473/// Transform Type::Reference values using the provided mappings
474fn transform_type_references(ty: &mut Type, mappings: &HashMap<String, String>) {
475    match ty {
476        Type::Reference(name) => {
477            // Check if we have a mapping for this reference
478            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        _ => {} // Other types don't contain references
501    }
502}
503
504// Alias for tests
505#[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    // Check if the type references k8s.io types
513    // This is a simplified check - would need more sophisticated analysis
514    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}