amalgam_codegen/
nickel_package.rs

1//! Nickel package manifest generation
2//!
3//! This module provides functionality to generate Nickel package manifests
4//! (Nickel-pkg.ncl files) for generated type definitions.
5
6use crate::CodegenError;
7use amalgam_core::ir::Module;
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11/// Configuration for generating a Nickel package
12#[derive(Debug, Clone)]
13pub struct NickelPackageConfig {
14    /// Package name (e.g., "k8s-types", "crossplane-types")
15    pub name: String,
16    /// Package version
17    pub version: String,
18    /// Minimum Nickel version required
19    pub minimal_nickel_version: String,
20    /// Package description
21    pub description: String,
22    /// Package authors
23    pub authors: Vec<String>,
24    /// Package license
25    pub license: String,
26    /// Package keywords for discovery
27    pub keywords: Vec<String>,
28}
29
30impl Default for NickelPackageConfig {
31    fn default() -> Self {
32        Self {
33            name: "generated-types".to_string(),
34            version: "0.1.0".to_string(),
35            minimal_nickel_version: "1.9.0".to_string(),
36            description: "Auto-generated Nickel type definitions".to_string(),
37            authors: vec!["amalgam".to_string()],
38            license: "Apache-2.0".to_string(),
39            keywords: vec![
40                "kubernetes".to_string(),
41                "crd".to_string(),
42                "types".to_string(),
43            ],
44        }
45    }
46}
47
48/// Generator for Nickel package manifests
49pub struct NickelPackageGenerator {
50    config: NickelPackageConfig,
51}
52
53impl NickelPackageGenerator {
54    pub fn new(config: NickelPackageConfig) -> Self {
55        Self { config }
56    }
57
58    /// Generate a Nickel package manifest for a set of modules
59    pub fn generate_manifest(
60        &self,
61        _modules: &[Module],
62        dependencies: HashMap<String, PackageDependency>,
63    ) -> Result<String, CodegenError> {
64        let mut manifest = String::new();
65
66        // Start the manifest object
67        manifest.push_str("{\n");
68
69        // Basic package metadata
70        manifest.push_str(&format!("  name = \"{}\",\n", self.config.name));
71        manifest.push_str(&format!(
72            "  description = \"{}\",\n",
73            self.config.description
74        ));
75        manifest.push_str(&format!("  version = \"{}\",\n", self.config.version));
76
77        // Authors
78        if !self.config.authors.is_empty() {
79            manifest.push_str("  authors = [\n");
80            for author in &self.config.authors {
81                manifest.push_str(&format!("    \"{}\",\n", author));
82            }
83            manifest.push_str("  ],\n");
84        }
85
86        // License
87        if !self.config.license.is_empty() {
88            manifest.push_str(&format!("  license = \"{}\",\n", self.config.license));
89        }
90
91        // Keywords
92        if !self.config.keywords.is_empty() {
93            manifest.push_str("  keywords = [\n");
94            for keyword in &self.config.keywords {
95                manifest.push_str(&format!("    \"{}\",\n", keyword));
96            }
97            manifest.push_str("  ],\n");
98        }
99
100        // Minimal Nickel version
101        manifest.push_str(&format!(
102            "  minimal_nickel_version = \"{}\",\n",
103            self.config.minimal_nickel_version
104        ));
105
106        // Dependencies
107        if !dependencies.is_empty() {
108            manifest.push_str("  dependencies = {\n");
109            for (name, dep) in dependencies {
110                manifest.push_str(&format!("    {} = {},\n", name, dep.to_nickel_string()));
111            }
112            manifest.push_str("  },\n");
113        }
114
115        // Close the manifest and apply the contract
116        manifest.push_str("} | std.package.Manifest\n");
117
118        Ok(manifest)
119    }
120
121    /// Generate a main entry point file that exports all types
122    pub fn generate_main_module(&self, modules: &[Module]) -> Result<String, CodegenError> {
123        let mut main = String::new();
124
125        main.push_str("# Main module for ");
126        main.push_str(&self.config.name);
127        main.push('\n');
128        main.push_str("# This file exports all generated types\n\n");
129
130        main.push_str("{\n");
131
132        // Group modules by their base name (e.g., group in k8s context)
133        let mut grouped_modules: HashMap<String, Vec<&Module>> = HashMap::new();
134        for module in modules {
135            let parts: Vec<&str> = module.name.split('.').collect();
136            if let Some(group) = parts.first() {
137                grouped_modules
138                    .entry(group.to_string())
139                    .or_default()
140                    .push(module);
141            }
142        }
143
144        // Export each group
145        for (group, group_modules) in grouped_modules {
146            main.push_str(&format!("  {} = {{\n", sanitize_identifier(&group)));
147
148            for module in group_modules {
149                // Get the relative module name (e.g., "v1" from "core.v1")
150                let parts: Vec<&str> = module.name.split('.').collect();
151                if parts.len() > 1 {
152                    let version = parts[1];
153                    main.push_str(&format!(
154                        "    {} = import \"./{}/{}/mod.ncl\",\n",
155                        sanitize_identifier(version),
156                        group,
157                        version
158                    ));
159                }
160            }
161
162            main.push_str("  },\n");
163        }
164
165        main.push_str("}\n");
166
167        Ok(main)
168    }
169}
170
171/// Represents a dependency in a Nickel package
172#[derive(Debug, Clone)]
173pub enum PackageDependency {
174    /// A path dependency to a local package
175    Path(PathBuf),
176    /// A dependency from the package index
177    Index { package: String, version: String },
178    /// A git dependency
179    Git {
180        url: String,
181        branch: Option<String>,
182        tag: Option<String>,
183        rev: Option<String>,
184    },
185}
186
187impl PackageDependency {
188    /// Convert the dependency to its Nickel representation
189    pub fn to_nickel_string(&self) -> String {
190        match self {
191            PackageDependency::Path(path) => {
192                format!("'Path \"{}\"", path.display())
193            }
194            PackageDependency::Index { package, version } => {
195                format!(
196                    "'Index {{ package = \"{}\", version = \"{}\" }}",
197                    package, version
198                )
199            }
200            PackageDependency::Git {
201                url,
202                branch,
203                tag,
204                rev,
205            } => {
206                let mut parts = vec![format!("url = \"{}\"", url)];
207                if let Some(branch) = branch {
208                    parts.push(format!("branch = \"{}\"", branch));
209                }
210                if let Some(tag) = tag {
211                    parts.push(format!("tag = \"{}\"", tag));
212                }
213                if let Some(rev) = rev {
214                    parts.push(format!("rev = \"{}\"", rev));
215                }
216                format!("'Git {{ {} }}", parts.join(", "))
217            }
218        }
219    }
220}
221
222/// Sanitize an identifier to be valid in Nickel
223fn sanitize_identifier(s: &str) -> String {
224    // Replace invalid characters with underscores
225    s.chars()
226        .map(|c| {
227            if c.is_alphanumeric() || c == '_' {
228                c
229            } else {
230                '_'
231            }
232        })
233        .collect()
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_generate_basic_manifest() {
242        let config = NickelPackageConfig {
243            name: "test-package".to_string(),
244            version: "1.0.0".to_string(),
245            minimal_nickel_version: "1.9.0".to_string(),
246            description: "A test package".to_string(),
247            authors: vec!["Test Author".to_string()],
248            license: "MIT".to_string(),
249            keywords: vec!["test".to_string()],
250        };
251
252        let generator = NickelPackageGenerator::new(config);
253        let manifest = generator.generate_manifest(&[], HashMap::new()).unwrap();
254
255        assert!(manifest.contains("name = \"test-package\""));
256        assert!(manifest.contains("version = \"1.0.0\""));
257        assert!(manifest.contains("| std.package.Manifest"));
258    }
259
260    #[test]
261    fn test_path_dependency() {
262        let dep = PackageDependency::Path(PathBuf::from("../k8s-types"));
263        assert_eq!(dep.to_nickel_string(), "'Path \"../k8s-types\"");
264    }
265
266    #[test]
267    fn test_index_dependency() {
268        let dep = PackageDependency::Index {
269            package: "github:nickel-lang/stdlib".to_string(),
270            version: ">=1.0.0".to_string(),
271        };
272        assert_eq!(
273            dep.to_nickel_string(),
274            "'Index { package = \"github:nickel-lang/stdlib\", version = \">=1.0.0\" }"
275        );
276    }
277}