amalgam_parser/
crd.rs

1//! Kubernetes CRD parser
2
3use crate::{imports::ImportResolver, k8s_authoritative::K8sTypePatterns, Parser, ParserError};
4use amalgam_core::{
5    ir::{IRBuilder, IR},
6    types::Type,
7};
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11/// Kubernetes CustomResourceDefinition (simplified)
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CRD {
14    #[serde(rename = "apiVersion")]
15    pub api_version: String,
16    pub kind: String,
17    pub metadata: CRDMetadata,
18    pub spec: CRDSpec,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CRDMetadata {
23    pub name: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CRDSpec {
28    pub group: String,
29    pub versions: Vec<CRDVersion>,
30    pub names: CRDNames,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct CRDVersion {
35    pub name: String,
36    pub served: bool,
37    pub storage: bool,
38    pub schema: Option<CRDSchema>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct CRDSchema {
43    #[serde(rename = "openAPIV3Schema")]
44    pub openapi_v3_schema: serde_json::Value,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CRDNames {
49    pub plural: String,
50    pub singular: String,
51    pub kind: String,
52}
53
54pub struct CRDParser {
55    _import_resolver: ImportResolver,
56    k8s_patterns: K8sTypePatterns,
57}
58
59impl Parser for CRDParser {
60    type Input = CRD;
61
62    fn parse(&self, input: Self::Input) -> Result<IR, ParserError> {
63        let mut ir = IR::new();
64
65        // Create a separate module for each version
66        for version in input.spec.versions {
67            if let Some(schema) = version.schema {
68                let module_name = format!(
69                    "{}.{}.{}",
70                    input.spec.names.kind, version.name, input.spec.group
71                );
72                let mut builder = IRBuilder::new().module(module_name);
73
74                let type_name = input.spec.names.kind.clone();
75                let ty = self.json_schema_to_type(&schema.openapi_v3_schema)?;
76
77                // Enhance the type with proper k8s fields
78                let enhanced_ty = self.enhance_kubernetes_type(ty)?;
79
80                builder = builder.add_type(type_name, enhanced_ty);
81
82                // Add this version's module to the IR
83                let version_ir = builder.build();
84                for module in version_ir.modules {
85                    ir.add_module(module);
86                }
87            }
88        }
89
90        // If no versions had schemas, create an empty module
91        if ir.modules.is_empty() {
92            let module_name = format!("{}.{}", input.spec.names.kind, input.spec.group);
93            let builder = IRBuilder::new().module(module_name);
94            ir = builder.build();
95        }
96
97        Ok(ir)
98    }
99}
100
101impl CRDParser {
102    pub fn new() -> Self {
103        Self {
104            _import_resolver: ImportResolver::new(),
105            k8s_patterns: K8sTypePatterns::new(),
106        }
107    }
108
109    /// Parse a specific version of a CRD
110    pub fn parse_version(&self, crd: &CRD, version_name: &str) -> Result<IR, ParserError> {
111        // Find the specific version
112        let version = crd
113            .spec
114            .versions
115            .iter()
116            .find(|v| v.name == version_name)
117            .ok_or_else(|| {
118                ParserError::Parse(format!("Version {} not found in CRD", version_name))
119            })?;
120
121        if let Some(schema) = &version.schema {
122            let module_name = format!(
123                "{}.{}.{}",
124                crd.spec.names.kind, version.name, crd.spec.group
125            );
126            let mut builder = IRBuilder::new().module(module_name);
127
128            let type_name = crd.spec.names.kind.clone();
129            let ty = self.json_schema_to_type(&schema.openapi_v3_schema)?;
130
131            // Enhance the type with proper k8s fields
132            let enhanced_ty = self.enhance_kubernetes_type(ty)?;
133
134            builder = builder.add_type(type_name, enhanced_ty);
135            Ok(builder.build())
136        } else {
137            Err(ParserError::Parse(format!(
138                "Version {} has no schema",
139                version_name
140            )))
141        }
142    }
143
144    /// Enhance a Kubernetes resource type with proper field references
145    fn enhance_kubernetes_type(&self, ty: Type) -> Result<Type, ParserError> {
146        if let Type::Record { mut fields, open } = ty {
147            // Check for standard Kubernetes fields
148
149            // Replace empty metadata with ObjectMeta reference
150            if let Some(metadata_field) = fields.get_mut("metadata") {
151                if matches!(metadata_field.ty, Type::Record { ref fields, .. } if fields.is_empty())
152                {
153                    metadata_field.ty = Type::Reference(
154                        "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta".to_string(),
155                    );
156                }
157            }
158
159            // Check for status field that might need enhancement
160            if let Some(status_field) = fields.get_mut("status") {
161                // Status fields often reference common condition types
162                if let Type::Record {
163                    fields: ref mut status_fields,
164                    ..
165                } = &mut status_field.ty
166                {
167                    if let Some(conditions_field) = status_fields.get_mut("conditions") {
168                        // Conditions are often arrays of metav1.Condition
169                        if matches!(conditions_field.ty, Type::Array(_)) {
170                            // Could enhance with proper Condition type reference
171                        }
172                    }
173                }
174            }
175
176            // Recursively enhance nested record types
177            for field in fields.values_mut() {
178                field.ty = self.enhance_field_type(field.ty.clone())?;
179            }
180
181            Ok(Type::Record { fields, open })
182        } else {
183            Ok(ty)
184        }
185    }
186
187    /// Enhance field types using authoritative Kubernetes type patterns
188    fn enhance_field_type(&self, ty: Type) -> Result<Type, ParserError> {
189        self.enhance_field_type_with_context(ty, &[])
190    }
191
192    /// Enhance field types with context path for more precise matching
193    fn enhance_field_type_with_context(
194        &self,
195        ty: Type,
196        context: &[&str],
197    ) -> Result<Type, ParserError> {
198        match ty {
199            Type::Record { fields, open } => {
200                let mut enhanced_fields = fields;
201
202                // Check for fields using authoritative patterns
203                for (field_name, field) in enhanced_fields.iter_mut() {
204                    // Check if we have an authoritative type for this field
205                    if let Some(go_type) =
206                        self.k8s_patterns.get_contextual_type(field_name, context)
207                    {
208                        // Convert Go type string to appropriate Nickel type
209                        let replacement_type = self.go_type_string_to_nickel_type(go_type)?;
210
211                        // Only replace if the current type matches expected pattern
212                        let should_replace =
213                            match (field_name.as_str(), &field.ty, go_type.as_str()) {
214                                // Metadata should be ObjectMeta if it's currently empty
215                                ("metadata", Type::Record { fields, .. }, _)
216                                    if fields.is_empty() =>
217                                {
218                                    true
219                                }
220
221                                // Arrays should be replaced if currently generic
222                                (_, Type::Array(_), go_type) if go_type.starts_with("[]") => true,
223
224                                // Records should be replaced if they're empty or generic
225                                (_, Type::Record { fields, .. }, _) if fields.is_empty() => true,
226
227                                // Maps for specific patterns
228                                ("nodeSelector", Type::Map { .. }, _) => false, // Keep as map
229
230                                _ => false,
231                            };
232
233                        if should_replace {
234                            field.ty = replacement_type;
235                            continue;
236                        }
237                    }
238
239                    // Recursively enhance nested types with updated context
240                    let mut new_context = context.to_vec();
241                    new_context.push(field_name);
242                    field.ty =
243                        self.enhance_field_type_with_context(field.ty.clone(), &new_context)?;
244                }
245
246                Ok(Type::Record {
247                    fields: enhanced_fields,
248                    open,
249                })
250            }
251            Type::Array(inner) => Ok(Type::Array(Box::new(
252                self.enhance_field_type_with_context(*inner, context)?,
253            ))),
254            Type::Optional(inner) => Ok(Type::Optional(Box::new(
255                self.enhance_field_type_with_context(*inner, context)?,
256            ))),
257            _ => Ok(ty),
258        }
259    }
260
261    /// Convert Go type string to Nickel Type
262    #[allow(clippy::only_used_in_recursion)]
263    fn go_type_string_to_nickel_type(&self, go_type: &str) -> Result<Type, ParserError> {
264        if let Some(elem_type) = go_type.strip_prefix("[]") {
265            // Array type
266            let elem = self.go_type_string_to_nickel_type(elem_type)?;
267            Ok(Type::Array(Box::new(elem)))
268        } else if go_type.starts_with("map[") {
269            // For now, keep as generic map - could be more sophisticated
270            Ok(Type::Map {
271                key: Box::new(Type::String),
272                value: Box::new(Type::String),
273            })
274        } else if go_type.contains("/") {
275            // Qualified type name - create reference
276            Ok(Type::Reference(go_type.to_string()))
277        } else {
278            // Basic types or unqualified names
279            match go_type {
280                "string" => Ok(Type::String),
281                "int" | "int32" | "int64" => Ok(Type::Integer),
282                "float32" | "float64" => Ok(Type::Number),
283                "bool" => Ok(Type::Bool),
284                _ => Ok(Type::Reference(go_type.to_string())),
285            }
286        }
287    }
288
289    #[allow(clippy::only_used_in_recursion)]
290    fn json_schema_to_type(&self, schema: &serde_json::Value) -> Result<Type, ParserError> {
291        use serde_json::Value;
292
293        let schema_type = schema.get("type").and_then(|v| v.as_str());
294
295        match schema_type {
296            Some("string") => Ok(Type::String),
297            Some("number") => Ok(Type::Number),
298            Some("integer") => Ok(Type::Integer),
299            Some("boolean") => Ok(Type::Bool),
300            Some("null") => Ok(Type::Null),
301            Some("array") => {
302                let items = schema
303                    .get("items")
304                    .map(|i| self.json_schema_to_type(i))
305                    .transpose()?
306                    .unwrap_or(Type::Any);
307                Ok(Type::Array(Box::new(items)))
308            }
309            Some("object") => {
310                let mut fields = BTreeMap::new();
311                if let Some(Value::Object(props)) = schema.get("properties") {
312                    let required = schema
313                        .get("required")
314                        .and_then(|r| r.as_array())
315                        .map(|arr| {
316                            arr.iter()
317                                .filter_map(|v| v.as_str())
318                                .map(String::from)
319                                .collect::<Vec<_>>()
320                        })
321                        .unwrap_or_default();
322
323                    for (name, prop_schema) in props {
324                        let ty = self.json_schema_to_type(prop_schema)?;
325                        fields.insert(
326                            name.clone(),
327                            amalgam_core::types::Field {
328                                ty,
329                                required: required.contains(name),
330                                description: prop_schema
331                                    .get("description")
332                                    .and_then(|d| d.as_str())
333                                    .map(String::from),
334                                default: prop_schema.get("default").cloned(),
335                            },
336                        );
337                    }
338                }
339
340                let open = schema
341                    .get("additionalProperties")
342                    .and_then(|v| v.as_bool())
343                    .unwrap_or(false);
344
345                Ok(Type::Record { fields, open })
346            }
347            _ => {
348                // Check for oneOf, anyOf, allOf
349                if let Some(Value::Array(schemas)) = schema.get("oneOf") {
350                    let types = schemas
351                        .iter()
352                        .map(|s| self.json_schema_to_type(s))
353                        .collect::<Result<Vec<_>, _>>()?;
354                    return Ok(Type::Union(types));
355                }
356
357                if let Some(Value::Array(schemas)) = schema.get("anyOf") {
358                    let types = schemas
359                        .iter()
360                        .map(|s| self.json_schema_to_type(s))
361                        .collect::<Result<Vec<_>, _>>()?;
362                    return Ok(Type::Union(types));
363                }
364
365                Ok(Type::Any)
366            }
367        }
368    }
369}
370
371impl Default for CRDParser {
372    fn default() -> Self {
373        Self::new()
374    }
375}