@@ -11,7 +11,7 @@ use serde_json::Value;
1111use serde_repr:: { Deserialize_repr , Serialize_repr } ;
1212use url:: Url ;
1313
14- use std:: { collections:: HashMap , time:: Duration } ;
14+ use std:: { collections:: HashMap , path :: PathBuf , time:: Duration } ;
1515
1616#[ cfg( feature = "reqwest-client" ) ]
1717pub use reqwest:: header;
@@ -114,7 +114,7 @@ impl Client {
114114 request_builder = request_builder. params ( & query) ;
115115 }
116116
117- if let Some ( headers) = request. headers {
117+ if let Some ( headers) = & request. headers {
118118 for ( name, value) in headers. 0 . iter ( ) {
119119 request_builder = request_builder. header ( name, value) ;
120120 }
@@ -130,14 +130,69 @@ impl Client {
130130 Body :: Text ( text) => request_builder. body ( attohttpc:: body:: Bytes ( text) ) . send ( ) ?,
131131 Body :: Json ( json) => request_builder. json ( & json) ?. send ( ) ?,
132132 Body :: Form ( form_body) => {
133- let mut form = Vec :: new ( ) ;
134- for ( name, part) in form_body. 0 {
135- match part {
136- FormPart :: Bytes ( bytes) => form. push ( ( name, serde_json:: to_string ( & bytes) ?) ) ,
137- FormPart :: Text ( text) => form. push ( ( name, text) ) ,
133+ #[ allow( unused_variables) ]
134+ fn send_form (
135+ request_builder : attohttpc:: RequestBuilder ,
136+ headers : & Option < HeaderMap > ,
137+ form_body : FormBody ,
138+ ) -> crate :: api:: Result < attohttpc:: Response > {
139+ #[ cfg( feature = "http-multipart" ) ]
140+ if matches ! (
141+ headers
142+ . as_ref( )
143+ . and_then( |h| h. 0 . get( "content-type" ) )
144+ . map( |v| v. as_bytes( ) ) ,
145+ Some ( b"multipart/form-data" )
146+ ) {
147+ let mut multipart = attohttpc:: MultipartBuilder :: new ( ) ;
148+ let mut byte_cache: HashMap < String , Vec < u8 > > = Default :: default ( ) ;
149+
150+ for ( name, part) in & form_body. 0 {
151+ if let FormPart :: File { file, .. } = part {
152+ byte_cache. insert ( name. to_string ( ) , file. clone ( ) . try_into ( ) ?) ;
153+ }
154+ }
155+ for ( name, part) in & form_body. 0 {
156+ multipart = match part {
157+ FormPart :: File {
158+ file,
159+ mime,
160+ file_name,
161+ } => {
162+ // safe to unwrap: always set by previous loop
163+ let mut file =
164+ attohttpc:: MultipartFile :: new ( name, byte_cache. get ( name) . unwrap ( ) ) ;
165+ if let Some ( mime) = mime {
166+ file = file. with_type ( mime) ?;
167+ }
168+ if let Some ( file_name) = file_name {
169+ file = file. with_filename ( file_name) ;
170+ }
171+ multipart. with_file ( file)
172+ }
173+ FormPart :: Text ( value) => multipart. with_text ( name, value) ,
174+ } ;
175+ }
176+ return request_builder
177+ . body ( multipart. build ( ) ?)
178+ . send ( )
179+ . map_err ( Into :: into) ;
180+ }
181+
182+ let mut form = Vec :: new ( ) ;
183+ for ( name, part) in form_body. 0 {
184+ match part {
185+ FormPart :: File { file, .. } => {
186+ let bytes: Vec < u8 > = file. try_into ( ) ?;
187+ form. push ( ( name, serde_json:: to_string ( & bytes) ?) )
188+ }
189+ FormPart :: Text ( value) => form. push ( ( name, value) ) ,
190+ }
138191 }
192+ request_builder. form ( & form) ?. send ( ) . map_err ( Into :: into)
139193 }
140- request_builder. form ( & form) ?. send ( ) ?
194+
195+ send_form ( request_builder, & request. headers , form_body) ?
141196 }
142197 }
143198 } else {
@@ -176,14 +231,61 @@ impl Client {
176231 Body :: Text ( text) => request_builder. body ( bytes:: Bytes :: from ( text) ) ,
177232 Body :: Json ( json) => request_builder. json ( & json) ,
178233 Body :: Form ( form_body) => {
179- let mut form = Vec :: new ( ) ;
180- for ( name, part) in form_body. 0 {
181- match part {
182- FormPart :: Bytes ( bytes) => form. push ( ( name, serde_json:: to_string ( & bytes) ?) ) ,
183- FormPart :: Text ( text) => form. push ( ( name, text) ) ,
234+ #[ allow( unused_variables) ]
235+ fn send_form (
236+ request_builder : reqwest:: RequestBuilder ,
237+ headers : & Option < HeaderMap > ,
238+ form_body : FormBody ,
239+ ) -> crate :: api:: Result < reqwest:: RequestBuilder > {
240+ #[ cfg( feature = "http-multipart" ) ]
241+ if matches ! (
242+ headers
243+ . as_ref( )
244+ . and_then( |h| h. 0 . get( "content-type" ) )
245+ . map( |v| v. as_bytes( ) ) ,
246+ Some ( b"multipart/form-data" )
247+ ) {
248+ let mut multipart = reqwest:: multipart:: Form :: new ( ) ;
249+
250+ for ( name, part) in form_body. 0 {
251+ let part = match part {
252+ FormPart :: File {
253+ file,
254+ mime,
255+ file_name,
256+ } => {
257+ let bytes: Vec < u8 > = file. try_into ( ) ?;
258+ let mut part = reqwest:: multipart:: Part :: bytes ( bytes) ;
259+ if let Some ( mime) = mime {
260+ part = part. mime_str ( & mime) ?;
261+ }
262+ if let Some ( file_name) = file_name {
263+ part = part. file_name ( file_name) ;
264+ }
265+ part
266+ }
267+ FormPart :: Text ( value) => reqwest:: multipart:: Part :: text ( value) ,
268+ } ;
269+
270+ multipart = multipart. part ( name, part) ;
271+ }
272+
273+ return Ok ( request_builder. multipart ( multipart) ) ;
184274 }
275+
276+ let mut form = Vec :: new ( ) ;
277+ for ( name, part) in form_body. 0 {
278+ match part {
279+ FormPart :: File { file, .. } => {
280+ let bytes: Vec < u8 > = file. try_into ( ) ?;
281+ form. push ( ( name, serde_json:: to_string ( & bytes) ?) )
282+ }
283+ FormPart :: Text ( value) => form. push ( ( name, value) ) ,
284+ }
285+ }
286+ Ok ( request_builder. form ( & form) )
185287 }
186- request_builder . form ( & form )
288+ send_form ( request_builder , & request . headers , form_body ) ?
187289 }
188290 } ;
189291 }
@@ -216,20 +318,52 @@ pub enum ResponseType {
216318 Binary ,
217319}
218320
321+ /// A file path or contents.
322+ #[ derive( Debug , Clone , Deserialize ) ]
323+ #[ serde( untagged) ]
324+ #[ non_exhaustive]
325+ pub enum FilePart {
326+ /// File path.
327+ Path ( PathBuf ) ,
328+ /// File contents.
329+ Contents ( Vec < u8 > ) ,
330+ }
331+
332+ impl TryFrom < FilePart > for Vec < u8 > {
333+ type Error = crate :: api:: Error ;
334+ fn try_from ( file : FilePart ) -> crate :: api:: Result < Self > {
335+ let bytes = match file {
336+ FilePart :: Path ( path) => std:: fs:: read ( & path) ?,
337+ FilePart :: Contents ( bytes) => bytes,
338+ } ;
339+ Ok ( bytes)
340+ }
341+ }
342+
219343/// [`FormBody`] data types.
220344#[ derive( Debug , Deserialize ) ]
221345#[ serde( untagged) ]
222346#[ non_exhaustive]
223347pub enum FormPart {
224348 /// A string value.
225349 Text ( String ) ,
226- /// A byte array value.
227- Bytes ( Vec < u8 > ) ,
350+ /// A file value.
351+ #[ serde( rename_all = "camelCase" ) ]
352+ File {
353+ /// File path or content.
354+ file : FilePart ,
355+ /// Mime type of this part.
356+ /// Only used when the `Content-Type` header is set to `multipart/form-data`.
357+ mime : Option < String > ,
358+ /// File name.
359+ /// Only used when the `Content-Type` header is set to `multipart/form-data`.
360+ file_name : Option < String > ,
361+ } ,
228362}
229363
230364/// Form body definition.
231365#[ derive( Debug , Deserialize ) ]
232- pub struct FormBody ( HashMap < String , FormPart > ) ;
366+ pub struct FormBody ( pub ( crate ) HashMap < String , FormPart > ) ;
233367
234368impl FormBody {
235369 /// Creates a new form body.
@@ -243,7 +377,7 @@ impl FormBody {
243377#[ serde( tag = "type" , content = "payload" ) ]
244378#[ non_exhaustive]
245379pub enum Body {
246- /// A multipart formdata body.
380+ /// A form body.
247381 Form ( FormBody ) ,
248382 /// A JSON body.
249383 Json ( Value ) ,
0 commit comments