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:
@@ -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}/>
|
||||
|
||||
Reference in New Issue
Block a user