use heck::ToPascalCase as _;
use utoipa::openapi;
use utoipa::openapi::schema::ArrayItems;
use utoipa::openapi::schema::{AdditionalProperties, KnownFormat, SchemaFormat, SchemaType, Type};
use crate::ast::common::GoDocComment;
use crate::ast::go_expression::GoExpression;
use crate::ast::ty::{GoPrimitive, GoStruct, GoTypeAlias, GoTypeDefinition};
use crate::config::GoHttpConfig;
use crate::consts::escape_go_keyword;
use crate::errors::GeneratorError;
use openapi_nexus_core::traits::OpenApiRefExt as _;
pub fn schema_to_go_expression(
schema_ref: &openapi::RefOr<openapi::Schema>,
components: Option<&openapi::Components>,
) -> Result<GoExpression, GeneratorError> {
match schema_ref {
openapi::RefOr::T(schema) => schema_to_go_expression_inner(schema, components),
openapi::RefOr::Ref(reference) => {
let schema_name =
reference
.schema_name()
.ok_or_else(|| GeneratorError::TypeMapping {
source: Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid schema reference: {:?}", reference),
)),
})?;
if let Some(components) = components
&& let Some(resolved_schema) = components.schemas.get(schema_name)
{
return schema_to_go_expression(resolved_schema, Some(components));
}
Ok(GoExpression::Reference(schema_name.to_pascal_case()))
}
}
}
fn schema_to_go_expression_inner(
schema: &openapi::Schema,
components: Option<&openapi::Components>,
) -> Result<GoExpression, GeneratorError> {
match schema {
openapi::Schema::Object(obj_schema) => {
if let Some(format) = &obj_schema.format {
match format {
SchemaFormat::KnownFormat(KnownFormat::DateTime) => {
Ok(GoExpression::Reference("time.Time".to_string()))
}
SchemaFormat::KnownFormat(KnownFormat::Date) => {
Ok(GoExpression::Reference("time.Time".to_string()))
}
SchemaFormat::KnownFormat(KnownFormat::Byte) => Ok(GoExpression::Slice(
Box::new(GoExpression::Primitive(GoPrimitive::Byte)),
)),
_ => Ok(GoExpression::Primitive(GoPrimitive::String)),
}
} else {
Ok(GoExpression::Primitive(GoPrimitive::String))
}
}
openapi::Schema::Array(arr_schema) => {
let item_type = match &arr_schema.items {
ArrayItems::RefOrSchema(item_ref) => schema_to_go_expression(item_ref, components)?,
ArrayItems::False => GoExpression::Any,
};
Ok(GoExpression::Slice(Box::new(item_type)))
}
openapi::Schema::OneOf(one_of) => {
if let Some(first_item) = one_of.items.first() {
schema_to_go_expression(first_item, components)
} else {
Ok(GoExpression::Any)
}
}
openapi::Schema::AnyOf(any_of) => {
if let Some(first_item) = any_of.items.first() {
schema_to_go_expression(first_item, components)
} else {
Ok(GoExpression::Any)
}
}
openapi::Schema::AllOf(all_of) => {
if let Some(first_item) = all_of.items.first() {
schema_to_go_expression(first_item, components)
} else {
Ok(GoExpression::Any)
}
}
_ => Ok(GoExpression::Any),
}
}
pub fn schema_to_go_type_definition(
name: &str,
schema_ref: &openapi::RefOr<openapi::Schema>,
components: &openapi::Components,
) -> Result<GoTypeDefinition, GeneratorError> {
let struct_name = name.to_pascal_case();
match schema_ref {
openapi::RefOr::T(openapi::Schema::Object(obj_schema)) => {
if let Some(enum_values) = &obj_schema.enum_values
&& !enum_values.is_empty()
{
let type_expr = GoExpression::Primitive(GoPrimitive::String);
let doc = obj_schema
.description
.as_ref()
.map(|d| GoDocComment::new(d.clone()));
return Ok(GoTypeDefinition::TypeAlias(
GoTypeAlias::new(struct_name, type_expr)
.with_doc(doc.unwrap_or_else(|| GoDocComment::new(String::new()))),
));
}
if !obj_schema.properties.is_empty() {
let mut fields = Vec::new();
for (prop_name, prop_schema) in &obj_schema.properties {
let field_name = escape_go_keyword(&prop_name.to_pascal_case());
let go_type = schema_to_go_expression(prop_schema, Some(components))?;
let required: bool = obj_schema.required.contains(prop_name);
let nullable = match prop_schema {
openapi::RefOr::T(openapi::Schema::Object(obj)) => {
matches!(&obj.schema_type, SchemaType::Array(types) if types.contains(&Type::Null))
|| matches!(&obj.schema_type, SchemaType::Type(Type::Null))
}
openapi::RefOr::T(openapi::Schema::Array(arr)) => {
matches!(&arr.schema_type, SchemaType::Array(types) if types.contains(&Type::Null))
|| matches!(&arr.schema_type, SchemaType::Type(Type::Null))
}
_ => false,
};
let final_type = if required && !nullable {
go_type
} else {
GoExpression::OptionalNullable(Box::new(go_type))
};
let json_tag = if required && !nullable {
format!("json:\"{}\"", prop_name)
} else {
format!("json:\"{},omitzero\"", prop_name)
};
let field = crate::ast::common::GoField::new(field_name, final_type)
.with_json_tag(json_tag);
fields.push(field);
}
if let Some(additional_props) = &obj_schema.additional_properties {
match additional_props.as_ref() {
AdditionalProperties::FreeForm(true) => {
let additional_field = crate::ast::common::GoField::new(
"AdditionalProperties".to_string(),
GoExpression::Map {
key: Box::new(GoExpression::Primitive(GoPrimitive::String)),
value: Box::new(GoExpression::Any),
},
)
.with_json_tag("additionalProperties:\"true\" json:\"-\"".to_string());
fields.push(additional_field);
}
AdditionalProperties::RefOr(schema_ref) => {
let value_type = schema_to_go_expression(schema_ref, Some(components))?;
let additional_field = crate::ast::common::GoField::new(
"AdditionalProperties".to_string(),
GoExpression::Map {
key: Box::new(GoExpression::Primitive(GoPrimitive::String)),
value: Box::new(value_type),
},
)
.with_json_tag("additionalProperties:\"true\" json:\"-\"".to_string());
fields.push(additional_field);
}
AdditionalProperties::FreeForm(false) => {
}
}
}
let doc = obj_schema
.description
.as_ref()
.map(|d| GoDocComment::new(d.clone()));
let mut struct_def = GoStruct::new(struct_name).with_fields(fields);
if let Some(doc) = doc {
struct_def = struct_def.with_doc(doc);
}
return Ok(GoTypeDefinition::Struct(struct_def));
}
let type_expr = schema_to_go_expression(schema_ref, Some(components))?;
let doc = obj_schema
.description
.as_ref()
.map(|d| GoDocComment::new(d.clone()));
let mut type_alias = GoTypeAlias::new(struct_name, type_expr);
if let Some(doc) = doc {
type_alias = type_alias.with_doc(doc);
}
Ok(GoTypeDefinition::TypeAlias(type_alias))
}
openapi::RefOr::T(openapi::Schema::OneOf(one_of)) => {
let type_expr = if let Some(first_item) = one_of.items.first() {
schema_to_go_expression(first_item, Some(components))?
} else {
GoExpression::Any
};
Ok(GoTypeDefinition::TypeAlias(GoTypeAlias::new(
struct_name,
type_expr,
)))
}
openapi::RefOr::T(openapi::Schema::AnyOf(any_of)) => {
let type_expr = if let Some(first_item) = any_of.items.first() {
schema_to_go_expression(first_item, Some(components))?
} else {
GoExpression::Any
};
Ok(GoTypeDefinition::TypeAlias(GoTypeAlias::new(
struct_name,
type_expr,
)))
}
openapi::RefOr::T(openapi::Schema::AllOf(all_of)) => {
let type_expr = if let Some(first_item) = all_of.items.first() {
schema_to_go_expression(first_item, Some(components))?
} else {
GoExpression::Any
};
Ok(GoTypeDefinition::TypeAlias(GoTypeAlias::new(
struct_name,
type_expr,
)))
}
openapi::RefOr::T(_) => {
let type_expr = schema_to_go_expression(schema_ref, Some(components))?;
Ok(GoTypeDefinition::TypeAlias(GoTypeAlias::new(
struct_name,
type_expr,
)))
}
openapi::RefOr::Ref(reference) => {
if let Some(schema_name) = reference.schema_name()
&& let Some(resolved) = components.schemas.get(schema_name)
{
return schema_to_go_type_definition(name, resolved, components);
}
let schema_name: String = reference.schema_name().unwrap_or(name).to_pascal_case();
let type_expr = GoExpression::Reference(schema_name);
Ok(GoTypeDefinition::TypeAlias(GoTypeAlias::new(
struct_name,
type_expr,
)))
}
}
}
pub fn generate_model_data(
name: &str,
schema_ref: &openapi::RefOr<openapi::Schema>,
components: &openapi::Components,
_config: &GoHttpConfig,
_header_data: &openapi_nexus_core::data::HeaderData,
) -> Result<(GoTypeDefinition, Vec<String>, Vec<String>), GeneratorError> {
let type_def = schema_to_go_type_definition(name, schema_ref, components)?;
let mut imports = Vec::new();
let needs_optionalnullable = match &type_def {
crate::ast::ty::GoTypeDefinition::Struct(s) => s.fields.iter().any(|f| {
matches!(
&f.field_type,
crate::ast::go_expression::GoExpression::OptionalNullable(_)
)
}),
_ => false,
};
if needs_optionalnullable {
imports.push("optionalnullable".to_string());
}
let needs_utils = matches!(&type_def, crate::ast::ty::GoTypeDefinition::Struct(_));
if needs_utils {
imports.push("internal/utils".to_string());
}
let required_fields = match schema_ref {
openapi::RefOr::T(openapi::Schema::Object(obj_schema)) => obj_schema.required.clone(),
_ => Vec::new(),
};
Ok((type_def, imports, required_fields))
}