Skip to content

Commit

Permalink
fix: jsonSchemaComponent file/files (swagger-api#5997)
Browse files Browse the repository at this point in the history
oas3 - files
oas2 - file
  • Loading branch information
tim-lai committed May 14, 2020
1 parent ce45c37 commit fa53732
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 42 deletions.
19 changes: 17 additions & 2 deletions src/core/curlify.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import win from "./window"

/**
* if duplicate key name existed from FormData entries,
* we mutated the key name by appending a hashIdx
* @param {String} k - possibly mutated key name
* @return {String} - src key name
*/
const extractKey = (k) => {
const hashIdx = "_**[]"
if (k.indexOf(hashIdx) < 0) {
return k
}
return k.split(hashIdx)[0].trim()
}

export default function curl( request ){
let curlified = []
let type = ""
Expand All @@ -21,11 +35,12 @@ export default function curl( request ){

if(type === "multipart/form-data" && request.get("method") === "POST") {
for( let [ k,v ] of request.get("body").entrySeq()) {
let extractedKey = extractKey(k)
curlified.push( "-F" )
if (v instanceof win.File) {
curlified.push( `"${k}=@${v.name}${v.type ? `;type=${v.type}` : ""}"` )
curlified.push(`"${extractedKey}=@${v.name}${v.type ? `;type=${v.type}` : ""}"` )
} else {
curlified.push( `"${k}=${v}"` )
curlified.push(`"${extractedKey}=${v}"` )
}
}
} else {
Expand Down
100 changes: 68 additions & 32 deletions src/core/json-schema-components.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class JsonSchemaForm extends Component {
let { schema, errors, value, onChange, getComponent, fn, disabled } = this.props
const format = schema && schema.get ? schema.get("format") : null
const type = schema && schema.get ? schema.get("type") : null

let getComponentSilently = (name) => getComponent(name, false, { failSilently: true })
let Comp = type ? format ?
getComponentSilently(`JsonSchema_${type}_${format}`) :
Expand All @@ -63,7 +64,7 @@ export class JsonSchema_string extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
onChange = (e) => {
const value = this.props.schema && this.props.schema["type"] === "file" ? e.target.files[0] : e.target.value
const value = this.props.schema && this.props.schema.get("type") === "file" ? e.target.files[0] : e.target.value
this.props.onChange(value, this.props.keyName)
}
onEnumChange = (val) => this.props.onChange(val)
Expand Down Expand Up @@ -92,23 +93,27 @@ export class JsonSchema_string extends Component {
const isDisabled = disabled || (schemaIn && schemaIn === "formData" && !("FormData" in window))
const Input = getComponent("Input")
if (type && type === "file") {
return (<Input type="file"
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
onChange={ this.onChange }
disabled={isDisabled}/>)
return (
<Input type="file"
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
onChange={this.onChange}
disabled={isDisabled} />
)
}
else {
return (<DebounceInput
type={ format && format === "password" ? "password" : "text" }
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
value={value}
minLength={0}
debounceTimeout={350}
placeholder={description}
onChange={ this.onChange }
disabled={isDisabled}/>)
return (
<DebounceInput
type={format && format === "password" ? "password" : "text"}
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
value={value}
minLength={0}
debounceTimeout={350}
placeholder={description}
onChange={this.onChange}
disabled={isDisabled} />
)
}
}
}
Expand Down Expand Up @@ -170,14 +175,15 @@ export class JsonSchema_array extends PureComponent {
const schemaItemsSchema = schema.getIn(["items", "schema"])
let ArrayItemsComponent
let isArrayItemText = false
let isArrayItemFile = schemaItemsType === "file" ? true : false
if (schemaItemsType && schemaItemsFormat) {
ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}_${schemaItemsFormat}`)
} else if (schemaItemsType === "boolean" || schemaItemsType === "array" || schemaItemsType === "object") {
ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}`)
}
// if ArrayItemsComponent not assigned or does not exist,
// use default schemaItemsType === "string" & JsonSchemaArrayItemText component
if (!ArrayItemsComponent) {
if (!ArrayItemsComponent && !isArrayItemFile) {
isArrayItemText = true
}

Expand Down Expand Up @@ -205,22 +211,30 @@ export class JsonSchema_array extends PureComponent {
return (
<div key={i} className="json-schema-form-item">
{
isArrayItemText ?
<JsonSchemaArrayItemText
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
/>
: <ArrayItemsComponent {...this.props}
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
schema={schemaItemsSchema}
getComponent={getComponent}
fn={fn}
isArrayItemFile ?
<JsonSchemaArrayItemFile
value={item}
onChange={(val)=> this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
getComponent={getComponent}
/>
: isArrayItemText ?
<JsonSchemaArrayItemText
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
/>
: <ArrayItemsComponent {...this.props}
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
schema={schemaItemsSchema}
getComponent={getComponent}
fn={fn}
/>
}
{!disabled ? (
<Button
Expand Down Expand Up @@ -275,6 +289,28 @@ export class JsonSchemaArrayItemText extends Component {
}
}

export class JsonSchemaArrayItemFile extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps

onFileChange = (e) => {
const value = e.target.files[0]
this.props.onChange(value, this.props.keyName)
}

render() {
let { getComponent, errors, disabled } = this.props
const Input = getComponent("Input")
const isDisabled = disabled || !("FormData" in window)

return (<Input type="file"
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
onChange={this.onFileChange}
disabled={isDisabled} />)
}
}

export class JsonSchema_boolean extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
Expand Down
63 changes: 55 additions & 8 deletions src/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,64 @@ export function arrayify (thing) {
return normalizeArray(thing)
}

export function fromJSOrdered (js) {
if(isImmutable(js))
export function fromJSOrdered(js) {
if (isImmutable(js)) {
return js // Can't do much here

if (js instanceof win.File)
}
if (js instanceof win.File) {
return js
}
if (!isObject(js)) {
return js
}
if (Array.isArray(js)) {
return Im.Seq(js).map(fromJSOrdered).toList()
}
if (js.entries) {
// handle multipart/form-data
const objWithHashedKeys = createObjWithHashedKeys(js)
return Im.OrderedMap(objWithHashedKeys).map(fromJSOrdered)
}
return Im.OrderedMap(js).map(fromJSOrdered)
}

return !isObject(js) ? js :
Array.isArray(js) ?
Im.Seq(js).map(fromJSOrdered).toList() :
Im.OrderedMap(js).map(fromJSOrdered)
/**
* Convert a FormData object into plain object
* Append a hashIdx and counter to the key name, if multiple exists
* if single, key name = <original>
* if multiple, key name = <original><hashIdx><count>
* @param {FormData} fdObj - a FormData object
* @return {Object} - a plain object
*/
export function createObjWithHashedKeys (fdObj) {
if (!fdObj.entries) {
return fdObj // not a FormData object with iterable
}
const newObj = {}
const hashIdx = "_**[]" // our internal identifier
const trackKeys = {}
for (let pair of fdObj.entries()) {
if (!newObj[pair[0]] && !(trackKeys[pair[0]] && trackKeys[pair[0]].containsMultiple)) {
newObj[pair[0]] = pair[1] // first key name: no hash required
} else {
if (!trackKeys[pair[0]]) {
// initiate tracking key for multiple
trackKeys[pair[0]] = {
containsMultiple: true,
length: 1
}
// "reassign" first pair to matching hashed format for multiple
let hashedKeyFirst = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}`
newObj[hashedKeyFirst] = newObj[pair[0]]
// remove non-hashed key of multiple
delete newObj[pair[0]] // first
}
trackKeys[pair[0]].length += 1
let hashedKeyCurrent = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}`
newObj[hashedKeyCurrent] = pair[1]
}
}
return newObj
}

export function bindToState(obj, state) {
Expand Down
20 changes: 20 additions & 0 deletions test/mocha/core/curlify.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,26 @@ describe("curlify", function() {
expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"name=Sahar\"")
})

it("should print a curl with formData that extracts array representation with hashIdx", function() {
// Note: hashIdx = `_**[]${counter}`
// Usage of hashIdx is an internal SwaggerUI method to convert formData array into something curlify can handle
const req = {
url: "http://example.com",
method: "POST",
headers: { "content-type": "multipart/form-data" },
body: {
id: "123",
"fruits[]_**[]1": "apple",
"fruits[]_**[]2": "banana",
"fruits[]_**[]3": "grape"
}
}

let curlified = curl(Im.fromJS(req))

expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"fruits[]=apple\" -F \"fruits[]=banana\" -F \"fruits[]=grape\"")
})

it("should print a curl with formData and file", function() {
var file = new win.File()
file.name = "file.txt"
Expand Down

0 comments on commit fa53732

Please sign in to comment.