11import 'cross-fetch/polyfill' /* global fetch */
22import qs from 'qs'
33import jsYaml from 'js-yaml'
4- import isString from 'lodash/isString '
4+ import pick from 'lodash/pick '
55import isFunction from 'lodash/isFunction'
6- import isNil from 'lodash/isNil'
76import FormData from './internal/form-data-monkey-patch'
7+ import { encodeDisallowedCharacters } from './execute/oas3/style-serializer'
8+
89
910// For testing
1011export const self = {
@@ -156,78 +157,169 @@ export function isFile(obj, navigatorObj) {
156157 }
157158 return false
158159 }
159- if ( typeof File !== 'undefined' ) {
160- // eslint-disable-next-line no-undef
161- return obj instanceof File
160+
161+ if ( typeof File !== 'undefined' && obj instanceof File ) { // eslint-disable-line no-undef
162+ return true
163+ }
164+ if ( typeof Blob !== 'undefined' && obj instanceof Blob ) { // eslint-disable-line no-undef
165+ return true
166+ }
167+ if ( typeof Buffer !== 'undefined' && obj instanceof Buffer ) {
168+ return true
162169 }
170+
163171 return obj !== null && typeof obj === 'object' && typeof obj . pipe === 'function'
164172}
165173
166- function formatValue ( input , skipEncoding ) {
167- const { collectionFormat, allowEmptyValue} = input
168- // `input` can be string in OAS3 contexts
169- const value = typeof input === 'object' ? input . value : input
170- const SEPARATORS = {
171- csv : ',' ,
172- ssv : '%20' ,
173- tsv : '%09' ,
174- pipes : '|'
175- }
174+ function isArrayOfFile ( obj , navigatorObj ) {
175+ return ( Array . isArray ( obj ) && obj . some ( v => isFile ( v , navigatorObj ) ) )
176+ }
177+
178+ const STYLE_SEPARATORS = {
179+ form : ',' ,
180+ spaceDelimited : '%20' ,
181+ pipeDelimited : '|'
182+ }
183+
184+ const SEPARATORS = {
185+ csv : ',' ,
186+ ssv : '%20' ,
187+ tsv : '%09' ,
188+ pipes : '|'
189+ }
190+
191+ // Formats a key-value and returns an array of key-value pairs.
192+ //
193+ // Return value example 1: [['color', 'blue']]
194+ // Return value example 2: [['color', 'blue,black,brown']]
195+ // Return value example 3: [['color', ['blue', 'black', 'brown']]]
196+ // Return value example 4: [['color', 'R,100,G,200,B,150']]
197+ // Return value example 5: [['R', '100'], ['G', '200'], ['B', '150']]
198+ // Return value example 6: [['color[R]', '100'], ['color[G]', '200'], ['color[B]', '150']]
199+ function formatKeyValue ( key , input , skipEncoding = false ) {
200+ const { collectionFormat, allowEmptyValue, serializationOption, encoding} = input
201+ // `input` can be string
202+ const value = ( typeof input === 'object' && ! Array . isArray ( input ) ) ? input . value : input
203+ const encodeFn = skipEncoding ? ( k => k . toString ( ) ) : ( k => encodeURIComponent ( k ) )
204+ const encodedKey = encodeFn ( key )
176205
177206 if ( typeof value === 'undefined' && allowEmptyValue ) {
178- return ''
207+ return [ [ encodedKey , '' ] ]
208+ }
209+
210+ // file
211+ if ( isFile ( value ) || isArrayOfFile ( value ) ) {
212+ return [ [ encodedKey , value ] ]
179213 }
180214
181- if ( isFile ( value ) || typeof value === 'boolean' ) {
182- return value
215+ // for OAS 3 Parameter Object for serialization
216+ if ( serializationOption ) {
217+ return formatKeyValueBySerializationOption ( key , value , skipEncoding , serializationOption )
183218 }
184219
185- let encodeFn = encodeURIComponent
186- // skipEncoding is an option to skip using the encodeURIComponent
187- // and allow reassignment to a different "encoding" function
188- // we should only use encodeURIComponent for known url strings
189- if ( skipEncoding ) {
190- if ( isString ( value ) || Array . isArray ( value ) ) {
191- encodeFn = str => str
220+ // for OAS 3 Encoding Object
221+ if ( encoding ) {
222+ if ( [ typeof encoding . style , typeof encoding . explode , typeof encoding . allowReserved ] . some ( type => type !== 'undefined' ) ) {
223+ return formatKeyValueBySerializationOption ( key , value , skipEncoding , pick ( encoding , [ 'style' , 'explode' , 'allowReserved' ] ) )
192224 }
193- else {
194- encodeFn = obj => JSON . stringify ( obj )
225+
226+ if ( encoding . contentType ) {
227+ if ( encoding . contentType === 'application/json' ) {
228+ // If value is a string, assume value is already a JSON string
229+ const json = typeof value === 'string' ? value : JSON . stringify ( value )
230+ return [ [ encodedKey , encodeFn ( json ) ] ]
231+ }
232+ return [ [ encodedKey , encodeFn ( value . toString ( ) ) ] ]
233+ }
234+
235+ // Primitive
236+ if ( typeof value !== 'object' ) {
237+ return [ [ encodedKey , encodeFn ( value ) ] ]
238+ }
239+
240+ // Array of primitives
241+ if ( Array . isArray ( value ) && value . every ( v => typeof v !== 'object' ) ) {
242+ return [ [ encodedKey , value . map ( encodeFn ) . join ( ',' ) ] ]
243+ }
244+
245+ // Array or object
246+ return [ [ encodedKey , encodeFn ( JSON . stringify ( value ) ) ] ]
247+ }
248+
249+ // for OAS 2 Parameter Object
250+ // Primitive
251+ if ( typeof value !== 'object' ) {
252+ return [ [ encodedKey , encodeFn ( value ) ] ]
253+ }
254+
255+ // Array
256+ if ( Array . isArray ( value ) ) {
257+ if ( collectionFormat === 'multi' ) {
258+ // In case of multipart/formdata, it is used as array.
259+ // Otherwise, the caller will convert it to a query by qs.stringify.
260+ return [ [ encodedKey , value . map ( encodeFn ) ] ]
195261 }
262+
263+ return [ [ encodedKey , value . map ( encodeFn ) . join ( SEPARATORS [ collectionFormat || 'csv' ] ) ] ]
196264 }
197265
198- if ( typeof value === 'object' && ! Array . isArray ( value ) ) {
199- return ''
266+ // Object
267+ return [ [ encodedKey , '' ] ]
268+ }
269+
270+ function formatKeyValueBySerializationOption ( key , value , skipEncoding , serializationOption ) {
271+ const style = serializationOption . style || 'form'
272+ const explode = typeof serializationOption . explode === 'undefined' ? style === 'form' : serializationOption . explode
273+ // eslint-disable-next-line no-nested-ternary
274+ const escape = skipEncoding ? false : ( serializationOption && serializationOption . allowReserved ? 'unsafe' : 'reserved' )
275+ const encodeFn = v => encodeDisallowedCharacters ( v , { escape} )
276+ const encodeKeyFn = skipEncoding ? ( k => k ) : ( k => encodeDisallowedCharacters ( k , { escape} ) )
277+
278+ // Primitive
279+ if ( typeof value !== 'object' ) {
280+ return [ [ encodeKeyFn ( key ) , encodeFn ( value ) ] ]
200281 }
201282
202- if ( ! Array . isArray ( value ) ) {
203- return encodeFn ( value )
283+ // Array
284+ if ( Array . isArray ( value ) ) {
285+ if ( explode ) {
286+ // In case of multipart/formdata, it is used as array.
287+ // Otherwise, the caller will convert it to a query by qs.stringify.
288+ return [ [ encodeKeyFn ( key ) , value . map ( encodeFn ) ] ]
289+ }
290+ return [ [ encodeKeyFn ( key ) , value . map ( encodeFn ) . join ( STYLE_SEPARATORS [ style ] ) ] ]
204291 }
205292
206- if ( Array . isArray ( value ) && ! collectionFormat ) {
207- return value . map ( encodeFn ) . join ( ',' )
293+ // Object
294+ if ( style === 'deepObject' ) {
295+ return Object . keys ( value ) . map ( valueKey => [ encodeKeyFn ( `${ key } [${ valueKey } ]` ) , encodeFn ( value [ valueKey ] ) ] )
208296 }
209- if ( collectionFormat === 'multi' ) {
210- // query case (not multipart/formdata)
211- return value . map ( encodeFn )
297+
298+ if ( explode ) {
299+ return Object . keys ( value ) . map ( valueKey => [ encodeKeyFn ( valueKey ) , encodeFn ( value [ valueKey ] ) ] )
212300 }
213- return value . map ( encodeFn ) . join ( SEPARATORS [ collectionFormat ] )
301+
302+ return [ [ encodeKeyFn ( key ) , Object . keys ( value ) . map ( valueKey => [ `${ encodeKeyFn ( valueKey ) } ,${ encodeFn ( value [ valueKey ] ) } ` ] ) . join ( ',' ) ] ]
214303}
215304
216305function buildFormData ( reqForm ) {
217306 /**
218307 * Build a new FormData instance, support array as field value
219- * OAS2.0 - via collectionFormat in spec definition
220- * OAS3.0 - via oas3BuildRequest, isOAS3formatArray flag
308+ * OAS2.0 - when collectionFormat is multi
309+ * OAS3.0 - when explode of Encoding Object is true
221310 * @param {Object } reqForm - ori req.form
222311 * @return {FormData } - new FormData instance
223312 */
224313 return Object . entries ( reqForm ) . reduce ( ( formData , [ name , input ] ) => {
225- if ( ( isNil ( input . collectionFormat ) || input . collectionFormat !== 'multi' ) && ! input . isOAS3formatArray ) {
226- formData . append ( name , formatValue ( input , true ) )
227- }
228- else {
229- input . value . forEach ( item =>
230- formData . append ( name , formatValue ( { ...input , value : item } , true ) ) )
314+ for ( const [ key , value ] of formatKeyValue ( name , input , true ) ) {
315+ if ( Array . isArray ( value ) ) {
316+ for ( const v of value ) {
317+ formData . append ( key , v )
318+ }
319+ }
320+ else {
321+ formData . append ( key , value )
322+ }
231323 }
232324 return formData
233325 } , new FormData ( ) )
@@ -242,14 +334,9 @@ export function encodeFormOrQuery(data) {
242334 * @return {object } encoded parameter names and values
243335 */
244336 const encodedQuery = Object . keys ( data ) . reduce ( ( result , parameterName ) => {
245- const isObject = a => a && typeof a === 'object'
246- const paramValue = data [ parameterName ]
247- const skipEncoding = ! ! paramValue . skipEncoding
248- const encodedParameterName = skipEncoding ? parameterName : encodeURIComponent ( parameterName )
249- const notArray = isObject ( paramValue ) && ! Array . isArray ( paramValue )
250- result [ encodedParameterName ] = formatValue (
251- notArray ? paramValue : { value : paramValue } , skipEncoding
252- )
337+ for ( const [ key , value ] of formatKeyValue ( parameterName , data [ parameterName ] ) ) {
338+ result [ key ] = value
339+ }
253340 return result
254341 } , { } )
255342 return qs . stringify ( encodedQuery , { encode : false , indices : false } ) || ''
@@ -266,7 +353,8 @@ export function mergeInQueryOrForm(req = {}) {
266353
267354 if ( form ) {
268355 const hasFile = Object . keys ( form ) . some ( ( key ) => {
269- return isFile ( form [ key ] . value )
356+ const value = form [ key ] . value
357+ return isFile ( value ) || isArrayOfFile ( value )
270358 } )
271359
272360 const contentType = req . headers [ 'content-type' ] || req . headers [ 'Content-Type' ]
0 commit comments