feat(api): support typed binary success response bodies

Preserve byte-exact BinaryBody responses in generated Axum/Reqwest adapters, declared response headers, metadata and inline binary OpenAPI schemas. Request and JSON/error behavior stay unchanged. Validated all-feature api-macros/api-macros-impl suites including 5 binary response tests, existing HTTP adapters, OpenAPI and compile-fail contracts.
This commit is contained in:
2026-10-09 00:02:09 +09:00
parent 60ec815237
commit 9da79ab9f0
6 changed files with 403 additions and 7 deletions
+32 -4
View File
@@ -1534,6 +1534,13 @@ fn validate_named_body_type(ty: &Type, kind: &str) -> syn::Result<()> {
Ok(())
}
fn is_binary_body_type(ty: &Type) -> bool {
matches!(ty, Type::Path(path)
if path.qself.is_none()
&& path.path.segments.last().is_some_and(|last|
last.ident == "BinaryBody" && matches!(last.arguments, PathArguments::None)))
}
fn validate_binary_body_type(ty: &Type) -> syn::Result<()> {
let Type::Path(path) = ty else {
return Err(syn::Error::new_spanned(
@@ -1738,6 +1745,9 @@ fn reqwest_adapter_tokens(
}
});
let body_parser = match response.body.as_ref() {
Some(ty) if is_binary_body_type(ty) => quote! {
let body = __response.decode_binary();
},
Some(ty) => quote! {
let body = __response
.decode_json::<#ty>(#api_crate::reqwest::DecodeKind::Success)
@@ -1775,7 +1785,11 @@ fn reqwest_adapter_tokens(
.alternate_status
.map(|alternate| quote!(__actual == #response_status || __actual == #alternate))
.unwrap_or_else(|| quote!(__actual == #response_status));
let success = if let Some(response) = operation.response_body.as_ref() {
let success = if operation.response_body.as_ref().is_some_and(is_binary_body_type) {
quote! {
return ::core::result::Result::Ok(__response.decode_binary());
}
} else if let Some(response) = operation.response_body.as_ref() {
quote! {
return __response
.decode_json::<#response>(#api_crate::reqwest::DecodeKind::Success)
@@ -2133,7 +2147,9 @@ fn axum_adapter_tokens(
let variant_ident = format_ident!("Status{}", status);
let body_pattern = response.body.as_ref().map(|_| quote!(body,));
let header_fields = response.headers.iter().map(|header| &header.field_ident);
let base_response = if response.body.is_some() {
let base_response = if response.body.as_ref().is_some_and(is_binary_body_type) {
quote!(#api_crate::axum::binary_response(#api_crate::axum::status(#status), body))
} else if response.body.is_some() {
quote!(#api_crate::axum::json_response(#api_crate::axum::status(#status), body))
} else {
quote!(#api_crate::axum::empty_response(#api_crate::axum::status(#status)))
@@ -2170,14 +2186,21 @@ fn axum_adapter_tokens(
}
}
} else if let Some(alternate_status) = operation.alternate_status {
let response_encoder = if operation.response_body.as_ref().is_some_and(is_binary_body_type) {
quote!(#api_crate::axum::binary_response)
} else {
quote!(#api_crate::axum::json_response)
};
quote!({
let status = #api_crate::HttpSuccess::status_code(&value);
if status == #response_status || status == #alternate_status {
#api_crate::axum::json_response(#api_crate::axum::status(status), value)
#response_encoder(#api_crate::axum::status(status), value)
} else {
#api_crate::axum::empty_response(#api_crate::axum::status(500))
}
})
} else if operation.response_body.as_ref().is_some_and(is_binary_body_type) {
quote!(#api_crate::axum::binary_response(#api_crate::axum::status(#response_status), value))
} else if operation.response_body.is_some() {
quote!(#api_crate::axum::json_response(#api_crate::axum::status(#response_status), value))
} else {
@@ -2323,6 +2346,9 @@ fn openapi_adapter_tokens(
"Alternate successful response"
};
let body = match response.body.as_ref() {
Some(ty) if is_binary_body_type(ty) => quote! {
operation.binary_response(#status, #description)?;
},
Some(ty) => quote! {
operation.response::<#ty>(#status, "application/json", #description)?;
},
@@ -2620,7 +2646,9 @@ fn metadata_tokens(
};
let success_responses = operation.success_responses.iter().map(|response| {
let status = response.status;
let wire_kind = if response.body.is_some() {
let wire_kind = if response.body.as_ref().is_some_and(is_binary_body_type) {
quote!(#api_crate::WireKind::Binary)
} else if response.body.is_some() {
quote!(#api_crate::WireKind::Json)
} else {
quote!(#api_crate::WireKind::Empty)