use crate::CodegenError;
use amalgam_core::ir::Module;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct NickelPackageConfig {
pub name: String,
pub version: String,
pub minimal_nickel_version: String,
pub description: String,
pub authors: Vec<String>,
pub license: String,
pub keywords: Vec<String>,
}
impl Default for NickelPackageConfig {
fn default() -> Self {
Self {
name: "generated-types".to_string(),
version: "0.1.0".to_string(),
minimal_nickel_version: "1.9.0".to_string(),
description: "Auto-generated Nickel type definitions".to_string(),
authors: vec!["amalgam".to_string()],
license: "Apache-2.0".to_string(),
keywords: vec![
"kubernetes".to_string(),
"crd".to_string(),
"types".to_string(),
],
}
}
}
pub struct NickelPackageGenerator {
config: NickelPackageConfig,
}
impl NickelPackageGenerator {
pub fn new(config: NickelPackageConfig) -> Self {
Self { config }
}
pub fn generate_manifest(
&self,
_modules: &[Module],
dependencies: HashMap<String, PackageDependency>,
) -> Result<String, CodegenError> {
let mut manifest = String::new();
manifest.push_str("{\n");
manifest.push_str(&format!(" name = \"{}\",\n", self.config.name));
manifest.push_str(&format!(
" description = \"{}\",\n",
self.config.description
));
manifest.push_str(&format!(" version = \"{}\",\n", self.config.version));
if !self.config.authors.is_empty() {
manifest.push_str(" authors = [\n");
for author in &self.config.authors {
manifest.push_str(&format!(" \"{}\",\n", author));
}
manifest.push_str(" ],\n");
}
if !self.config.license.is_empty() {
manifest.push_str(&format!(" license = \"{}\",\n", self.config.license));
}
if !self.config.keywords.is_empty() {
manifest.push_str(" keywords = [\n");
for keyword in &self.config.keywords {
manifest.push_str(&format!(" \"{}\",\n", keyword));
}
manifest.push_str(" ],\n");
}
manifest.push_str(&format!(
" minimal_nickel_version = \"{}\",\n",
self.config.minimal_nickel_version
));
if !dependencies.is_empty() {
manifest.push_str(" dependencies = {\n");
for (name, dep) in dependencies {
manifest.push_str(&format!(" {} = {},\n", name, dep.to_nickel_string()));
}
manifest.push_str(" },\n");
}
manifest.push_str("} | std.package.Manifest\n");
Ok(manifest)
}
pub fn generate_main_module(&self, modules: &[Module]) -> Result<String, CodegenError> {
let mut main = String::new();
main.push_str("# Main module for ");
main.push_str(&self.config.name);
main.push('\n');
main.push_str("# This file exports all generated types\n\n");
main.push_str("{\n");
let mut grouped_modules: HashMap<String, Vec<&Module>> = HashMap::new();
for module in modules {
let parts: Vec<&str> = module.name.split('.').collect();
if let Some(group) = parts.first() {
grouped_modules
.entry(group.to_string())
.or_default()
.push(module);
}
}
for (group, group_modules) in grouped_modules {
main.push_str(&format!(" {} = {{\n", sanitize_identifier(&group)));
for module in group_modules {
let parts: Vec<&str> = module.name.split('.').collect();
if parts.len() > 1 {
let version = parts[1];
main.push_str(&format!(
" {} = import \"./{}/{}/mod.ncl\",\n",
sanitize_identifier(version),
group,
version
));
}
}
main.push_str(" },\n");
}
main.push_str("}\n");
Ok(main)
}
}
#[derive(Debug, Clone)]
pub enum PackageDependency {
Path(PathBuf),
Index { package: String, version: String },
Git {
url: String,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
},
}
impl PackageDependency {
pub fn to_nickel_string(&self) -> String {
match self {
PackageDependency::Path(path) => {
format!("'Path \"{}\"", path.display())
}
PackageDependency::Index { package, version } => {
format!(
"'Index {{ package = \"{}\", version = \"{}\" }}",
package, version
)
}
PackageDependency::Git {
url,
branch,
tag,
rev,
} => {
let mut parts = vec![format!("url = \"{}\"", url)];
if let Some(branch) = branch {
parts.push(format!("branch = \"{}\"", branch));
}
if let Some(tag) = tag {
parts.push(format!("tag = \"{}\"", tag));
}
if let Some(rev) = rev {
parts.push(format!("rev = \"{}\"", rev));
}
format!("'Git {{ {} }}", parts.join(", "))
}
}
}
}
fn sanitize_identifier(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_basic_manifest() {
let config = NickelPackageConfig {
name: "test-package".to_string(),
version: "1.0.0".to_string(),
minimal_nickel_version: "1.9.0".to_string(),
description: "A test package".to_string(),
authors: vec!["Test Author".to_string()],
license: "MIT".to_string(),
keywords: vec!["test".to_string()],
};
let generator = NickelPackageGenerator::new(config);
let manifest = generator.generate_manifest(&[], HashMap::new()).unwrap();
assert!(manifest.contains("name = \"test-package\""));
assert!(manifest.contains("version = \"1.0.0\""));
assert!(manifest.contains("| std.package.Manifest"));
}
#[test]
fn test_path_dependency() {
let dep = PackageDependency::Path(PathBuf::from("../k8s-types"));
assert_eq!(dep.to_nickel_string(), "'Path \"../k8s-types\"");
}
#[test]
fn test_index_dependency() {
let dep = PackageDependency::Index {
package: "github:nickel-lang/stdlib".to_string(),
version: ">=1.0.0".to_string(),
};
assert_eq!(
dep.to_nickel_string(),
"'Index { package = \"github:nickel-lang/stdlib\", version = \">=1.0.0\" }"
);
}
}