use std::rc::Rc;
use oas3::spec::ObjectOrReference;
use super::{
inline_resolver::InlineTypeResolver,
methods::MethodGenerator,
parameters::{ConvertedParams, ParameterConverter},
};
use crate::{
generator::{
ast::{
ContentCategory, Documentation, FieldDef, FieldNameToken, MultipartFieldInfo, OperationBody, RustPrimitive,
RustType, StructDef, StructKind, StructMethod, StructToken, TypeRef,
},
converter::ConverterContext,
naming::{
constants::{BODY_FIELD_NAME, REQUEST_BODY_SUFFIX},
identifiers::to_rust_type_name,
},
operation_registry::OperationEntry,
schema_registry::SchemaRegistry,
},
utils::SchemaExt,
};
#[derive(Debug, Clone)]
pub(crate) struct RequestOutput {
pub(crate) main_struct: StructDef,
pub(crate) nested_structs: Vec<StructDef>,
pub(crate) inline_types: Vec<RustType>,
pub(crate) parameter_fields: Vec<FieldDef>,
pub(crate) warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct RequestConverter {
param_converter: ParameterConverter,
}
impl RequestConverter {
pub(crate) fn new(context: &Rc<ConverterContext>) -> Self {
Self {
param_converter: ParameterConverter::new(context),
}
}
pub(crate) fn build(
&self,
name: &str,
entry: &OperationEntry,
body_info: &BodyInfo,
extra_method: Option<StructMethod>,
) -> anyhow::Result<RequestOutput> {
let params = self.param_converter.convert_all(name, &entry.path, &entry.operation)?;
let ConvertedParams {
mut main_fields,
nested_structs,
all_fields,
inline_types,
warnings,
} = params;
if let Some(body_field) = body_info.create_field() {
main_fields.push(body_field);
}
let methods = extra_method
.into_iter()
.chain(MethodGenerator::build_builder_method(&nested_structs, &main_fields))
.collect::<Vec<_>>();
let main_struct = StructDef::builder()
.name(StructToken::new(name))
.docs(Documentation::from_optional(
entry
.operation
.description
.as_ref()
.or(entry.operation.summary.as_ref()),
))
.fields(main_fields)
.methods(methods)
.kind(StructKind::OperationRequest)
.build();
Ok(RequestOutput {
main_struct,
nested_structs,
inline_types,
parameter_fields: all_fields,
warnings,
})
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct BodyInfo {
pub(crate) generated_types: Vec<RustType>,
pub(crate) type_usage: Vec<String>,
pub(crate) field_name: Option<FieldNameToken>,
pub(crate) body_type: Option<TypeRef>,
pub(crate) description: Option<String>,
pub(crate) optional: bool,
pub(crate) content_category: ContentCategory,
pub(crate) multipart_fields: Option<Vec<MultipartFieldInfo>>,
}
impl BodyInfo {
pub(crate) fn new(context: &Rc<ConverterContext>, entry: &OperationEntry) -> anyhow::Result<Self> {
let spec = context.graph().spec();
let Some(body_ref) = entry.operation.request_body.as_ref() else {
return Ok(Self::empty(true));
};
let body = body_ref.resolve(spec)?;
let is_required = body.required.unwrap_or(false);
let Some((content_type, media_type)) = body.content.iter().next() else {
return Ok(Self::empty(!is_required));
};
let Some(schema_ref) = media_type.schema.as_ref() else {
return Ok(Self::empty(!is_required));
};
let inline_resolver = InlineTypeResolver::new(context.clone());
let (generated_types, type_name) = match schema_ref {
ObjectOrReference::Ref { ref_path, .. } => {
let Some(name) = SchemaRegistry::parse_ref(ref_path) else {
return Ok(Self::empty(!is_required));
};
(vec![], to_rust_type_name(&name))
}
ObjectOrReference::Object(schema) => {
let base_name = schema.infer_name_from_context(&entry.path, REQUEST_BODY_SUFFIX);
let Some(output) = inline_resolver.try_inline_schema(schema, &base_name)? else {
return Ok(Self::empty(!is_required));
};
(output.inline_types, output.result)
}
};
let body_type = TypeRef::new(&type_name);
let content_category = ContentCategory::from_content_type(content_type);
let multipart_fields = Self::resolve_multipart_fields(content_category, &body_type, &generated_types);
Ok(Self {
generated_types,
type_usage: vec![type_name],
field_name: Some(FieldNameToken::new(BODY_FIELD_NAME)),
body_type: Some(body_type),
description: body.description.clone(),
optional: !is_required,
content_category,
multipart_fields,
})
}
fn resolve_multipart_fields(
category: ContentCategory,
body_type: &TypeRef,
generated_types: &[RustType],
) -> Option<Vec<MultipartFieldInfo>> {
if category != ContentCategory::Multipart {
return None;
}
let body_type_name = body_type.unboxed_base_type_name();
let struct_def = generated_types.iter().find_map(|t| {
if let RustType::Struct(def) = t
&& def.name.as_str() == body_type_name
{
return Some(def);
}
None
})?;
let fields = struct_def
.fields
.iter()
.map(|f| MultipartFieldInfo {
name: f.name.clone(),
nullable: f.rust_type.nullable,
is_bytes: matches!(f.rust_type.base_type, RustPrimitive::Bytes),
requires_json: f.rust_type.requires_json_serialization(),
})
.collect();
Some(fields)
}
pub(crate) fn create_field(&self) -> Option<FieldDef> {
let type_ref = self.body_type.clone()?;
Some(FieldDef::body_field(
BODY_FIELD_NAME,
self.description.as_ref(),
type_ref,
self.optional,
))
}
pub(crate) fn to_operation_body(&self) -> Option<OperationBody> {
let field_name = self.field_name.as_ref()?;
Some(
OperationBody::builder()
.field_name(field_name.clone())
.maybe_body_type(self.body_type.clone())
.optional(self.optional)
.content_category(self.content_category)
.maybe_multipart_fields(self.multipart_fields.clone())
.build(),
)
}
fn empty(optional: bool) -> Self {
Self {
optional,
..Default::default()
}
}
}