* 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.4bubble
@@ -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, { | |||
error: true, err: serializeError(err) | |||
}) | |||
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 | |||
} | |||
oas3ObjectCheck = true | |||
} | |||
// 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 = [ | |||
@@ -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()) | |||
} |
@@ -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 = { | |||
@@ -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 |
@@ -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"}]` | |||
)}` | |||
) | |||
}) | |||
}) |