Skip to content

Commit ab478f1

Browse files
committed
feat(codegen): generate builder APIs for multipart/related
Implement code generation for multipart/related request bodies with expanded builder patterns. Instead of a single body parameter, multipart schemas are expanded into individual builder methods for each property (e.g., .file() and .metadata() methods). Key changes: - Parse multipart/related schemas and expand into typed properties - Convert binary schema properties (format: binary) to Vec<u8> - Generate MultipartRelatedBody trait implementations - Reconstruct multipart structs in builder send() methods - Binary fields serialized as application/octet-stream - JSON fields serialized with serde_json Addresses #1240
1 parent b3779e8 commit ab478f1

22 files changed

+4103
-2577
lines changed

progenitor-impl/src/cli.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,8 @@ impl Generator {
440440
// are currently...
441441
OperationParameterType::RawBody => None,
442442

443-
OperationParameterType::Type(body_type_id) => Some(body_type_id),
443+
OperationParameterType::Type(body_type_id)
444+
| OperationParameterType::MultipartRelated(body_type_id) => Some(body_type_id),
444445
});
445446

446447
if let Some(body_type_id) = maybe_body_type_id {

progenitor-impl/src/httpmock.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,14 @@ impl Generator {
161161
.get_type(arg_type_id)
162162
.unwrap()
163163
.parameter_ident(),
164+
OperationParameterType::MultipartRelated(arg_type_id) => self
165+
.type_space
166+
.get_type(arg_type_id)
167+
.unwrap()
168+
.parameter_ident(),
164169
OperationParameterType::RawBody => match kind {
165-
OperationParameterKind::Body(BodyContentType::OctetStream) => quote! {
170+
OperationParameterKind::Body(BodyContentType::OctetStream)
171+
| OperationParameterKind::Body(BodyContentType::MultipartRelated) => quote! {
166172
::serde_json::Value
167173
},
168174
OperationParameterKind::Body(BodyContentType::Text(_)) => quote! {
@@ -250,8 +256,14 @@ impl Generator {
250256

251257
},
252258
),
259+
OperationParameterType::MultipartRelated(_) => (
260+
true,
261+
quote! {
262+
Self(self.0.json_body_obj(value))
263+
},
264+
),
253265
OperationParameterType::RawBody => match body_content_type {
254-
BodyContentType::OctetStream => (
266+
BodyContentType::OctetStream | BodyContentType::MultipartRelated => (
255267
true,
256268
quote! {
257269
Self(self.0.json_body(value))

progenitor-impl/src/lib.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use proc_macro2::TokenStream;
1111
use quote::quote;
1212
use serde::Deserialize;
1313
use thiserror::Error;
14-
use typify::{TypeSpace, TypeSpaceSettings};
14+
use typify::{TypeId, TypeSpace, TypeSpaceSettings};
1515

1616
use crate::to_schema::ToSchema;
1717

@@ -50,6 +50,7 @@ pub type Result<T> = std::result::Result<T, Error>;
5050
/// OpenAPI generator.
5151
pub struct Generator {
5252
type_space: TypeSpace,
53+
multipart_related: indexmap::IndexSet<TypeId>,
5354
settings: GenerationSettings,
5455
uses_futures: bool,
5556
uses_websockets: bool,
@@ -261,6 +262,7 @@ impl Default for Generator {
261262
fn default() -> Self {
262263
Self {
263264
type_space: TypeSpace::new(TypeSpaceSettings::default().with_type_mod("types")),
265+
multipart_related: Default::default(),
264266
settings: Default::default(),
265267
uses_futures: Default::default(),
266268
uses_websockets: Default::default(),
@@ -312,6 +314,7 @@ impl Generator {
312314

313315
Self {
314316
type_space: TypeSpace::new(&type_settings),
317+
multipart_related: Default::default(),
315318
settings: settings.clone(),
316319
uses_futures: false,
317320
uses_websockets: false,
@@ -374,6 +377,71 @@ impl Generator {
374377

375378
let types = self.type_space.to_stream();
376379

380+
// Generate MultipartRelatedBody trait impl for multipart/related types
381+
let multipart_helpers = TokenStream::from_iter(
382+
self.multipart_related
383+
.iter()
384+
.map(|type_id| {
385+
let typ = self.get_type_space().get_type(type_id).unwrap();
386+
let type_name = typ.ident();
387+
388+
let td = typ.details();
389+
let typify::TypeDetails::Struct(tstru) = td else {
390+
panic!("multipart/related type must be a struct");
391+
};
392+
393+
// Generate code to extract each property
394+
let parts_extraction = tstru.properties().map(|(prop_name, prop_id)| {
395+
let prop_ty = self.get_type_space().get_type(&prop_id).ok();
396+
let field_ident = quote::format_ident!("{}", prop_name);
397+
398+
// Check if this is a binary field - if it's Vec<u8>, it's binary
399+
let is_binary = prop_ty.map(|t| {
400+
// Check if this is a Vec type with u8 elements
401+
if let typify::TypeDetails::Vec(inner_id) = t.details() {
402+
// Check if the inner type is u8 by looking at its ident
403+
if let Ok(inner_ty) = self.get_type_space().get_type(&inner_id) {
404+
inner_ty.ident().to_string() == "u8"
405+
} else {
406+
false
407+
}
408+
} else {
409+
false
410+
}
411+
}).unwrap_or(false);
412+
413+
if is_binary {
414+
quote! {
415+
progenitor_client::MultipartPart {
416+
content_type: "application/octet-stream",
417+
content_id: #prop_name,
418+
bytes: self.#field_ident.clone(),
419+
}
420+
}
421+
} else {
422+
quote! {
423+
progenitor_client::MultipartPart {
424+
content_type: "application/json",
425+
content_id: #prop_name,
426+
bytes: ::serde_json::to_vec(&self.#field_ident)
427+
.expect("failed to serialize field"),
428+
}
429+
}
430+
}
431+
}).collect::<Vec<_>>();
432+
433+
quote! {
434+
impl progenitor_client::MultipartRelatedBody for #type_name {
435+
fn as_multipart_parts(&self) -> Vec<progenitor_client::MultipartPart> {
436+
vec![
437+
#(#parts_extraction),*
438+
]
439+
}
440+
}
441+
}
442+
}),
443+
);
444+
377445
let (inner_type, inner_fn_value) = match self.settings.inner_type.as_ref() {
378446
Some(inner_type) => (inner_type.clone(), quote! { &self.inner }),
379447
None => (quote! { () }, quote! { &() }),
@@ -440,6 +508,8 @@ impl Generator {
440508
#[allow(clippy::all)]
441509
pub mod types {
442510
#types
511+
512+
#multipart_helpers
443513
}
444514

445515
#[derive(Clone, Debug)]

0 commit comments

Comments
 (0)