amalgam_codegen/
package_mode.rs

1//! Package mode handling for code generation
2//!
3//! This module provides generic package handling without special casing
4//! for specific packages. All package detection is based on actual usage.
5
6use amalgam_core::dependency_analyzer::DependencyAnalyzer;
7use amalgam_core::types::Type;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12/// Represents how a package dependency should be resolved
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PackageDependency {
15    /// The package identifier (e.g., "github:seryl/nickel-pkgs/k8s-io")
16    pub package_id: String,
17    /// Version constraint (e.g., ">=1.31.0")
18    pub version: String,
19}
20
21/// Determines how imports are generated in the output
22#[derive(Debug, Clone, Default)]
23pub enum PackageMode {
24    /// Generate relative file imports (default for local development)
25    #[default]
26    Relative,
27
28    /// Generate package imports for nickel-mine
29    Package {
30        /// Map of external package dependencies discovered through analysis
31        dependencies: HashMap<String, PackageDependency>,
32        /// Dependency analyzer for automatic detection
33        analyzer: DependencyAnalyzer,
34    },
35
36    /// Local development mode with local package paths
37    LocalDevelopment {
38        /// Map of package names to local paths
39        local_paths: HashMap<String, PathBuf>,
40    },
41}
42
43impl PackageMode {
44    /// Create a new package mode with automatic dependency detection
45    pub fn new_with_analyzer(manifest_path: Option<&PathBuf>) -> Self {
46        let mut analyzer = DependencyAnalyzer::new();
47
48        // If we have a manifest, register known types from it
49        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    /// Analyze types to detect dependencies automatically
60    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            // Analyze all types to find external references
69            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            // Determine required dependencies
76            let detected_deps = analyzer.determine_dependencies(&all_refs);
77
78            // Update our dependency map
79            for dep in detected_deps {
80                if !dependencies.contains_key(&dep.package_name) {
81                    // Auto-generate package ID based on detected package
82                    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    /// Convert an import path based on the package mode
105    pub fn convert_import(&self, import_path: &str) -> String {
106        match self {
107            PackageMode::Relative => {
108                // Keep as relative import
109                import_path.to_string()
110            }
111            PackageMode::Package { .. } => {
112                // Check if this import references an external package
113                if let Some(package_name) = self.detect_package_from_path(import_path) {
114                    // Convert to package import
115                    format!("\"{}\"", package_name)
116                } else {
117                    // Keep as relative import within same package
118                    import_path.to_string()
119                }
120            }
121            PackageMode::LocalDevelopment { local_paths } => {
122                // Check if this import references a local package
123                for (package_name, local_path) in local_paths {
124                    if import_path.contains(package_name) {
125                        return local_path.to_string_lossy().to_string();
126                    }
127                }
128                import_path.to_string()
129            }
130        }
131    }
132
133    /// Detect package name from an import path
134    fn detect_package_from_path(&self, import_path: &str) -> Option<String> {
135        // Look for package patterns in the path
136        // This is based on path structure, not hardcoded names
137
138        // Pattern: ../../../package_name/...
139        if import_path.starts_with("../") {
140            let parts: Vec<&str> = import_path.split('/').collect();
141            // Find the first non-".." component
142            for part in parts {
143                if part != ".." && part != "." && !part.ends_with(".ncl") {
144                    // This might be a package name
145                    // Check if we know about this package
146                    if let PackageMode::Package { dependencies, .. } = self {
147                        if dependencies.contains_key(part) {
148                            return Some(part.to_string());
149                        }
150                        // Also check common transformations
151                        let normalized = part.replace('_', "-");
152                        if dependencies.contains_key(&normalized) {
153                            return Some(normalized);
154                        }
155                    }
156                    break;
157                }
158            }
159        }
160
161        None
162    }
163
164    /// Generate import statements for detected dependencies
165    pub fn generate_imports(&self, types: &[Type], current_package: &str) -> Vec<String> {
166        match self {
167            PackageMode::Package { analyzer, .. } => {
168                // Use analyzer to detect and generate imports
169                let mut analyzer = analyzer.clone();
170                analyzer.set_current_package(current_package);
171
172                let mut all_refs = std::collections::HashSet::new();
173                for ty in types {
174                    let refs = analyzer.analyze_type(ty, current_package);
175                    all_refs.extend(refs);
176                }
177
178                let deps = analyzer.determine_dependencies(&all_refs);
179                analyzer.generate_imports(&deps, true)
180            }
181            _ => Vec::new(),
182        }
183    }
184
185    /// Add Nickel package manifest fields based on detected dependencies
186    pub fn add_to_manifest(&self, content: &str, _package_name: &str) -> String {
187        if let PackageMode::Package { dependencies, .. } = self {
188            if !dependencies.is_empty() {
189                let mut deps_str = String::from("  dependencies = {\n");
190                for (dep_name, dep_info) in dependencies {
191                    deps_str.push_str(&format!(
192                        "    \"{}\" = \"{}\",\n",
193                        dep_name, dep_info.version
194                    ));
195                }
196                deps_str.push_str("  },\n");
197
198                // Insert dependencies into manifest
199                if content.contains("dependencies = {}") {
200                    content.replace("dependencies = {}", deps_str.trim_end())
201                } else {
202                    content.to_string()
203                }
204            } else {
205                content.to_string()
206            }
207        } else {
208            content.to_string()
209        }
210    }
211
212    /// Get the dependency map (for testing and debugging)
213    pub fn get_dependencies(&self) -> Option<&HashMap<String, PackageDependency>> {
214        match self {
215            PackageMode::Package { dependencies, .. } => Some(dependencies),
216            _ => None,
217        }
218    }
219}
220
221/// Helper to create a basic Nickel package manifest
222pub fn create_package_manifest(
223    name: &str,
224    version: &str,
225    description: &str,
226    keywords: Vec<String>,
227    dependencies: HashMap<String, String>,
228) -> String {
229    let deps = if dependencies.is_empty() {
230        "{}".to_string()
231    } else {
232        let entries: Vec<String> = dependencies
233            .iter()
234            .map(|(k, v)| format!("    \"{}\" = \"{}\"", k, v))
235            .collect();
236        format!("{{\n{}\n  }}", entries.join(",\n"))
237    };
238
239    format!(
240        r#"{{
241  name = "{}",
242  version = "{}",
243  description = "{}",
244  
245  keywords = [{}],
246  
247  dependencies = {},
248  
249  # Auto-generated by amalgam
250  minimal_nickel_version = "1.9.0",
251}} | std.package.Manifest
252"#,
253        name,
254        version,
255        description,
256        keywords
257            .iter()
258            .map(|k| format!("\"{}\"", k))
259            .collect::<Vec<_>>()
260            .join(", "),
261        deps
262    )
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn test_import_conversion_with_analyzer() {
271        let mode = PackageMode::new_with_analyzer(None);
272
273        // Test that imports are converted based on detected packages
274        let import = "../../../k8s_io/v1/objectmeta.ncl";
275        let converted = mode.convert_import(import);
276
277        // Without registered dependencies, should stay as-is
278        assert_eq!(converted, import);
279    }
280
281    #[test]
282    fn test_package_manifest_generation() {
283        let manifest = create_package_manifest(
284            "test-package",
285            "1.0.0",
286            "Test package",
287            vec!["test".to_string()],
288            HashMap::new(),
289        );
290
291        assert!(manifest.contains("name = \"test-package\""));
292        assert!(manifest.contains("version = \"1.0.0\""));
293        assert!(manifest.contains("dependencies = {}"));
294    }
295}