feature: support for Parameter.content (#5571)
* add `getParameterSchema` OAS helper * use `Parameter.content.[firstKey].schema` as schema value when present * `newValue` -> `initialValue` * make `paramWithMeta` a const * add trailing comma to `swagger2SchemaKeys` * refactor `helpers` to a folder * deprecate `src/core/utils.js` in favor of `src/core/helpers/` * support `Parameter.content.[mediaType].schema` in validateParam * reject `null` as an OAS3 object value * expose Fetch errors in the browser console * generate ParameterRow default values based on `content` values * add tests for `getParameterSchema` * remove debugger statement * remove debugger statement * don't apply `generatedSampleValue`s to parameters with `examples` * remove extra semi * disable JSON check in parameter runtime validation * stringify JsonSchema_object textarea values * add Cypress tests * swagger-client@3.9.4
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -21100,9 +21100,9 @@
|
||||
}
|
||||
},
|
||||
"swagger-client": {
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.9.3.tgz",
|
||||
"integrity": "sha512-MkSI3oi9fBdpmihgY5Eo3XFzhnE/mryYA9siIykTd1y3xtSMXTxoQjXo3hUEUjp3vtdEW/yLMexUsq7fAjrP7Q==",
|
||||
"version": "3.9.4",
|
||||
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.9.4.tgz",
|
||||
"integrity": "sha512-Rd4BrAUQeVIFYqzg7lkJMpd1P/oMVzcXEaKKaD+hfF8x4ZTTElNClCikD2yjFvmRCx0J2eIWs908kQOPkwc63w==",
|
||||
"requires": {
|
||||
"@babel/runtime-corejs2": "^7.0.0",
|
||||
"@kyleshockey/object-assign-deep": "^0.4.0",
|
||||
@@ -21125,9 +21125,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.2.tgz",
|
||||
"integrity": "sha512-iy9koArjAFCzGnx3ZvNA6Z0clIbbFgbdWQ0mKD3hO0krOrZh8UgA6qMKcZvwLJxS+D6iVR76+5/pV56yMNYTag==",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4"
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"remarkable": "^1.7.4",
|
||||
"reselect": "^2.5.4",
|
||||
"serialize-error": "^2.1.0",
|
||||
"swagger-client": "^3.9.3",
|
||||
"swagger-client": "^3.9.4",
|
||||
"url-parse": "^1.4.7",
|
||||
"xml-but-prettier": "^1.0.1",
|
||||
"zenscroll": "^4.0.2"
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Map, List } from "immutable"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
import win from "core/window"
|
||||
import { getExtensions, getCommonExtensions, numberToString, stringify } from "core/utils"
|
||||
import { getSampleSchema, getExtensions, getCommonExtensions, numberToString, stringify } from "core/utils"
|
||||
import getParameterSchema from "../../helpers/get-parameter-schema.js"
|
||||
|
||||
export default class ParameterRow extends Component {
|
||||
static propTypes = {
|
||||
@@ -40,7 +41,7 @@ export default class ParameterRow extends Component {
|
||||
let enumValue
|
||||
|
||||
if(isOAS3) {
|
||||
let schema = parameterWithMeta.get("schema") || Map()
|
||||
let schema = getParameterSchema(parameterWithMeta, { isOAS3 })
|
||||
enumValue = schema.get("enum")
|
||||
} else {
|
||||
enumValue = parameterWithMeta ? parameterWithMeta.get("enum") : undefined
|
||||
@@ -95,30 +96,68 @@ export default class ParameterRow extends Component {
|
||||
setDefaultValue = () => {
|
||||
let { specSelectors, pathMethod, rawParam, oas3Selectors } = this.props
|
||||
|
||||
let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()
|
||||
const paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()
|
||||
|
||||
const schema = getParameterSchema(paramWithMeta, { isOAS3: specSelectors.isOAS3() })
|
||||
|
||||
const parameterMediaType = paramWithMeta
|
||||
.get("content", Map())
|
||||
.keySeq()
|
||||
.first()
|
||||
|
||||
const generatedSampleValue = getSampleSchema(schema.toJS(), parameterMediaType, {
|
||||
includeWriteOnly: true
|
||||
})
|
||||
|
||||
if (!paramWithMeta || paramWithMeta.get("value") !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if( paramWithMeta.get("in") !== "body" ) {
|
||||
let newValue
|
||||
let initialValue
|
||||
|
||||
//// Find an initial value
|
||||
|
||||
if (specSelectors.isSwagger2()) {
|
||||
newValue = paramWithMeta.get("x-example")
|
||||
|| paramWithMeta.getIn(["default"])
|
||||
initialValue = paramWithMeta.get("x-example")
|
||||
|| paramWithMeta.getIn(["schema", "example"])
|
||||
|| paramWithMeta.getIn(["schema", "default"])
|
||||
|| schema.getIn(["default"])
|
||||
} else if (specSelectors.isOAS3()) {
|
||||
const currentExampleKey = oas3Selectors.activeExamplesMember(...pathMethod, "parameters", this.getParamKey())
|
||||
newValue = paramWithMeta.getIn(["examples", currentExampleKey, "value"])
|
||||
initialValue = paramWithMeta.getIn(["examples", currentExampleKey, "value"])
|
||||
|| paramWithMeta.getIn(["content", parameterMediaType, "example"])
|
||||
|| paramWithMeta.get("example")
|
||||
|| paramWithMeta.getIn(["schema", "example"])
|
||||
|| paramWithMeta.getIn(["schema", "default"])
|
||||
|| schema.get("example")
|
||||
|| schema.get("default")
|
||||
}
|
||||
if(newValue !== undefined) {
|
||||
|
||||
//// Process the initial value
|
||||
|
||||
if(initialValue !== undefined && !List.isList(initialValue)) {
|
||||
// Stringify if it isn't a List
|
||||
initialValue = stringify(initialValue)
|
||||
}
|
||||
|
||||
//// Dispatch the initial value
|
||||
|
||||
if(initialValue !== undefined) {
|
||||
this.onChangeWrapper(initialValue)
|
||||
} else if(
|
||||
schema.get("type") === "object"
|
||||
&& generatedSampleValue
|
||||
&& !paramWithMeta.get("examples")
|
||||
) {
|
||||
// Object parameters get special treatment.. if the user doesn't set any
|
||||
// default or example values, we'll provide initial values generated from
|
||||
// the schema.
|
||||
// However, if `examples` exist for the parameter, we won't do anything,
|
||||
// so that the appropriate `examples` logic can take over.
|
||||
this.onChangeWrapper(
|
||||
List.isList(newValue) ? newValue : stringify(newValue)
|
||||
List.isList(generatedSampleValue) ? (
|
||||
generatedSampleValue
|
||||
) : (
|
||||
stringify(generatedSampleValue)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -171,7 +210,7 @@ export default class ParameterRow extends Component {
|
||||
|
||||
let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()
|
||||
let format = param.get("format")
|
||||
let schema = isOAS3 ? param.get("schema") : param
|
||||
let schema = getParameterSchema(param, { isOAS3 })
|
||||
let type = schema.get("type")
|
||||
let isFormData = inType === "formData"
|
||||
let isFormDataSupported = "FormData" in win
|
||||
@@ -285,7 +324,7 @@ export default class ParameterRow extends Component {
|
||||
getConfigs={ getConfigs }
|
||||
isExecute={ isExecute }
|
||||
specSelectors={ specSelectors }
|
||||
schema={ param.get("schema") }
|
||||
schema={ schema }
|
||||
example={ bodyParam }/>
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { List, fromJS } from "immutable"
|
||||
import cx from "classnames"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
import DebounceInput from "react-debounce-input"
|
||||
import { stringify } from "core/utils"
|
||||
//import "less/json-schema-form"
|
||||
|
||||
const noop = ()=> {}
|
||||
@@ -269,7 +270,7 @@ export class JsonSchema_object extends PureComponent {
|
||||
<TextArea
|
||||
className={cx({ invalid: errors.size })}
|
||||
title={ errors.size ? errors.join(", ") : ""}
|
||||
value={value}
|
||||
value={stringify(value)}
|
||||
disabled={disabled}
|
||||
onChange={ this.handleOnChange }/>
|
||||
</div>
|
||||
|
||||
@@ -436,9 +436,12 @@ export const executeRequest = (req) =>
|
||||
specActions.setResponse(req.pathName, req.method, res)
|
||||
} )
|
||||
.catch(
|
||||
err => specActions.setResponse(req.pathName, req.method, {
|
||||
err => {
|
||||
console.error(err)
|
||||
specActions.setResponse(req.pathName, req.method, {
|
||||
error: true, err: serializeError(err)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
/*
|
||||
ATTENTION! This file (but not the functions within) is deprecated.
|
||||
|
||||
You should probably add a new file to `./helpers/` instead of adding a new
|
||||
function here.
|
||||
|
||||
One-function-per-file is a better pattern than what we have here.
|
||||
|
||||
If you're refactoring something in here, feel free to break it out to a file
|
||||
in `./helpers` if you have the time.
|
||||
*/
|
||||
|
||||
import Im from "immutable"
|
||||
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"
|
||||
import camelCase from "lodash/camelCase"
|
||||
@@ -9,6 +21,7 @@ import eq from "lodash/eq"
|
||||
import { memoizedSampleFromSchema, memoizedCreateXMLExample } from "core/plugins/samples/fn"
|
||||
import win from "./window"
|
||||
import cssEscape from "css.escape"
|
||||
import getParameterSchema from "../helpers/get-parameter-schema"
|
||||
|
||||
const DEFAULT_RESPONSE_KEY = "default"
|
||||
|
||||
@@ -488,7 +501,7 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
let errors = []
|
||||
let required = param.get("required")
|
||||
|
||||
let paramDetails = isOAS3 ? param.get("schema") : param
|
||||
let paramDetails = getParameterSchema(param, { isOAS3 })
|
||||
|
||||
if(!paramDetails) return errors
|
||||
|
||||
@@ -517,18 +530,23 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
|
||||
let oas3ObjectCheck = false
|
||||
|
||||
if(false || isOAS3 && type === "object") {
|
||||
if(typeof value === "object") {
|
||||
if(isOAS3 && type === "object") {
|
||||
if(typeof value === "object" && value !== null) {
|
||||
oas3ObjectCheck = true
|
||||
} else if(typeof value === "string") {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
oas3ObjectCheck = true
|
||||
} catch(e) {
|
||||
errors.push("Parameter string value must be valid JSON")
|
||||
return errors
|
||||
}
|
||||
}
|
||||
// Disabled because `validateParam` doesn't consider the MediaType of the
|
||||
// `Parameter.content` hint correctly.
|
||||
// } else if(typeof value === "string") {
|
||||
// try {
|
||||
// JSON.parse(value)
|
||||
// oas3ObjectCheck = true
|
||||
// } catch(e) {
|
||||
// errors.push("Parameter string value must be valid JSON")
|
||||
// return errors
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const allChecks = [
|
||||
|
||||
67
src/helpers/get-parameter-schema.js
Normal file
67
src/helpers/get-parameter-schema.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import Im from "immutable"
|
||||
|
||||
const swagger2SchemaKeys = Im.Set.of(
|
||||
"type",
|
||||
"format",
|
||||
"items",
|
||||
"default",
|
||||
"maximum",
|
||||
"exclusiveMaximum",
|
||||
"minimum",
|
||||
"exclusiveMinimum",
|
||||
"maxLength",
|
||||
"minLength",
|
||||
"pattern",
|
||||
"maxItems",
|
||||
"minItems",
|
||||
"uniqueItems",
|
||||
"enum",
|
||||
"multipleOf"
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the effective schema value for a parameter, or an empty Immutable.Map if
|
||||
* no suitable schema can be found.
|
||||
*
|
||||
* Supports OpenAPI 3.0 `Parameter.content` priority -- since a Parameter Object
|
||||
* cannot have both `schema` and `content`, this function ignores `schema` when
|
||||
* `content` is present.
|
||||
*
|
||||
* @param {Immutable.Map} parameter The parameter to identify a schema for
|
||||
* @param {object} config
|
||||
* @param {boolean} config.isOAS3 Whether the parameter is from an OpenAPI 2.0
|
||||
* or OpenAPI 3.0 definition
|
||||
* @return {Immutable.Map} The desired schema
|
||||
*/
|
||||
export default function getParameterSchema(parameter, { isOAS3 } = {}) {
|
||||
// Return empty Map if `parameter` isn't a Map
|
||||
if (!Im.Map.isMap(parameter)) return Im.Map()
|
||||
|
||||
if (!isOAS3) {
|
||||
// Swagger 2.0
|
||||
if (parameter.get("in") === "body") {
|
||||
return parameter.get("schema", Im.Map())
|
||||
} else {
|
||||
return parameter.filter((v, k) => swagger2SchemaKeys.includes(k))
|
||||
}
|
||||
}
|
||||
|
||||
// If we've reached here, the parameter is OpenAPI 3.0
|
||||
|
||||
if (parameter.get("content")) {
|
||||
const parameterContentMediaTypes = parameter
|
||||
.get("content", Im.Map({}))
|
||||
.keySeq()
|
||||
|
||||
return parameter.getIn(
|
||||
["content", parameterContentMediaTypes.first(), "schema"],
|
||||
Im.Map()
|
||||
)
|
||||
}
|
||||
|
||||
return parameter.get("schema", Im.Map())
|
||||
}
|
||||
142
test/core/helpers/get-parameter-schema.js
Normal file
142
test/core/helpers/get-parameter-schema.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import expect from "expect"
|
||||
import Im, { fromJS } from "immutable"
|
||||
import getParameterSchema from "../../../src/helpers/get-parameter-schema"
|
||||
|
||||
describe("getParameterSchema", () => {
|
||||
it("should return an empty Map when given no parameters", () => {
|
||||
const result = getParameterSchema()
|
||||
|
||||
expect(result).toEqual(fromJS({}))
|
||||
})
|
||||
|
||||
it("should return an empty Map when given an empty Map", () => {
|
||||
const result = getParameterSchema(fromJS({}))
|
||||
|
||||
expect(result).toEqual(fromJS({}))
|
||||
})
|
||||
|
||||
it("should return a schema for a Swagger 2.0 query parameter", () => {
|
||||
const result = getParameterSchema(
|
||||
fromJS({
|
||||
name: "id",
|
||||
in: "query",
|
||||
description: "ID of the object to fetch",
|
||||
required: false,
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
collectionFormat: "multi",
|
||||
})
|
||||
)
|
||||
|
||||
expect(result.toJS()).toEqual({
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should return a schema for a Swagger 2.0 body parameter", () => {
|
||||
const result = getParameterSchema(
|
||||
fromJS({
|
||||
name: "user",
|
||||
in: "body",
|
||||
description: "user to add to the system",
|
||||
required: true,
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(result.toJS()).toEqual({
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should return a schema for an OpenAPI 3.0 query parameter", () => {
|
||||
const result = getParameterSchema(
|
||||
fromJS({
|
||||
name: "id",
|
||||
in: "query",
|
||||
description: "ID of the object to fetch",
|
||||
required: false,
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
style: "form",
|
||||
explode: true,
|
||||
}),
|
||||
{
|
||||
isOAS3: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.toJS()).toEqual({
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should return a schema for an OpenAPI 3.0 query parameter with `content`", () => {
|
||||
const result = getParameterSchema(
|
||||
fromJS({
|
||||
in: "query",
|
||||
name: "coordinates",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["lat", "long"],
|
||||
properties: {
|
||||
lat: {
|
||||
type: "number",
|
||||
},
|
||||
long: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
"should-ignore/the-second-media-type": {
|
||||
type: "string",
|
||||
default: "this shouldn't be returned",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
isOAS3: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.toJS()).toEqual({
|
||||
type: "object",
|
||||
required: ["lat", "long"],
|
||||
properties: {
|
||||
lat: {
|
||||
type: "number",
|
||||
},
|
||||
long: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -412,15 +412,15 @@ describe("utils", function() {
|
||||
})
|
||||
assertValidateOas3Param(param, value, [])
|
||||
|
||||
// invalid object-as-string
|
||||
param = {
|
||||
required: true,
|
||||
schema: {
|
||||
type: "object"
|
||||
}
|
||||
}
|
||||
value = "{{}"
|
||||
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
|
||||
// // invalid object-as-string
|
||||
// param = {
|
||||
// required: true,
|
||||
// schema: {
|
||||
// type: "object"
|
||||
// }
|
||||
// }
|
||||
// value = "{{}"
|
||||
// assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
|
||||
|
||||
// missing when required
|
||||
param = {
|
||||
@@ -456,14 +456,14 @@ describe("utils", function() {
|
||||
})
|
||||
assertValidateOas3Param(param, value, [])
|
||||
|
||||
// invalid object-as-string
|
||||
param = {
|
||||
schema: {
|
||||
type: "object"
|
||||
}
|
||||
}
|
||||
value = "{{}"
|
||||
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
|
||||
// // invalid object-as-string
|
||||
// param = {
|
||||
// schema: {
|
||||
// type: "object"
|
||||
// }
|
||||
// }
|
||||
// value = "{{}"
|
||||
// assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
|
||||
|
||||
// missing when not required
|
||||
param = {
|
||||
|
||||
41
test/e2e-cypress/static/documents/bugs/4442.yaml
Normal file
41
test/e2e-cypress/static/documents/bugs/4442.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
openapi: "3.0.0"
|
||||
|
||||
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
parameters:
|
||||
- name: users
|
||||
in: query
|
||||
description: List of users to query for
|
||||
content:
|
||||
application/json:
|
||||
example:
|
||||
- userId: 1
|
||||
currency: USD
|
||||
- userId: 2
|
||||
currency: CAD
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserArray"
|
||||
responses:
|
||||
200:
|
||||
description: OK!
|
||||
components:
|
||||
schemas:
|
||||
|
||||
UserArray:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/User"
|
||||
|
||||
User:
|
||||
type: object
|
||||
required:
|
||||
- userId
|
||||
- currency
|
||||
properties:
|
||||
userId:
|
||||
type: integer
|
||||
format: int32
|
||||
currency:
|
||||
type: string
|
||||
37
test/e2e-cypress/tests/bugs/4442.js
Normal file
37
test/e2e-cypress/tests/bugs/4442.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
describe("UI #4442: Parameter.content display and execution", function() {
|
||||
it("should display textareas as static documentation according to the `example`", () => {
|
||||
cy.visit("/?url=/documents/bugs/4442.yaml")
|
||||
.get(`#operations-default-get_`)
|
||||
.click()
|
||||
.get(".btn.try-out__btn")
|
||||
.click()
|
||||
.get(
|
||||
`div.json-schema-array > div:nth-child(1) > div > textarea`
|
||||
)
|
||||
.should("have.value", `{\n "userId": 1,\n "currency": "USD"\n}`)
|
||||
.get(
|
||||
`div.json-schema-array > div:nth-child(2) > div > textarea`
|
||||
)
|
||||
.should("have.value", `{\n "userId": 2,\n "currency": "CAD"\n}`)
|
||||
})
|
||||
it("should serialize JSON into a query correctly", () => {
|
||||
cy.visit("/?url=/documents/bugs/4442.yaml")
|
||||
.get(`#operations-default-get_`)
|
||||
.click()
|
||||
.get(".btn.try-out__btn")
|
||||
.click()
|
||||
.get(".btn.execute")
|
||||
.click()
|
||||
.get(".request-url pre")
|
||||
.should(
|
||||
"have.text",
|
||||
`http://localhost:3230/?users=${encodeURIComponent(
|
||||
`[{"userId":1,"currency":"USD"},{"userId":2,"currency":"CAD"}]`
|
||||
)}`
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user