-
Notifications
You must be signed in to change notification settings - Fork 69
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
impl "multipart/form-data" support #418
base: main
Are you sure you want to change the base?
Conversation
00b610a
to
7979fe2
Compare
@ahl if you have a few minutes to spare, it'd be great to get some feedback, in particular on:
|
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.
Thanks very much for submitting this. A great start. I left some scattered comments at varying levels of detail. The highest order bit comment I offer is to please add test cases--at a minimum modifying one of the openapi documents to include a body parameter of this type.
@ahl if you have a few minutes to spare, it'd be great to get some feedback, in particular on:
is Generator the intended place for additional metadata?
I mean... it can, but in the case of forms
my inclination is to inline that code in the body
method.
is the boundary between parsing and generation sufficiently upheld in your opinion?
I think so modulo the prior comment.
do you expect to panic on failed assumptions - currently the generation uses a filter iterator modifier and hence does not panic.
I mean... sure? It depends? I think the real answer is "yes" except when it would screw up currently working users in unexpected ways... which is hard to say.
progenitor-impl/Cargo.toml
Outdated
@@ -11,6 +11,7 @@ readme = "../README.md" | |||
heck = "0.4.1" | |||
getopts = "0.2" | |||
indexmap = "1.9" | |||
itertools = "0.10" |
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.
is this used?
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.
Woud you mind if I add a deny
rule?
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.
Addressed in 4323c11
progenitor-impl/src/method.rs
Outdated
@@ -328,7 +345,7 @@ impl Generator { | |||
|
|||
let typ = self | |||
.type_space | |||
.add_type_with_name(&schema, Some(name))?; | |||
.add_type_with_name(&schema, Some(dbg!(name)))?; |
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.
remove before merging?
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 really thought I fixed that..
let url_renames = | ||
HashMap::from_iter(method.params.iter().filter_map(|param| { | ||
match ¶m.kind { | ||
OperationParameterKind::Path => { | ||
Some((¶m.api_name, ¶m.name)) | ||
} | ||
_ => None, | ||
} | ||
_ => None, | ||
}) | ||
.collect(); | ||
})); |
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.
spurious?
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.
Somewhat yes, collect
elides what the elemets are being collected into which I personally find a lot easier to reason about code. Will remove.
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.
Thanks.
fn form_from_raw< | ||
S: AsRef<str>, | ||
T: AsRef<[u8]>, | ||
I: Sized + IntoIterator<Item = (S, T)>, |
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 IntoIterator
?
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.
Because a form can contain multiple streams. That's the least limiting for an Impl and sufficient for the R..BuilderExt
trait. But with the comments below this might become obsolete
.map(|prop_typ| (prop_name, prop_typ)) | ||
}) | ||
); | ||
let properties = syn::punctuated::Punctuated::<_, syn::Token![,]>::from_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.
let properties = syn::punctuated::Punctuated::<_, syn::Token![,]>::from_iter( | |
let properties = |
#(#properties,)* below
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.
The above creates a punctuated list, which can both be used with quote to create the correct expansion, while also being iterable, can change to Vec<TokenStream>
and use quote!{ #(#propeties),*
if desired.
progenitor-impl/src/lib.rs
Outdated
pub fn as_form<'f>(&'f self) -> impl std::iter::Iterator<Item=(&'static str, &'f [u8])> { | ||
[#properties] | ||
.into_iter() | ||
.filter_map(|(name, val)| val.as_ref().map(|val| (name, val.as_slice()))) |
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.
Does this mean that all val
have to be strings?
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.
No, not at all. It means all values have to be convertible to a stream of bytes. The representation is limiting and should be improved, i.e. it only allows for basic usage key
but not for keys derived from objects where the keys end up as foo[bar].sub
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 see. Are there types we might encounter here other than String
and Vec[u8]
that satisfy 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.
Any type really, such that we could call as_form
for nested types. That was the direction I wanted to go into. Hence avoiding the direct expansion. There are a few pitfalls, i.e. metadata desc such as mime type and filename that are commonly expected but not defined in the openapi spec. I am not yet sure how to handle 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.
Note that the PR here is in a state of rough draft/idea/make my immediate needs meet, so I am open to changing direction as you see fit.
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.
Gentle ping on this one.
) => { | ||
Some(quote! { | ||
// This uses progenitor_client::RequestBuilderExt which sets up a simple form data based on bytes | ||
.form_from_raw(body.as_form())? |
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.
did you think about unrolled the as_form
logic here? That seems potentially cleaner than an impl
on the generated type.
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 try to avoid writing to much code with quote
if it can be formalized as a simple trait implementation without many constraints.
Hey team, Do you think the above has the potential to be merged. If not, is there anything I can do to complete the PR so we can merge it ? |
@PlamenHristov I think you'd be welcome to make a fork of this work, and resolve conflicts. As I recall this was pretty close. I don't see any tests and that would be another good addition to this work. |
Happy to pick this up again, the silence on my last set of comments and the scarcity of time made me question if it's worth continuing. If @ahl believes it's close to merging, I am happy to push it over the finish line. @PlamenHristov if you want to help, adding test cases is probably the most effective way |
@drahnr my apologies for discouraging you. I appreciate the contribution and suffer from the same endemic time scarcity! |
@ahl |
# [patch."https://github.com/oxidecomputer/typify"] | ||
# typify = { path = "../typify/typify" } | ||
|
||
#[patch.crates-io] | ||
# [patch.crates-io] | ||
#serde_tokenstream = { path = "../serde_tokenstream" } | ||
#typify = { path = "../typify/typify" } | ||
# typify = { path = "../typify/typify" } |
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?
let url_renames = | ||
HashMap::from_iter(method.params.iter().filter_map(|param| { | ||
match ¶m.kind { | ||
OperationParameterKind::Path => { | ||
Some((¶m.api_name, ¶m.name)) | ||
} | ||
_ => None, | ||
} | ||
_ => None, | ||
}) | ||
.collect(); | ||
})); |
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.
Thanks.
// "format": "binary" | ||
// } | ||
|
||
let _mapped = match schema.item(components)? { |
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 ignoring the value we create?
}; | ||
|
||
println!("cargo::rerun-if-changed={}", path_str); |
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's going on here?
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.
Very much looking forward to this PR being completed!
While testing the changes myself, I encountered two issues.
pub fn as_form<'f>(&'f self) -> impl std::iter::Iterator<Item=(&'static str, &'f [u8])> { | ||
[#properties] | ||
.into_iter() | ||
.filter_map(|(name, val)| val.as_ref().map(|val| (name, val.as_bytes()))) |
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.
When trying to compile I'm getting the following error (rust v1.78.0):
--> <redacted>/src/lib.rs:12324:47
|
12324 | .filter_map(|(name, val)| val.as_ref().map(|val| (name, val.as_bytes())))
| ^^^^^^ --- type must be known at this point
|
help: try using a fully qualified path to specify the expected types
|
12324 | .filter_map(|(name, val)| <std::string::String as AsRef<T>>::as_ref(val).map(|val| (name, val.as_bytes())))
| ++++++++++++++++++++++++++++++++++++++++++ ~
The specification I'm using includes a single property.
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 ran into this limitation a few weeks back, and the root cause here being no unified way between &str
, Vec<u8>
, and others to retrieve a byte slice. Adding a custom trait and implementing fixes that.
That said, It'll be September before I'll be able to work on this PR
openapiv3::SchemaKind::Type(openapiv3::Type::String( | ||
openapiv3::StringType { | ||
format: | ||
openapiv3::VariantOrUnknownOrEmpty::Item( | ||
openapiv3::StringFormat::Binary, |
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.
Are you planning to get rid of this limitation as part of this PR? My use-case requires support for boolean
s and string
s with format date-time
, uuid
or no specified format.
I reviewed your code, but I don't think I'd be able to add support for them without some guidance 🤔
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 think it's there be cause my specific use case only required binary.
Could you provide a minimal/trimmed down openapi spec where this'd fail?
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.
Sure, here's a trimmed down spec I'm using: example.json
This is based on the OpenAPI spec for https://github.com/immich-app/immich
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.
Another two observations found while testing the PR and fiddling with progenitor
and typify
.
.header( | ||
reqwest::header::CONTENT_TYPE, | ||
reqwest::header::HeaderValue::from_static( | ||
"multipart/form-data", | ||
), | ||
) |
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 redundant, the .multipart
call done below will add the header (including the boundary parameter).
let properties = indexmap::IndexMap::<&'_ str, _>::from_iter( | ||
tstru | ||
.properties() | ||
.filter_map(|(prop_name, prop_id)| { |
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.
The prop_name
seems not to be the right choice here 🤔 It's the name of the property from the Rust struct and not the name of the property from the OpenAPI spec.
In my case, the OpenAPI spec uses lowerCamelCase
for properties, whereas Rust uses snake_case
and the generated code uses the latter which makes the request invalid.
I went through progenitor
and typify
's source code and I believe that despite the original-cased information being available in StructProperty
, it's not then exposed via TypeStructPropInfo
. I made a fork that changes that (diff) and integrated into your PR (diff). Now generated as_form
has proper casing 👍
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'm not sure if this is the best approach, possibly one of the maintainers could suggest a better one.
Currently form data handling is not implemented.
This PR has an initial, dirty implementation on which I'll iterate over the next weeks.
Closes #402