1use crate::types::Type;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::path::Path;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct TypeReference {
14 pub full_name: String,
16 pub simple_name: String,
18 pub api_group: Option<String>,
20 pub source_location: String,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DetectedDependency {
27 pub package_name: String,
29 pub required_types: HashSet<String>,
31 pub api_version: Option<String>,
33 pub is_core_type: bool,
35}
36
37#[derive(Debug, Clone)]
39pub struct DependencyAnalyzer {
40 type_registry: HashMap<String, String>,
43 api_group_registry: HashMap<String, String>,
45 current_package: Option<String>,
47}
48
49impl DependencyAnalyzer {
50 pub fn new() -> Self {
52 Self {
53 type_registry: HashMap::new(),
54 api_group_registry: HashMap::new(),
55 current_package: None,
56 }
57 }
58
59 pub fn register_from_manifest(&mut self, manifest_path: &Path) -> Result<(), String> {
61 let content = std::fs::read_to_string(manifest_path)
63 .map_err(|e| format!("Failed to read manifest: {}", e))?;
64
65 let manifest: toml::Value =
66 toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
67
68 if let Some(packages) = manifest.get("packages").and_then(|p| p.as_array()) {
69 for package in packages {
70 if let Some(name) = package.get("name").and_then(|n| n.as_str()) {
71 if name == "k8s-io" {
73 self.register_k8s_core_types();
74 } else if let Some(type_val) = package.get("type").and_then(|t| t.as_str()) {
75 if type_val == "url" {
76 if let Some(url) = package.get("url").and_then(|u| u.as_str()) {
77 self.register_package_from_url(name, url);
78 }
79 }
80 }
81 }
82 }
83 }
84
85 Ok(())
86 }
87
88 fn register_k8s_core_types(&mut self) {
90 let core_types = vec![
93 ("ObjectMeta", "io.k8s.apimachinery.pkg.apis.meta.v1"),
94 ("ListMeta", "io.k8s.apimachinery.pkg.apis.meta.v1"),
95 ("LabelSelector", "io.k8s.apimachinery.pkg.apis.meta.v1"),
96 ("Time", "io.k8s.apimachinery.pkg.apis.meta.v1"),
97 ("MicroTime", "io.k8s.apimachinery.pkg.apis.meta.v1"),
98 ("Status", "io.k8s.apimachinery.pkg.apis.meta.v1"),
99 ("StatusDetails", "io.k8s.apimachinery.pkg.apis.meta.v1"),
100 ("DeleteOptions", "io.k8s.apimachinery.pkg.apis.meta.v1"),
101 ("OwnerReference", "io.k8s.apimachinery.pkg.apis.meta.v1"),
102 ("ManagedFieldsEntry", "io.k8s.apimachinery.pkg.apis.meta.v1"),
103 ("Condition", "io.k8s.apimachinery.pkg.apis.meta.v1"),
104 ("Volume", "io.k8s.api.core.v1"),
105 ("VolumeMount", "io.k8s.api.core.v1"),
106 ("Container", "io.k8s.api.core.v1"),
107 ("PodSpec", "io.k8s.api.core.v1"),
108 ("ResourceRequirements", "io.k8s.api.core.v1"),
109 ("Affinity", "io.k8s.api.core.v1"),
110 ("Toleration", "io.k8s.api.core.v1"),
111 ("LocalObjectReference", "io.k8s.api.core.v1"),
112 ("SecretKeySelector", "io.k8s.api.core.v1"),
113 ("ConfigMapKeySelector", "io.k8s.api.core.v1"),
114 ];
115
116 for (type_name, api_group) in core_types {
117 self.type_registry
118 .insert(type_name.to_string(), "k8s_io".to_string());
119 self.api_group_registry
120 .insert(api_group.to_string(), "k8s_io".to_string());
121 }
122 }
123
124 fn register_package_from_url(&mut self, package_name: &str, url: &str) {
126 if url.contains("github.com") {
128 if let Some(parts) = url.split("github.com/").nth(1) {
129 let components: Vec<&str> = parts.split('/').collect();
130 if components.len() >= 2 {
131 let org = components[0];
132 let repo = components[1];
133
134 let api_groups = match (org, repo) {
137 (org, _) if org.contains("crossplane") => {
138 vec![format!("apiextensions.{}.io", org), format!("{}.io", org)]
139 }
140 ("prometheus-operator", _repo) => {
141 vec!["monitoring.coreos.com".to_string()]
142 }
143 ("cert-manager", _repo) => {
144 vec![
145 "cert-manager.io".to_string(),
146 "acme.cert-manager.io".to_string(),
147 ]
148 }
149 (org, _) => {
150 vec![
151 format!("{}.io", org.replace('-', ".")),
152 format!("{}.com", org),
153 ]
154 }
155 };
156
157 for api_group in api_groups {
158 self.api_group_registry
159 .insert(api_group, package_name.to_string());
160 }
161 }
162 }
163 }
164 }
165
166 pub fn analyze_type(&self, ty: &Type, current_package: &str) -> HashSet<TypeReference> {
168 let mut refs = HashSet::new();
169 self.collect_type_references(ty, &mut refs, current_package);
170 refs
171 }
172
173 fn collect_type_references(
175 &self,
176 ty: &Type,
177 refs: &mut HashSet<TypeReference>,
178 location: &str,
179 ) {
180 match ty {
181 Type::Reference(name) => {
182 if let Some(type_ref) = self.parse_type_reference(name, location) {
184 refs.insert(type_ref);
185 }
186 }
187 Type::Array(inner) => {
188 self.collect_type_references(inner, refs, location);
189 }
190 Type::Optional(inner) => {
191 self.collect_type_references(inner, refs, location);
192 }
193 Type::Map { value, .. } => {
194 self.collect_type_references(value, refs, location);
195 }
196 Type::Record { fields, .. } => {
197 for (field_name, field) in fields {
198 let field_location = format!("{}.{}", location, field_name);
199 self.collect_type_references(&field.ty, refs, &field_location);
200 }
201 }
202 Type::Union(types) => {
203 for t in types {
204 self.collect_type_references(t, refs, location);
205 }
206 }
207 Type::TaggedUnion { variants, .. } => {
208 for (variant_name, t) in variants {
209 let variant_location = format!("{}[{}]", location, variant_name);
210 self.collect_type_references(t, refs, &variant_location);
211 }
212 }
213 Type::Contract { base, .. } => {
214 self.collect_type_references(base, refs, location);
215 }
216 _ => {}
217 }
218 }
219
220 fn parse_type_reference(&self, name: &str, location: &str) -> Option<TypeReference> {
222 let simple_name = name.split('.').next_back().unwrap_or(name).to_string();
224
225 if self.type_registry.contains_key(&simple_name) {
227 if let Some(package) = self.type_registry.get(&simple_name) {
229 if Some(package.as_str()) != self.current_package.as_deref() {
230 return Some(TypeReference {
231 full_name: name.to_string(),
232 simple_name,
233 api_group: self.extract_api_group(name),
234 source_location: location.to_string(),
235 });
236 }
237 }
238 }
239
240 if let Some(api_group) = self.extract_api_group(name) {
242 if self.api_group_registry.contains_key(&api_group) {
243 return Some(TypeReference {
244 full_name: name.to_string(),
245 simple_name,
246 api_group: Some(api_group),
247 source_location: location.to_string(),
248 });
249 }
250 }
251
252 None
253 }
254
255 fn extract_api_group(&self, full_name: &str) -> Option<String> {
257 let parts: Vec<&str> = full_name.split('.').collect();
259 if parts.len() > 1 {
260 let api_group = parts[..parts.len() - 1].join(".");
262 if api_group.contains('.') {
263 return Some(api_group);
264 }
265 }
266 None
267 }
268
269 pub fn determine_dependencies(
271 &self,
272 type_refs: &HashSet<TypeReference>,
273 ) -> Vec<DetectedDependency> {
274 let mut dependencies: HashMap<String, DetectedDependency> = HashMap::new();
275
276 for type_ref in type_refs {
277 let package_name = if let Some(name) = self.type_registry.get(&type_ref.simple_name) {
279 name.clone()
280 } else if let Some(api_group) = &type_ref.api_group {
281 if let Some(name) = self.api_group_registry.get(api_group) {
282 name.clone()
283 } else {
284 continue; }
286 } else {
287 continue; };
289
290 let entry =
292 dependencies
293 .entry(package_name.clone())
294 .or_insert_with(|| DetectedDependency {
295 package_name: package_name.clone(),
296 required_types: HashSet::new(),
297 api_version: type_ref.api_group.clone(),
298 is_core_type: package_name == "k8s_io",
299 });
300
301 entry.required_types.insert(type_ref.simple_name.clone());
302 }
303
304 dependencies.into_values().collect()
305 }
306
307 pub fn set_current_package(&mut self, package: &str) {
309 self.current_package = Some(package.to_string());
310 }
311
312 pub fn generate_imports(
314 &self,
315 dependencies: &[DetectedDependency],
316 package_mode: bool,
317 ) -> Vec<String> {
318 let mut imports = Vec::new();
319
320 for dep in dependencies {
321 if package_mode {
322 imports.push(format!(
324 "let {} = import \"{}\" in",
325 dep.package_name.replace('-', "_"),
326 dep.package_name
327 ));
328 } else {
329 let path = self.calculate_relative_path(&dep.package_name);
332 imports.push(format!(
333 "let {} = import \"{}\" in",
334 dep.package_name.replace('-', "_"),
335 path
336 ));
337 }
338 }
339
340 imports
341 }
342
343 fn calculate_relative_path(&self, target_package: &str) -> String {
345 format!("../../../{}/mod.ncl", target_package)
348 }
349}
350
351impl Default for DependencyAnalyzer {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_type_reference_detection() {
363 let mut analyzer = DependencyAnalyzer::new();
364 analyzer.register_k8s_core_types();
366
367 let type_ref = analyzer.parse_type_reference(
369 "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
370 "test_location",
371 );
372
373 assert!(type_ref.is_some());
374 let type_ref = type_ref.unwrap();
375 assert_eq!(type_ref.simple_name, "ObjectMeta");
376 assert_eq!(
377 type_ref.api_group,
378 Some("io.k8s.apimachinery.pkg.apis.meta.v1".to_string())
379 );
380 }
381
382 #[test]
383 fn test_dependency_detection() {
384 let mut analyzer = DependencyAnalyzer::new();
385 analyzer.register_k8s_core_types();
386 analyzer.set_current_package("crossplane");
387
388 let mut refs = HashSet::new();
389 refs.insert(TypeReference {
390 full_name: "ObjectMeta".to_string(),
391 simple_name: "ObjectMeta".to_string(),
392 api_group: Some("io.k8s.apimachinery.pkg.apis.meta.v1".to_string()),
393 source_location: "spec.metadata".to_string(),
394 });
395
396 let deps = analyzer.determine_dependencies(&refs);
397 assert_eq!(deps.len(), 1);
398 assert_eq!(deps[0].package_name, "k8s_io");
399 assert!(deps[0].required_types.contains("ObjectMeta"));
400 }
401}