@@ -41,6 +41,7 @@ | |||||
"dependencies": { | "dependencies": { | ||||
"base64-js": "^1.2.0", | "base64-js": "^1.2.0", | ||||
"brace": "0.7.0", | "brace": "0.7.0", | ||||
"classnames": "^2.2.5", | |||||
"deep-extend": "0.4.1", | "deep-extend": "0.4.1", | ||||
"expect": "1.20.2", | "expect": "1.20.2", | ||||
"getbase": "^2.8.2", | "getbase": "^2.8.2", | ||||
@@ -1,5 +1,6 @@ | |||||
import React from "react" | import React from "react" | ||||
import PropTypes from "prop-types" | import PropTypes from "prop-types" | ||||
import cx from "classnames" | |||||
import { fromJS, Seq } from "immutable" | import { fromJS, Seq } from "immutable" | ||||
import { getSampleSchema, fromJSOrdered } from "core/utils" | import { getSampleSchema, fromJSOrdered } from "core/utils" | ||||
@@ -46,7 +47,8 @@ export default class Response extends React.Component { | |||||
getComponent: PropTypes.func.isRequired, | getComponent: PropTypes.func.isRequired, | ||||
specSelectors: PropTypes.object.isRequired, | specSelectors: PropTypes.object.isRequired, | ||||
fn: PropTypes.object.isRequired, | fn: PropTypes.object.isRequired, | ||||
contentType: PropTypes.string | |||||
contentType: PropTypes.string, | |||||
controlsAcceptHeader: PropTypes.bool | |||||
} | } | ||||
static defaultProps = { | static defaultProps = { | ||||
@@ -61,7 +63,8 @@ export default class Response extends React.Component { | |||||
fn, | fn, | ||||
getComponent, | getComponent, | ||||
specSelectors, | specSelectors, | ||||
contentType | |||||
contentType, | |||||
controlsAcceptHeader | |||||
} = this.props | } = this.props | ||||
let { inferSchema } = fn | let { inferSchema } = fn | ||||
@@ -106,11 +109,18 @@ export default class Response extends React.Component { | |||||
<Markdown source={ response.get( "description" ) } /> | <Markdown source={ response.get( "description" ) } /> | ||||
</div> | </div> | ||||
{ isOAS3 ? <ContentType | |||||
value={this.state.responseContentType} | |||||
contentTypes={ response.get("content") ? response.get("content").keySeq() : Seq() } | |||||
onChange={(val) => this.setState({ responseContentType: val })} | |||||
className="response-content-type" /> : null } | |||||
{ isOAS3 ? | |||||
<div className={cx("response-content-type", { | |||||
"controls-accept-header": controlsAcceptHeader | |||||
})}> | |||||
<ContentType | |||||
value={this.state.responseContentType} | |||||
contentTypes={ response.get("content") ? response.get("content").keySeq() : Seq() } | |||||
onChange={(val) => this.setState({ responseContentType: val })} | |||||
/> | |||||
{ controlsAcceptHeader ? <small>Controls <code>Accept</code> header.</small> : null } | |||||
</div> | |||||
: null } | |||||
{ example ? ( | { example ? ( | ||||
<ModelExample | <ModelExample | ||||
@@ -1,7 +1,7 @@ | |||||
import React from "react" | import React from "react" | ||||
import PropTypes from "prop-types" | import PropTypes from "prop-types" | ||||
import { fromJS } from "immutable" | import { fromJS } from "immutable" | ||||
import { defaultStatusCode } from "core/utils" | |||||
import { defaultStatusCode, getAcceptControllingResponse } from "core/utils" | |||||
export default class Responses extends React.Component { | export default class Responses extends React.Component { | ||||
@@ -30,7 +30,17 @@ export default class Responses extends React.Component { | |||||
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val) | onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val) | ||||
render() { | render() { | ||||
let { responses, request, tryItOutResponse, getComponent, getConfigs, specSelectors, fn, producesValue, displayRequestDuration } = this.props | |||||
let { | |||||
responses, | |||||
request, | |||||
tryItOutResponse, | |||||
getComponent, | |||||
getConfigs, | |||||
specSelectors, | |||||
fn, | |||||
producesValue, | |||||
displayRequestDuration | |||||
} = this.props | |||||
let defaultCode = defaultStatusCode( responses ) | let defaultCode = defaultStatusCode( responses ) | ||||
const ContentType = getComponent( "contentType" ) | const ContentType = getComponent( "contentType" ) | ||||
@@ -39,6 +49,11 @@ export default class Responses extends React.Component { | |||||
let produces = this.props.produces && this.props.produces.size ? this.props.produces : Responses.defaultProps.produces | let produces = this.props.produces && this.props.produces.size ? this.props.produces : Responses.defaultProps.produces | ||||
const isSpecOAS3 = specSelectors.isOAS3() | |||||
const acceptControllingResponse = isSpecOAS3 ? | |||||
getAcceptControllingResponse(responses) : null | |||||
return ( | return ( | ||||
<div className="responses-wrapper"> | <div className="responses-wrapper"> | ||||
<div className="opblock-section-header"> | <div className="opblock-section-header"> | ||||
@@ -88,6 +103,7 @@ export default class Responses extends React.Component { | |||||
code={ code } | code={ code } | ||||
response={ response } | response={ response } | ||||
specSelectors={ specSelectors } | specSelectors={ specSelectors } | ||||
controlsAcceptHeader={response === acceptControllingResponse} | |||||
contentType={ producesValue } | contentType={ producesValue } | ||||
getComponent={ getComponent }/> | getComponent={ getComponent }/> | ||||
) | ) | ||||
@@ -650,3 +650,26 @@ export const shallowEqualKeys = (a,b, keys) => { | |||||
return eq(a[key], b[key]) | return eq(a[key], b[key]) | ||||
}) | }) | ||||
} | } | ||||
export function getAcceptControllingResponse(responses) { | |||||
if(!Im.OrderedMap.isOrderedMap(responses)) { | |||||
// wrong type! | |||||
return null | |||||
} | |||||
if(!responses.size) { | |||||
// responses is empty | |||||
return null | |||||
} | |||||
const suitable2xxResponse = responses.find((res, k) => { | |||||
return k.startsWith("2") && Object.keys(res.get("content") || {}).length > 0 | |||||
}) | |||||
// try to find a suitable `default` responses | |||||
const defaultResponse = responses.get("default") || Im.OrderedMap() | |||||
const defaultResponseMediaTypes = (defaultResponse.get("content") || Im.OrderedMap()).keySeq().toJS() | |||||
const suitableDefaultResponse = defaultResponseMediaTypes.length ? defaultResponse : null | |||||
return suitable2xxResponse || suitableDefaultResponse | |||||
} |
@@ -775,6 +775,17 @@ | |||||
.response-content-type { | .response-content-type { | ||||
padding-top: 1em; | padding-top: 1em; | ||||
&.controls-accept-header { | |||||
select { | |||||
border-color: green; | |||||
} | |||||
small { | |||||
color: green; | |||||
font-size: .7em; | |||||
} | |||||
} | |||||
} | } | ||||
@keyframes blinker | @keyframes blinker | ||||
@@ -1,7 +1,7 @@ | |||||
/* eslint-env mocha */ | /* eslint-env mocha */ | ||||
import expect from "expect" | import expect from "expect" | ||||
import { fromJS } from "immutable" | |||||
import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered } from "core/utils" | |||||
import { fromJS, OrderedMap } from "immutable" | |||||
import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered, getAcceptControllingResponse } from "core/utils" | |||||
import win from "core/window" | import win from "core/window" | ||||
describe("utils", function() { | describe("utils", function() { | ||||
@@ -581,5 +581,102 @@ describe("utils", function() { | |||||
const result = fromJSOrdered(param).toJS() | const result = fromJSOrdered(param).toJS() | ||||
expect( result ).toEqual( [1, 1, 2, 3, 5, 8] ) | expect( result ).toEqual( [1, 1, 2, 3, 5, 8] ) | ||||
}) | }) | ||||
}) | |||||
describe.only("getAcceptControllingResponse", () => { | |||||
it("should return the first 2xx response with a media type", () => { | |||||
const responses = fromJSOrdered({ | |||||
"200": { | |||||
content: { | |||||
"application/json": { | |||||
schema: { | |||||
type: "object" | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
"201": { | |||||
content: { | |||||
"application/json": { | |||||
schema: { | |||||
type: "object" | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}) | |||||
expect(getAcceptControllingResponse(responses)).toEqual(responses.get("200")) | |||||
}) | }) | ||||
it("should skip 2xx responses without defined media types", () => { | |||||
const responses = fromJSOrdered({ | |||||
"200": { | |||||
content: { | |||||
"application/json": { | |||||
schema: { | |||||
type: "object" | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
"201": { | |||||
content: { | |||||
"application/json": { | |||||
schema: { | |||||
type: "object" | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}) | |||||
expect(getAcceptControllingResponse(responses)).toEqual(responses.get("201")) | |||||
}) | |||||
it("should default to the `default` response if it has defined media types", () => { | |||||
const responses = fromJSOrdered({ | |||||
"200": { | |||||
description: "quite empty" | |||||
}, | |||||
"201": { | |||||
description: "quite empty" | |||||
}, | |||||
default: { | |||||
content: { | |||||
"application/json": { | |||||
schema: { | |||||
type: "object" | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}) | |||||
expect(getAcceptControllingResponse(responses)).toEqual(responses.get("default")) | |||||
}) | |||||
it("should return null if there are no suitable controlling responses", () => { | |||||
const responses = fromJSOrdered({ | |||||
"200": { | |||||
description: "quite empty" | |||||
}, | |||||
"201": { | |||||
description: "quite empty" | |||||
}, | |||||
"default": { | |||||
description: "also empty.." | |||||
} | |||||
}) | |||||
expect(getAcceptControllingResponse(responses)).toBe(null) | |||||
}) | |||||
it("should return null if an empty OrderedMap is passed", () => { | |||||
const responses = fromJSOrdered() | |||||
expect(getAcceptControllingResponse(responses)).toBe(null) | |||||
}) | |||||
it("should return null if anything except an OrderedMap is passed", () => { | |||||
const responses = {} | |||||
expect(getAcceptControllingResponse(responses)).toBe(null) | |||||
}) | |||||
}) | |||||
}) | }) |