amalgam_codegen/
package_mode.rs1use amalgam_core::dependency_analyzer::DependencyAnalyzer;
7use amalgam_core::types::Type;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PackageDependency {
15 pub package_id: String,
17 pub version: String,
19}
20
21#[derive(Debug, Clone, Default)]
23pub enum PackageMode {
24 #[default]
26 Relative,
27
28 Package {
30 dependencies: HashMap<String, PackageDependency>,
32 analyzer: DependencyAnalyzer,
34 },
35
36 LocalDevelopment {
38 local_paths: HashMap<String, PathBuf>,
40 },
41}
42
43impl PackageMode {
44 pub fn new_with_analyzer(manifest_path: Option<&PathBuf>) -> Self {
46 let mut analyzer = DependencyAnalyzer::new();
47
48 if let Some(path) = manifest_path {
50 let _ = analyzer.register_from_manifest(path);
51 }
52
53 PackageMode::Package {
54 dependencies: HashMap::new(),
55 analyzer,
56 }
57 }
58
59 pub fn analyze_and_update_dependencies(&mut self, types: &[Type], current_package: &str) {
61 if let PackageMode::Package {
62 analyzer,
63 dependencies,
64 } = self
65 {
66 analyzer.set_current_package(current_package);
67
68 let mut all_refs = std::collections::HashSet::new();
70 for ty in types {
71 let refs = analyzer.analyze_type(ty, current_package);
72 all_refs.extend(refs);
73 }
74
75 let detected_deps = analyzer.determine_dependencies(&all_refs);
77
78 for dep in detected_deps {
80 if !dependencies.contains_key(&dep.package_name) {
81 let base = std::env::var("NICKEL_PACKAGE_BASE")
83 .unwrap_or_else(|_| "github:seryl/nickel-pkgs".to_string());
84 let package_id = format!("{}/{}", base, &dep.package_name);
85
86 let version = if dep.is_core_type {
87 ">=1.31.0".to_string()
88 } else {
89 ">=0.1.0".to_string()
90 };
91
92 dependencies.insert(
93 dep.package_name.clone(),
94 PackageDependency {
95 package_id,
96 version,
97 },
98 );
99 }
100 }
101 }
102 }
103
104 pub fn convert_import(&self, import_path: &str) -> String {
106 match self {
107 PackageMode::Relative => {
108 import_path.to_string()
110 }
111 PackageMode::Package { dependencies, .. } => {
112 if let Some(package_name) = self.detect_package_from_path(import_path) {
114 if let Some(dep) = dependencies.get(&package_name) {
116 dep.package_id.clone()
118 } else {
119 format!("\"{}\"", package_name)
121 }
122 } else {
123 import_path.to_string()
125 }
126 }
127 PackageMode::LocalDevelopment { local_paths } => {
128 for (package_name, local_path) in local_paths {
130 if import_path.contains(package_name) {
131 return local_path.to_string_lossy().to_string();
132 }
133 }
134 import_path.to_string()
135 }
136 }
137 }
138
139 fn detect_package_from_path(&self, import_path: &str) -> Option<String> {
141 if import_path.starts_with("../") {
146 let parts: Vec<&str> = import_path.split('/').collect();
147 for part in parts {
149 if part != ".." && part != "." && !part.ends_with(".ncl") {
150 if let PackageMode::Package { dependencies, .. } = self {
153 if dependencies.contains_key(part) {
154 return Some(part.to_string());
155 }
156 let normalized = part.replace('_', "-");
158 if dependencies.contains_key(&normalized) {
159 return Some(normalized);
160 }
161 }
162 break;
163 }
164 }
165 }
166
167 None
168 }
169
170 pub fn generate_imports(&self, types: &[Type], current_package: &str) -> Vec<String> {
172 match self {
173 PackageMode::Package { analyzer, .. } => {
174 let mut analyzer = analyzer.clone();
176 analyzer.set_current_package(current_package);
177
178 let mut all_refs = std::collections::HashSet::new();
179 for ty in types {
180 let refs = analyzer.analyze_type(ty, current_package);
181 all_refs.extend(refs);
182 }
183
184 let deps = analyzer.determine_dependencies(&all_refs);
185 analyzer.generate_imports(&deps, true)
186 }
187 _ => Vec::new(),
188 }
189 }
190
191 pub fn add_to_manifest(&self, content: &str, _package_name: &str) -> String {
193 if let PackageMode::Package { dependencies, .. } = self {
194 if !dependencies.is_empty() {
195 let mut deps_str = String::from(" dependencies = {\n");
196 for (dep_name, dep_info) in dependencies {
197 deps_str.push_str(&format!(
198 " \"{}\" = \"{}\",\n",
199 dep_name, dep_info.version
200 ));
201 }
202 deps_str.push_str(" },\n");
203
204 if content.contains("dependencies = {}") {
206 content.replace("dependencies = {}", deps_str.trim_end())
207 } else {
208 content.to_string()
209 }
210 } else {
211 content.to_string()
212 }
213 } else {
214 content.to_string()
215 }
216 }
217
218 pub fn get_dependencies(&self) -> Option<&HashMap<String, PackageDependency>> {
220 match self {
221 PackageMode::Package { dependencies, .. } => Some(dependencies),
222 _ => None,
223 }
224 }
225}
226
227pub fn create_package_manifest(
229 name: &str,
230 version: &str,
231 description: &str,
232 keywords: Vec<String>,
233 dependencies: HashMap<String, String>,
234) -> String {
235 let deps = if dependencies.is_empty() {
236 "{}".to_string()
237 } else {
238 let entries: Vec<String> = dependencies
239 .iter()
240 .map(|(k, v)| format!(" \"{}\" = \"{}\"", k, v))
241 .collect();
242 format!("{{\n{}\n }}", entries.join(",\n"))
243 };
244
245 format!(
246 r#"{{
247 name = "{}",
248 version = "{}",
249 description = "{}",
250
251 keywords = [{}],
252
253 dependencies = {},
254
255 # Auto-generated by amalgam
256 minimal_nickel_version = "1.9.0",
257}} | std.package.Manifest
258"#,
259 name,
260 version,
261 description,
262 keywords
263 .iter()
264 .map(|k| format!("\"{}\"", k))
265 .collect::<Vec<_>>()
266 .join(", "),
267 deps
268 )
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_import_conversion_with_analyzer() {
277 let mode = PackageMode::new_with_analyzer(None);
278
279 let import = "../../../k8s_io/v1/objectmeta.ncl";
281 let converted = mode.convert_import(import);
282
283 assert_eq!(converted, import);
285 }
286
287 #[test]
288 fn test_package_manifest_generation() {
289 let manifest = create_package_manifest(
290 "test-package",
291 "1.0.0",
292 "Test package",
293 vec!["test".to_string()],
294 HashMap::new(),
295 );
296
297 assert!(manifest.contains("name = \"test-package\""));
298 assert!(manifest.contains("version = \"1.0.0\""));
299 assert!(manifest.contains("dependencies = {}"));
300 }
301}