feat: Multiple Examples for OpenAPI 3 Parameters, Request Bodies, and Responses (via #5427)

* add opt-in Prettier config

* remove legacy `examples` implementation

* create ExamplesSelect

* support `Response.examples` in OpenAPI 3

* create response controls group

* prettier reformat

* prepare to break up Parameters

* reunify Parameters and OAS3 Parameters

* Parameter Examples

* Example component

* handle parameter value stringification correctly

* FOR REVIEW: add prop for controlling Select

* use regular header for param examples in Try-It-Out

* manage active examples member via Redux

* Request Body Try-It-Out examples

* remove special Response description styling

* omit Example value display in Try-It-Out

* support disabled text inputs in JsonSchemaForm

* Example.omitValue => Example.showValue

* ExamplesSelectValueRetainer

* styling for disabled inputs

* remove console.log

* support "Modified Values" in ExamplesSelect

* remove Examples component
(wasn't used anywhere)

* use ParameterRow.getParamKey for active examples member keying

* split-rendering of examples in ParameterRow

* send disabled prop to JsonSchemaForm

* use content type to key request body active examples members

* remove debugger

* rewire RequestBodyEditor to be a controlled component

REVIEW: does this have perf implications?

* trigger synthetic onSelect events in ExamplesSelect

* prettier updates

* remove outdated Examples usage in RequestBody

* don't handle examples changes in ESVR

* make RequestBodyEditor semi-controlled

* don't default to an empty Map for request bodies

* add namespaceKey to ESVR for state mgmt

* don't key RequestBody activeExampleKeys on media type

* tweak ESVR isModifiedValueSelected calculation

* add trace class to ExamplesSelect

* remove usage of ESVR.currentNamespace

* reset to first example if currentExampleKey is invalid

* add default values to RequestBody rendering

* stringify things in ESVR

* avoid null select value (silences React warning)

* detect user inputs that match any examples member's value

* add trace class for json-schema-array

* shallowly convert namespace state, to preserve Immutable stucts in state

* stringify RBE values; don't trim JSON in editor

* match user input to an example when non-primitives are expressed in state as strings

* update Cypress

* don't apply sample values in JsonSchema_Object

* support disabling all JsonSchemaForm subcomponents

* Core tests

* style changes to accomodate Examples

* fix version-checking error in Response

* disable SCU for Responses

* don't stringify Select values

* ModelExample: default to Model tab if no example is available; provide a default no example message

* don't trim JSON ParamBody inputs

* read directly from 2.0 Response.schema instead of inferring a value

* show current Example information in RequestBody

* show label for Examples dropdown by default

* rework Response content ordering

* style disabled textareas like other read-only blocks

* meta: fix sourcemaps

* refactor ESVR setNameForNamespace

* protect second half of ternary expession

* cypress: `select.examples-select` => `.examples-select > select`

* clarify ModelExample.componentWillReceiveProps

* add gates/defaults to prevent issues in very bare-boned documents

* fix test block organization problem

* simplify RequestBodyEditor interface

* linter fixes

* prettier updates

* use plugin system for new components

* move ME Cypress helpers to other file
This commit is contained in:
kyle
2019-06-29 19:52:51 +01:00
committed by GitHub
parent 332ddaedcd
commit 23d7260f92
34 changed files with 3148 additions and 653 deletions

View File

@@ -5,19 +5,11 @@ import cx from "classnames"
import { fromJS, Seq, Iterable, List, Map } from "immutable"
import { getSampleSchema, fromJSOrdered, stringify } from "core/utils"
const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => {
if ( examples && examples.size ) {
return examples.entrySeq().map( ([ key, example ]) => {
let exampleValue = stringify(example)
return (<div key={ key }>
<h5>{ key }</h5>
<HighlightCode className="example" value={ exampleValue } />
</div>)
}).toArray()
}
if ( sampleResponse ) { return <div>
const getExampleComponent = ( sampleResponse, HighlightCode ) => {
if (
sampleResponse !== undefined &&
sampleResponse !== null
) { return <div>
<HighlightCode className="example" value={ sampleResponse } />
</div>
}
@@ -29,20 +21,24 @@ export default class Response extends React.Component {
super(props, context)
this.state = {
responseContentType: ""
responseContentType: "",
}
}
static propTypes = {
path: PropTypes.string.isRequired,
method: PropTypes.string.isRequired,
code: PropTypes.string.isRequired,
response: PropTypes.instanceOf(Iterable),
className: PropTypes.string,
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
specPath: ImPropTypes.list.isRequired,
fn: PropTypes.object.isRequired,
contentType: PropTypes.string,
activeExamplesKey: PropTypes.string,
controlsAcceptHeader: PropTypes.bool,
onContentTypeChange: PropTypes.func
}
@@ -61,8 +57,21 @@ export default class Response extends React.Component {
})
}
getTargetExamplesKey = () => {
const { response, contentType, activeExamplesKey } = this.props
const activeContentType = this.state.responseContentType || contentType
const activeMediaType = response.getIn(["content", activeContentType], Map({}))
const examplesForMediaType = activeMediaType.get("examples", null)
const firstExamplesKey = examplesForMediaType.keySeq().first()
return activeExamplesKey || firstExamplesKey
}
render() {
let {
path,
method,
code,
response,
className,
@@ -72,14 +81,14 @@ export default class Response extends React.Component {
getConfigs,
specSelectors,
contentType,
controlsAcceptHeader
controlsAcceptHeader,
oas3Actions,
} = this.props
let { inferSchema } = fn
let { isOAS3 } = specSelectors
let isOAS3 = specSelectors.isOAS3()
let headers = response.get("headers")
let examples = response.get("examples")
let links = response.get("links")
const Headers = getComponent("headers")
const HighlightCode = getComponent("highlightCode")
@@ -87,44 +96,53 @@ export default class Response extends React.Component {
const Markdown = getComponent( "Markdown" )
const OperationLink = getComponent("operationLink")
const ContentType = getComponent("contentType")
const ExamplesSelect = getComponent("ExamplesSelect")
const Example = getComponent("Example")
var sampleResponse
var sampleSchema
var schema, specPathWithPossibleSchema
const activeContentType = this.state.responseContentType || contentType
const activeMediaType = response.getIn(["content", activeContentType], Map({}))
const examplesForMediaType = activeMediaType.get("examples", null)
if(isOAS3()) {
const mediaType = response.getIn(["content", activeContentType], Map({}))
const oas3SchemaForContentType = mediaType.get("schema", Map({}))
// Goal: find a schema value for `schema`
if(isOAS3) {
const oas3SchemaForContentType = activeMediaType.get("schema", Map({}))
if(mediaType.get("example") !== undefined) {
sampleSchema = stringify(mediaType.get("example"))
} else {
sampleSchema = getSampleSchema(oas3SchemaForContentType.toJS(), this.state.responseContentType, {
includeReadOnly: true
})
}
sampleResponse = oas3SchemaForContentType ? sampleSchema : null
schema = oas3SchemaForContentType ? inferSchema(oas3SchemaForContentType.toJS()) : null
specPathWithPossibleSchema = oas3SchemaForContentType ? List(["content", this.state.responseContentType, "schema"]) : specPath
} else {
schema = inferSchema(response.toJS()) // TODO: don't convert back and forth. Lets just stick with immutable for inferSchema
schema = response.get("schema")
specPathWithPossibleSchema = response.has("schema") ? specPath.push("schema") : specPath
sampleResponse = schema ? getSampleSchema(schema, activeContentType, {
}
// Goal: find an example value for `sampleResponse`
if(isOAS3) {
const oas3SchemaForContentType = activeMediaType.get("schema", Map({}))
if(examplesForMediaType) {
const targetExamplesKey = this.getTargetExamplesKey()
const targetExample = examplesForMediaType.get(targetExamplesKey, Map({}))
sampleResponse = stringify(targetExample.get("value"))
} else if(activeMediaType.get("example") !== undefined) {
// use the example key's value
sampleResponse = stringify(activeMediaType.get("example"))
} else {
// use an example value generated based on the schema
sampleResponse = getSampleSchema(oas3SchemaForContentType.toJS(), this.state.responseContentType, {
includeReadOnly: true
})
}
} else {
sampleResponse = schema ? getSampleSchema(schema.toJS(), activeContentType, {
includeReadOnly: true,
includeWriteOnly: true // writeOnly has no filtering effect in swagger 2.0
}) : null
}
if(examples) {
examples = examples.map(example => {
// Remove unwanted properties from examples
return example.set ? example.set("$$ref", undefined) : example
})
}
let example = getExampleComponent( sampleResponse, examples, HighlightCode )
let example = getExampleComponent( sampleResponse, HighlightCode )
return (
<tr className={ "response " + ( className || "") } data-code={code}>
@@ -137,20 +155,55 @@ export default class Response extends React.Component {
<Markdown source={ response.get( "description" ) } />
</div>
{ isOAS3 ?
<div className={cx("response-content-type", {
"controls-accept-header": controlsAcceptHeader
})}>
<ContentType
{isOAS3 && response.get("content") ? (
<section className="response-controls">
<div
className={cx("response-control-media-type", {
"response-control-media-type--accept-controller": controlsAcceptHeader
})}
>
<small className="response-control-media-type__title">
Media type
</small>
<ContentType
value={this.state.responseContentType}
contentTypes={ response.get("content") ? response.get("content").keySeq() : Seq() }
contentTypes={
response.get("content")
? response.get("content").keySeq()
: Seq()
}
onChange={this._onContentTypeChange}
/>
{controlsAcceptHeader ? (
<small className="response-control-media-type__accept-message">
Controls <code>Accept</code> header.
</small>
) : null}
</div>
{examplesForMediaType ? (
<div className="response-control-examples">
<small className="response-control-examples__title">
Examples
</small>
<ExamplesSelect
examples={examplesForMediaType}
currentExampleKey={this.getTargetExamplesKey()}
onSelect={key =>
oas3Actions.setActiveExamplesMember({
name: key,
pathMethod: [path, method],
contextType: "responses",
contextName: code
})
}
showLabels={false}
/>
{ controlsAcceptHeader ? <small>Controls <code>Accept</code> header.</small> : null }
</div>
: null }
</div>
) : null}
</section>
) : null}
{ example ? (
{ example || schema ? (
<ModelExample
specPath={specPathWithPossibleSchema}
getComponent={ getComponent }
@@ -158,6 +211,14 @@ export default class Response extends React.Component {
specSelectors={ specSelectors }
schema={ fromJSOrdered(schema) }
example={ example }/>
) : null }
{ isOAS3 && examplesForMediaType ? (
<Example
example={examplesForMediaType.get(this.getTargetExamplesKey(), Map({}))}
getComponent={getComponent}
omitValue={true}
/>
) : null}
{ headers ? (
@@ -167,9 +228,8 @@ export default class Response extends React.Component {
/>
) : null}
</td>
{specSelectors.isOAS3() ? <td className="col response-col_links">
{isOAS3 ? <td className="col response-col_links">
{ links ?
links.toSeq().map((link, key) => {
return <OperationLink key={key} name={key} link={ link } getComponent={getComponent}/>