Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 5 additions & 40 deletions src/execute/oas3/build-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import assign from 'lodash/assign'
import get from 'lodash/get'
import btoa from 'btoa'
import {Buffer} from 'buffer/'

export default function (options, req) {
const {
Expand Down Expand Up @@ -50,49 +49,15 @@ export default function (options, req) {
if (requestBodyMediaTypes.indexOf(requestContentType) > -1) {
// only attach body if the requestBody has a definition for the
// contentType that has been explicitly set
if (requestContentType === 'application/x-www-form-urlencoded' || requestContentType.indexOf('multipart/') === 0) {
if (requestContentType === 'application/x-www-form-urlencoded' || requestContentType === 'multipart/form-data') {
if (typeof requestBody === 'object') {
const encoding = (requestBodyDef.content[requestContentType] || {}).encoding || {}

req.form = {}
Object.keys(requestBody).forEach((k) => {
const val = requestBody[k]
let newVal
let isFile
let isOAS3formatArray = false // oas3 query (default false) vs oas3 multipart

if (typeof File !== 'undefined') {
isFile = val instanceof File // eslint-disable-line no-undef
}

if (typeof Blob !== 'undefined') {
isFile = isFile || val instanceof Blob // eslint-disable-line no-undef
}

if (typeof Buffer !== 'undefined') {
isFile = isFile || Buffer.isBuffer(val)
}

if (typeof val === 'object' && !isFile) {
if (Array.isArray(val)) {
if (requestContentType === 'application/x-www-form-urlencoded') {
newVal = val.toString()
}
else {
// multipart case
newVal = val // keep as array
isOAS3formatArray = true
}
}
else {
newVal = JSON.stringify(val)
}
}
else {
newVal = val
}

req.form[k] = {
value: newVal,
isOAS3formatArray
value: requestBody[k],
encoding: encoding[k] || {},
}
})
}
Expand Down
56 changes: 4 additions & 52 deletions src/execute/oas3/parameter-builders.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pick from 'lodash/pick'
import stylize, {encodeDisallowedCharacters} from './style-serializer'
import serialize from './content-serializer'

Expand Down Expand Up @@ -46,58 +47,9 @@ export function query({req, value, parameter}) {
}

if (value) {
const type = typeof value

if (parameter.style === 'deepObject') {
const valueKeys = Object.keys(value)
valueKeys.forEach((k) => {
const v = value[k]
req.query[`${parameter.name}[${k}]`] = {
value: stylize({
key: k,
value: v,
style: 'deepObject',
escape: parameter.allowReserved ? 'unsafe' : 'reserved',
}),
skipEncoding: true
}
})
}
else if (
type === 'object' &&
!Array.isArray(value) &&
(parameter.style === 'form' || !parameter.style) &&
(parameter.explode || parameter.explode === undefined)
) {
// form explode needs to be handled here,
// since we aren't assigning to `req.query[parameter.name]`
// like we usually do.
const valueKeys = Object.keys(value)
valueKeys.forEach((k) => {
const v = value[k]
req.query[k] = {
value: stylize({
key: k,
value: v,
style: parameter.style || 'form',
escape: parameter.allowReserved ? 'unsafe' : 'reserved',
}),
skipEncoding: true
}
})
}
else {
const encodedParamName = encodeURIComponent(parameter.name)
req.query[encodedParamName] = {
value: stylize({
key: encodedParamName,
value,
style: parameter.style || 'form',
explode: typeof parameter.explode === 'undefined' ? true : parameter.explode,
escape: parameter.allowReserved ? 'unsafe' : 'reserved',
}),
skipEncoding: true
}
req.query[parameter.name] = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since encoding is not required when sending using FormData, encoding has been changed to execute when executing mergeInQueryOrForm, as in the Open API v2.

value,
serializationOption: pick(parameter, ['style', 'explode', 'allowReserved'])
}
}
else if (parameter.allowEmptyValue && value !== undefined) {
Expand Down
196 changes: 142 additions & 54 deletions src/http.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'cross-fetch/polyfill' /* global fetch */
import qs from 'qs'
import jsYaml from 'js-yaml'
import isString from 'lodash/isString'
import pick from 'lodash/pick'
import isFunction from 'lodash/isFunction'
import isNil from 'lodash/isNil'
import FormData from './internal/form-data-monkey-patch'
import {encodeDisallowedCharacters} from './execute/oas3/style-serializer'


// For testing
export const self = {
Expand Down Expand Up @@ -156,78 +157,169 @@ export function isFile(obj, navigatorObj) {
}
return false
}
if (typeof File !== 'undefined') {
// eslint-disable-next-line no-undef
return obj instanceof File

if (typeof File !== 'undefined' && obj instanceof File) { // eslint-disable-line no-undef
return true
}
if (typeof Blob !== 'undefined' && obj instanceof Blob) { // eslint-disable-line no-undef
return true
}
if (typeof Buffer !== 'undefined' && obj instanceof Buffer) {
return true
}

return obj !== null && typeof obj === 'object' && typeof obj.pipe === 'function'
}

function formatValue(input, skipEncoding) {
const {collectionFormat, allowEmptyValue} = input
// `input` can be string in OAS3 contexts
const value = typeof input === 'object' ? input.value : input
const SEPARATORS = {
csv: ',',
ssv: '%20',
tsv: '%09',
pipes: '|'
}
function isArrayOfFile(obj, navigatorObj) {
return (Array.isArray(obj) && obj.some(v => isFile(v, navigatorObj)))
}

const STYLE_SEPARATORS = {
form: ',',
spaceDelimited: '%20',
pipeDelimited: '|'
}

const SEPARATORS = {
csv: ',',
ssv: '%20',
tsv: '%09',
pipes: '|'
}

// Formats a key-value and returns an array of key-value pairs.
//
// Return value example 1: [['color', 'blue']]
// Return value example 2: [['color', 'blue,black,brown']]
// Return value example 3: [['color', ['blue', 'black', 'brown']]]
// Return value example 4: [['color', 'R,100,G,200,B,150']]
// Return value example 5: [['R', '100'], ['G', '200'], ['B', '150']]
// Return value example 6: [['color[R]', '100'], ['color[G]', '200'], ['color[B]', '150']]
function formatKeyValue(key, input, skipEncoding = false) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the key may change depending on the value, formatValue has been changed to a method that returns a key-value pair

const {collectionFormat, allowEmptyValue, serializationOption, encoding} = input
// `input` can be string
const value = (typeof input === 'object' && !Array.isArray(input)) ? input.value : input
const encodeFn = skipEncoding ? (k => k.toString()) : (k => encodeURIComponent(k))
const encodedKey = encodeFn(key)

if (typeof value === 'undefined' && allowEmptyValue) {
return ''
return [[encodedKey, '']]
}

// file
if (isFile(value) || isArrayOfFile(value)) {
return [[encodedKey, value]]
}

if (isFile(value) || typeof value === 'boolean') {
return value
// for OAS 3 Parameter Object for serialization
if (serializationOption) {
return formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption)
}

let encodeFn = encodeURIComponent
// skipEncoding is an option to skip using the encodeURIComponent
// and allow reassignment to a different "encoding" function
// we should only use encodeURIComponent for known url strings
if (skipEncoding) {
if (isString(value) || Array.isArray(value)) {
encodeFn = str => str
// for OAS 3 Encoding Object
if (encoding) {
if ([typeof encoding.style, typeof encoding.explode, typeof encoding.allowReserved].some(type => type !== 'undefined')) {
return formatKeyValueBySerializationOption(key, value, skipEncoding, pick(encoding, ['style', 'explode', 'allowReserved']))
}
else {
encodeFn = obj => JSON.stringify(obj)

if (encoding.contentType) {
if (encoding.contentType === 'application/json') {
// If value is a string, assume value is already a JSON string
const json = typeof value === 'string' ? value : JSON.stringify(value)
return [[encodedKey, encodeFn(json)]]
}
return [[encodedKey, encodeFn(value.toString())]]
}

// Primitive
if (typeof value !== 'object') {
return [[encodedKey, encodeFn(value)]]
}

// Array of primitives
if (Array.isArray(value) && value.every(v => typeof v !== 'object')) {
return [[encodedKey, value.map(encodeFn).join(',')]]
}

// Array or object
return [[encodedKey, encodeFn(JSON.stringify(value))]]
}

// for OAS 2 Parameter Object
// Primitive
if (typeof value !== 'object') {
return [[encodedKey, encodeFn(value)]]
}

// Array
if (Array.isArray(value)) {
if (collectionFormat === 'multi') {
// In case of multipart/formdata, it is used as array.
// Otherwise, the caller will convert it to a query by qs.stringify.
return [[encodedKey, value.map(encodeFn)]]
}

return [[encodedKey, value.map(encodeFn).join(SEPARATORS[collectionFormat || 'csv'])]]
}

if (typeof value === 'object' && !Array.isArray(value)) {
return ''
// Object
return [[encodedKey, '']]
}

function formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption) {
const style = serializationOption.style || 'form'
const explode = typeof serializationOption.explode === 'undefined' ? style === 'form' : serializationOption.explode
// eslint-disable-next-line no-nested-ternary
const escape = skipEncoding ? false : (serializationOption && serializationOption.allowReserved ? 'unsafe' : 'reserved')
const encodeFn = v => encodeDisallowedCharacters(v, {escape})
const encodeKeyFn = skipEncoding ? (k => k) : (k => encodeDisallowedCharacters(k, {escape}))

// Primitive
if (typeof value !== 'object') {
return [[encodeKeyFn(key), encodeFn(value)]]
}

if (!Array.isArray(value)) {
return encodeFn(value)
// Array
if (Array.isArray(value)) {
if (explode) {
// In case of multipart/formdata, it is used as array.
// Otherwise, the caller will convert it to a query by qs.stringify.
return [[encodeKeyFn(key), value.map(encodeFn)]]
}
return [[encodeKeyFn(key), value.map(encodeFn).join(STYLE_SEPARATORS[style])]]
}

if (Array.isArray(value) && !collectionFormat) {
return value.map(encodeFn).join(',')
// Object
if (style === 'deepObject') {
return Object.keys(value).map(valueKey => [encodeKeyFn(`${key}[${valueKey}]`), encodeFn(value[valueKey])])
}
if (collectionFormat === 'multi') {
// query case (not multipart/formdata)
return value.map(encodeFn)

if (explode) {
return Object.keys(value).map(valueKey => [encodeKeyFn(valueKey), encodeFn(value[valueKey])])
}
return value.map(encodeFn).join(SEPARATORS[collectionFormat])

return [[encodeKeyFn(key), Object.keys(value).map(valueKey => [`${encodeKeyFn(valueKey)},${encodeFn(value[valueKey])}`]).join(',')]]
}

function buildFormData(reqForm) {
/**
* Build a new FormData instance, support array as field value
* OAS2.0 - via collectionFormat in spec definition
* OAS3.0 - via oas3BuildRequest, isOAS3formatArray flag
* OAS2.0 - when collectionFormat is multi
* OAS3.0 - when explode of Encoding Object is true
* @param {Object} reqForm - ori req.form
* @return {FormData} - new FormData instance
*/
return Object.entries(reqForm).reduce((formData, [name, input]) => {
if ((isNil(input.collectionFormat) || input.collectionFormat !== 'multi') && !input.isOAS3formatArray) {
formData.append(name, formatValue(input, true))
}
else {
input.value.forEach(item =>
formData.append(name, formatValue({...input, value: item}, true)))
for (const [key, value] of formatKeyValue(name, input, true)) {
if (Array.isArray(value)) {
for (const v of value) {
formData.append(key, v)
}
}
else {
formData.append(key, value)
}
}
return formData
}, new FormData())
Expand All @@ -242,14 +334,9 @@ export function encodeFormOrQuery(data) {
* @return {object} encoded parameter names and values
*/
const encodedQuery = Object.keys(data).reduce((result, parameterName) => {
const isObject = a => a && typeof a === 'object'
const paramValue = data[parameterName]
const skipEncoding = !!paramValue.skipEncoding
const encodedParameterName = skipEncoding ? parameterName : encodeURIComponent(parameterName)
const notArray = isObject(paramValue) && !Array.isArray(paramValue)
result[encodedParameterName] = formatValue(
notArray ? paramValue : {value: paramValue}, skipEncoding
)
for (const [key, value] of formatKeyValue(parameterName, data[parameterName])) {
result[key] = value
}
return result
}, {})
return qs.stringify(encodedQuery, {encode: false, indices: false}) || ''
Expand All @@ -266,7 +353,8 @@ export function mergeInQueryOrForm(req = {}) {

if (form) {
const hasFile = Object.keys(form).some((key) => {
return isFile(form[key].value)
const value = form[key].value
return isFile(value) || isArrayOfFile(value)
})

const contentType = req.headers['content-type'] || req.headers['Content-Type']
Expand Down
Loading