Support multipart/form-data and file uploads#609
Conversation
* own OperationParamaterType * delare replaced PartMeta
Todo: * some work on cli * some work on httpmock * generate form in method body
except: create form from body
This reverts commit 9d8a789.
adds: use reqwest::multipart::Part;
adds super::Part import
approach has two flaws: * form params optional cannot be recovered from typify or ergonomically from schema * fails for bodies with nested compound types, e.g. Map, Vec
the main benefit is correct treatment of optionals and a baseline support for nested compound parameters
|
I didn't take a close look at this as it appears to still be a work in progress, but I did want to say:
|
curl -L -o sample_openapi/openai-openapi.yaml https://raw.githubusercontent.com/openai/openai-openapi/84e9aa615dcb81c66d2d63b8d5dad025259223b2/openapi.yaml
* removed endpoints not tagged with Images * removed x-oaiMeta key * inlined end_user_param_configuration still gen fail: TypeError(InvalidValue)
…o feature/form_data
reference types are emitted in modified and and modified versions though
Check out For an integrated example see: https://github.com/ahirner/openai-progenitor
The outputs are fully functional AFAIC. My last gripe is that progenitor/progenitor-impl/src/lib.rs Line 223 in a6bc062 LMK what you think! |
, thus no additional todo here
ReferenceOrExt already does everything we need, amazing
.. because a cloned and pruned body type is created separately. We probalby want to get rid of redundant body structs iff. they are referenced in multipart/form-data contents only
…y referenced in such requests
|
PR is complete and all comments so far adressed. |
Sounds good. I'll try to take a first pass this week. |
|
Hi @ahl, is there maybe something I can do in the meantime? |
ahl
left a comment
There was a problem hiding this comment.
This is not exhaustive, but what I could get through. I'd encourage you to generalize from these comments and go through the PR yourself again. Thank you for the submission and my apologies for the delay reviewing it.
| Note that keeper, buildomat and openai-openapi.yaml have diverged in order to validate support for various features. | ||
| The others should not be changed other than updating them from their source repository. |
| /// Binary or text parts of a multipart/form-data body. | ||
| /// The bool signifies if it is required. | ||
| // TODO any other body content ought to become optional, | ||
| // after which flag would be redundant | ||
| // (see comment in OperationParameterKind) | ||
| FormData(bool), |
There was a problem hiding this comment.
| /// Binary or text parts of a multipart/form-data body. | |
| /// The bool signifies if it is required. | |
| // TODO any other body content ought to become optional, | |
| // after which flag would be redundant | |
| // (see comment in OperationParameterKind) | |
| FormData(bool), | |
| /// Binary or text parts of a multipart/form-data body. | |
| FormData, |
It doesn't appear that this is needed for the test cases provided and if we're going to support optional bodies, let's do it properly in OpationalParameterKind
| if let OperationParameterKind::Body( | ||
| BodyContentType::FormData(required), | ||
| ) = param.kind |
There was a problem hiding this comment.
maybe if let .. else { unreachable!() };
|
|
||
| pub fn generate_tokens(&mut self, spec: &OpenAPI) -> Result<TokenStream> { | ||
| fn add_ref_types(&mut self, spec: &OpenAPI) -> Result<()> { | ||
| validate_openapi(spec)?; |
There was a problem hiding this comment.
I don't think this has to do with what I infer to be the purpose of this function.
| // Take unquoted String Value | ||
| Some(v) => Ok(v.to_string()), | ||
| // otherwise convert to quoted json | ||
| None => serde_json::to_string(value).map_err(|_| { |
There was a problem hiding this comment.
doesn't this make serde_json into a mandatory dependency for generated crates?
| let version_str = &spec.info.version; | ||
| let (to_form_string, pub_part) = if self.uses_form_parts { | ||
| ( | ||
| Some(quote! { ,to_form_string }), |
There was a problem hiding this comment.
use it unconditionally (note #[allow(unused_imports)])
| // collect all component names in request body media references | ||
| // that are only used for multipart/form-data operations. | ||
| let media_conents = spec | ||
| .paths | ||
| .iter() | ||
| .flat_map(|(_path, ref_or_item)| { | ||
| // Exclude externally defined path items. | ||
| let item = ref_or_item.as_item().unwrap(); | ||
| item.iter().map(move |(_method, operation)| operation) | ||
| }) | ||
| .flat_map(|operation| operation.request_body.iter()) | ||
| .filter_map(|b| b.as_item().map(|b| &b.content)) | ||
| .flat_map(|body_content| body_content.iter()); |
There was a problem hiding this comment.
we're trying to exclude these so we don't generate types that aren't used?
|
|
||
| // collect all component names in request body media references | ||
| // that are only used for multipart/form-data operations. | ||
| let media_conents = spec |
| .iter() | ||
| .flat_map(|(_path, ref_or_item)| { | ||
| // Exclude externally defined path items. | ||
| let item = ref_or_item.as_item().unwrap(); |
| /// Return all component names where schema references are keyed | ||
| /// by multipart/form-data but nowhere else. The last part of a media | ||
| /// schema's json path reference is considered the component name. | ||
| fn get_exclusive_formdata_refs<'a>( |
There was a problem hiding this comment.
these checks seem insufficient to validate if referenced types are used elsewhere e.g. what if the type is used as a body parameter or response parameter. I may be misunderstanding what's going on here, but I don't think we should do any of this.
There was a problem hiding this comment.
what if the type is used as a body parameter or response parameter
That's what this function should find out. Alas, it's only partially validated in tests, not provably.
what's going on here
It's to avoid that multipart/form-data body structs are emitted twice. Once with file fields factored out and once from the original ref.
should do any of this
I think so too, but haven't found a cooler way. My initial idea was way to inline types earlier, but I think the current typify API didn't let me. Maybe I'll find a better approach still.
There was a problem hiding this comment.
I would suggest we just not do anything clever here: generating orphaned types is fine--we already do it!
There was a problem hiding this comment.
Ok... good to know! Then this PR might come to a conclusion.
It's only that without f8baafa, the number of orphaned types was very, very large for our internal uses. This simply went against my sense of aesthetics. Technically, I don't think it added compile times really.
There was a problem hiding this comment.
there's probably a bigger, more holistic look one might take: start from all operations, see the types they touch. We could have special handling for these multi-part types (i.e. use in those cases wouldn't count as real use)
There was a problem hiding this comment.
Yes. OTOH, the more I thought about that, the larger the PR grew in my mind as well ;)
Intuitively, every operation could probably inferred from a generalized query into a graph of schema units.
Fixes #518 and #402.
This approach factors out binary form data as
reqwest::multipart::Partparameter. This has the following advantages:serde::Serializewhich they couldn't if aPartwas made from an async streamThe disadvantage is leaking a
reqwesttype. However, we could probably wrap and impleIntofor an internal type if required.An internal media heavy API compiled well. I'm looking for guidance and tips. Todos:
typifybinary formatted strings