1use 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#[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 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 let enhanced_ty = self.enhance_kubernetes_type(ty)?;
79
80 builder = builder.add_type(type_name, enhanced_ty);
81
82 let version_ir = builder.build();
84 for module in version_ir.modules {
85 ir.add_module(module);
86 }
87 }
88 }
89
90 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 pub fn parse_version(&self, crd: &CRD, version_name: &str) -> Result<IR, ParserError> {
111 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 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 fn enhance_kubernetes_type(&self, ty: Type) -> Result<Type, ParserError> {
146 if let Type::Record { mut fields, open } = ty {
147 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 if let Some(status_field) = fields.get_mut("status") {
161 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 if matches!(conditions_field.ty, Type::Array(_)) {
170 }
172 }
173 }
174 }
175
176 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 fn enhance_field_type(&self, ty: Type) -> Result<Type, ParserError> {
189 self.enhance_field_type_with_context(ty, &[])
190 }
191
192 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 for (field_name, field) in enhanced_fields.iter_mut() {
204 if let Some(go_type) =
206 self.k8s_patterns.get_contextual_type(field_name, context)
207 {
208 let replacement_type = self.go_type_string_to_nickel_type(go_type)?;
210
211 let should_replace =
213 match (field_name.as_str(), &field.ty, go_type.as_str()) {
214 ("metadata", Type::Record { fields, .. }, _)
216 if fields.is_empty() =>
217 {
218 true
219 }
220
221 (_, Type::Array(_), go_type) if go_type.starts_with("[]") => true,
223
224 (_, Type::Record { fields, .. }, _) if fields.is_empty() => true,
226
227 ("nodeSelector", Type::Map { .. }, _) => false, _ => false,
231 };
232
233 if should_replace {
234 field.ty = replacement_type;
235 continue;
236 }
237 }
238
239 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 #[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 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 Ok(Type::Map {
271 key: Box::new(Type::String),
272 value: Box::new(Type::String),
273 })
274 } else if go_type.contains("/") {
275 Ok(Type::Reference(go_type.to_string()))
277 } else {
278 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 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}