use crate::{imports::ImportResolver, k8s_authoritative::K8sTypePatterns, Parser, ParserError};
use amalgam_core::{
ir::{IRBuilder, IR},
types::Type,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CRD {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: CRDMetadata,
pub spec: CRDSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CRDMetadata {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CRDSpec {
pub group: String,
pub versions: Vec<CRDVersion>,
pub names: CRDNames,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CRDVersion {
pub name: String,
pub served: bool,
pub storage: bool,
pub schema: Option<CRDSchema>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CRDSchema {
#[serde(rename = "openAPIV3Schema")]
pub openapi_v3_schema: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CRDNames {
pub plural: String,
pub singular: String,
pub kind: String,
}
pub struct CRDParser {
_import_resolver: ImportResolver,
k8s_patterns: K8sTypePatterns,
}
impl Parser for CRDParser {
type Input = CRD;
fn parse(&self, input: Self::Input) -> Result<IR, ParserError> {
let mut ir = IR::new();
for version in input.spec.versions {
if let Some(schema) = version.schema {
let module_name = format!(
"{}.{}.{}",
input.spec.names.kind, version.name, input.spec.group
);
let mut builder = IRBuilder::new().module(module_name);
let type_name = input.spec.names.kind.clone();
let ty = self.json_schema_to_type(&schema.openapi_v3_schema)?;
let enhanced_ty = self.enhance_kubernetes_type(ty)?;
builder = builder.add_type(type_name, enhanced_ty);
let version_ir = builder.build();
for module in version_ir.modules {
ir.add_module(module);
}
}
}
if ir.modules.is_empty() {
let module_name = format!("{}.{}", input.spec.names.kind, input.spec.group);
let builder = IRBuilder::new().module(module_name);
ir = builder.build();
}
Ok(ir)
}
}
impl CRDParser {
pub fn new() -> Self {
Self {
_import_resolver: ImportResolver::new(),
k8s_patterns: K8sTypePatterns::new(),
}
}
pub fn parse_version(&self, crd: &CRD, version_name: &str) -> Result<IR, ParserError> {
let version = crd
.spec
.versions
.iter()
.find(|v| v.name == version_name)
.ok_or_else(|| {
ParserError::Parse(format!("Version {} not found in CRD", version_name))
})?;
if let Some(schema) = &version.schema {
let module_name = format!(
"{}.{}.{}",
crd.spec.names.kind, version.name, crd.spec.group
);
let mut builder = IRBuilder::new().module(module_name);
let type_name = crd.spec.names.kind.clone();
let ty = self.json_schema_to_type(&schema.openapi_v3_schema)?;
let enhanced_ty = self.enhance_kubernetes_type(ty)?;
builder = builder.add_type(type_name, enhanced_ty);
Ok(builder.build())
} else {
Err(ParserError::Parse(format!(
"Version {} has no schema",
version_name
)))
}
}
fn enhance_kubernetes_type(&self, ty: Type) -> Result<Type, ParserError> {
if let Type::Record { mut fields, open } = ty {
if let Some(metadata_field) = fields.get_mut("metadata") {
if matches!(metadata_field.ty, Type::Record { ref fields, .. } if fields.is_empty())
{
metadata_field.ty = Type::Reference(
"io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta".to_string(),
);
}
}
if let Some(status_field) = fields.get_mut("status") {
if let Type::Record {
fields: ref mut status_fields,
..
} = &mut status_field.ty
{
if let Some(conditions_field) = status_fields.get_mut("conditions") {
if matches!(conditions_field.ty, Type::Array(_)) {
}
}
}
}
for field in fields.values_mut() {
field.ty = self.enhance_field_type(field.ty.clone())?;
}
Ok(Type::Record { fields, open })
} else {
Ok(ty)
}
}
fn enhance_field_type(&self, ty: Type) -> Result<Type, ParserError> {
self.enhance_field_type_with_context(ty, &[])
}
fn enhance_field_type_with_context(
&self,
ty: Type,
context: &[&str],
) -> Result<Type, ParserError> {
match ty {
Type::Record { fields, open } => {
let mut enhanced_fields = fields;
for (field_name, field) in enhanced_fields.iter_mut() {
if let Some(go_type) =
self.k8s_patterns.get_contextual_type(field_name, context)
{
let replacement_type = self.go_type_string_to_nickel_type(go_type)?;
let should_replace =
match (field_name.as_str(), &field.ty, go_type.as_str()) {
("metadata", Type::Record { fields, .. }, _)
if fields.is_empty() =>
{
true
}
(_, Type::Array(_), go_type) if go_type.starts_with("[]") => true,
(_, Type::Record { fields, .. }, _) if fields.is_empty() => true,
("nodeSelector", Type::Map { .. }, _) => false,
_ => false,
};
if should_replace {
field.ty = replacement_type;
continue;
}
}
let mut new_context = context.to_vec();
new_context.push(field_name);
field.ty =
self.enhance_field_type_with_context(field.ty.clone(), &new_context)?;
}
Ok(Type::Record {
fields: enhanced_fields,
open,
})
}
Type::Array(inner) => Ok(Type::Array(Box::new(
self.enhance_field_type_with_context(*inner, context)?,
))),
Type::Optional(inner) => Ok(Type::Optional(Box::new(
self.enhance_field_type_with_context(*inner, context)?,
))),
_ => Ok(ty),
}
}
#[allow(clippy::only_used_in_recursion)]
fn go_type_string_to_nickel_type(&self, go_type: &str) -> Result<Type, ParserError> {
if let Some(elem_type) = go_type.strip_prefix("[]") {
let elem = self.go_type_string_to_nickel_type(elem_type)?;
Ok(Type::Array(Box::new(elem)))
} else if go_type.starts_with("map[") {
Ok(Type::Map {
key: Box::new(Type::String),
value: Box::new(Type::String),
})
} else if go_type.contains("/") {
Ok(Type::Reference(go_type.to_string()))
} else {
match go_type {
"string" => Ok(Type::String),
"int" | "int32" | "int64" => Ok(Type::Integer),
"float32" | "float64" => Ok(Type::Number),
"bool" => Ok(Type::Bool),
_ => Ok(Type::Reference(go_type.to_string())),
}
}
}
#[allow(clippy::only_used_in_recursion)]
fn json_schema_to_type(&self, schema: &serde_json::Value) -> Result<Type, ParserError> {
use serde_json::Value;
let schema_type = schema.get("type").and_then(|v| v.as_str());
match schema_type {
Some("string") => Ok(Type::String),
Some("number") => Ok(Type::Number),
Some("integer") => Ok(Type::Integer),
Some("boolean") => Ok(Type::Bool),
Some("null") => Ok(Type::Null),
Some("array") => {
let items = schema
.get("items")
.map(|i| self.json_schema_to_type(i))
.transpose()?
.unwrap_or(Type::Any);
Ok(Type::Array(Box::new(items)))
}
Some("object") => {
let mut fields = BTreeMap::new();
if let Some(Value::Object(props)) = schema.get("properties") {
let required = schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect::<Vec<_>>()
})
.unwrap_or_default();
for (name, prop_schema) in props {
let ty = self.json_schema_to_type(prop_schema)?;
fields.insert(
name.clone(),
amalgam_core::types::Field {
ty,
required: required.contains(name),
description: prop_schema
.get("description")
.and_then(|d| d.as_str())
.map(String::from),
default: prop_schema.get("default").cloned(),
},
);
}
}
let open = schema
.get("additionalProperties")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(Type::Record { fields, open })
}
_ => {
if let Some(Value::Array(schemas)) = schema.get("oneOf") {
let types = schemas
.iter()
.map(|s| self.json_schema_to_type(s))
.collect::<Result<Vec<_>, _>>()?;
return Ok(Type::Union(types));
}
if let Some(Value::Array(schemas)) = schema.get("anyOf") {
let types = schemas
.iter()
.map(|s| self.json_schema_to_type(s))
.collect::<Result<Vec<_>, _>>()?;
return Ok(Type::Union(types));
}
Ok(Type::Any)
}
}
}
}
impl Default for CRDParser {
fn default() -> Self {
Self::new()
}
}