-
Notifications
You must be signed in to change notification settings - Fork 72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
The others should not be changed other than updating them from their source repository. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revert?
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't this make serde_json
into a mandatory dependency for generated crates?
@@ -308,15 +337,24 @@ impl Generator { | |||
}; | |||
|
|||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're trying to exclude these so we don't generate types that aren't used?
validate_openapi(spec)?; | ||
|
||
// collect all component names in request body media references | ||
// that are only used for multipart/form-data operations. | ||
let media_conents = spec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
.iter() | ||
.flat_map(|(_path, ref_or_item)| { | ||
// Exclude externally defined path items. | ||
let item = ref_or_item.as_item().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this valid?
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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::Part
parameter. This has the following advantages:serde::Serialize
which they couldn't if aPart
was made from an async streamThe disadvantage is leaking a
reqwest
type. However, we could probably wrap and impleInto
for an internal type if required.An internal media heavy API compiled well. I'm looking for guidance and tips. Todos:
typify
binary formatted strings