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 { dependencies, .. } => {
112                // Check if this import references an external package
113                if let Some(package_name) = self.detect_package_from_path(import_path) {
114                    // Look up the full package ID from dependencies
115                    if let Some(dep) = dependencies.get(&package_name) {
116                        // Use the full package ID as-is (it should already be properly formatted)
117                        dep.package_id.clone()
118                    } else {
119                        // Fallback to bare package name if not found in dependencies
120                        format!("\"{}\"", package_name)
121                    }
122                } else {
123                    // Keep as relative import within same package
124                    import_path.to_string()
125                }
126            }
127            PackageMode::LocalDevelopment { local_paths } => {
128                // Check if this import references a local package
129                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    /// Detect package name from an import path
140    fn detect_package_from_path(&self, import_path: &str) -> Option<String> {
141        // Look for package patterns in the path
142        // This is based on path structure, not hardcoded names
143
144        // Pattern: ../../../package_name/...
145        if import_path.starts_with("../") {
146            let parts: Vec<&str> = import_path.split('/').collect();
147            // Find the first non-".." component
148            for part in parts {
149                if part != ".." && part != "." && !part.ends_with(".ncl") {
150                    // This might be a package name
151                    // Check if we know about this package
152                    if let PackageMode::Package { dependencies, .. } = self {
153                        if dependencies.contains_key(part) {
154                            return Some(part.to_string());
155                        }
156                        // Also check common transformations
157                        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    /// Generate import statements for detected dependencies
171    pub fn generate_imports(&self, types: &[Type], current_package: &str) -> Vec<String> {
172        match self {
173            PackageMode::Package { analyzer, .. } => {
174                // Use analyzer to detect and generate imports
175                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    /// Add Nickel package manifest fields based on detected dependencies
192    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                // Insert dependencies into manifest
205                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    /// Get the dependency map (for testing and debugging)
219    pub fn get_dependencies(&self) -> Option<&HashMap<String, PackageDependency>> {
220        match self {
221            PackageMode::Package { dependencies, .. } => Some(dependencies),
222            _ => None,
223        }
224    }
225}
226
227/// Helper to create a basic Nickel package manifest
228pub 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        // Test that imports are converted based on detected packages
280        let import = "../../../k8s_io/v1/objectmeta.ncl";
281        let converted = mode.convert_import(import);
282
283        // Without registered dependencies, should stay as-is
284        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}