diff --git a/package.json b/package.json index 8d85d26f..3d6350b3 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "base64-js": "^1.2.0", "brace": "0.7.0", + "classnames": "^2.2.5", "css.escape": "1.5.1", "deep-extend": "0.4.1", "expect": "1.20.2", diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index eb21b8db..fbf8ca60 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -28,6 +28,7 @@ export default class Operation extends PureComponent { authSelectors: PropTypes.object, specActions: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired, + oas3Actions: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired, layoutSelectors: PropTypes.object.isRequired, fn: PropTypes.object.isRequired, @@ -117,7 +118,8 @@ export default class Operation extends PureComponent { specSelectors, authActions, authSelectors, - getConfigs + getConfigs, + oas3Actions } = this.props let summary = operation.get("summary") @@ -265,6 +267,7 @@ export default class Operation extends PureComponent { getComponent={ getComponent } getConfigs={ getConfigs } specSelectors={ specSelectors } + oas3Actions={oas3Actions} specActions={ specActions } produces={ produces } producesValue={ operation.get("produces_value") } diff --git a/src/core/components/operations.jsx b/src/core/components/operations.jsx index e0bf0645..82bf62a3 100644 --- a/src/core/components/operations.jsx +++ b/src/core/components/operations.jsx @@ -9,6 +9,7 @@ export default class Operations extends React.Component { static propTypes = { specSelectors: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired, + oas3Actions: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, layoutSelectors: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired, @@ -21,6 +22,7 @@ export default class Operations extends React.Component { let { specSelectors, specActions, + oas3Actions, getComponent, layoutSelectors, layoutActions, @@ -147,6 +149,8 @@ export default class Operations extends React.Component { specActions={ specActions } specSelectors={ specSelectors } + oas3Actions={oas3Actions} + layoutActions={ layoutActions } layoutSelectors={ layoutSelectors } diff --git a/src/core/components/response.jsx b/src/core/components/response.jsx index 02662f90..8aad5109 100644 --- a/src/core/components/response.jsx +++ b/src/core/components/response.jsx @@ -1,5 +1,6 @@ import React from "react" import PropTypes from "prop-types" +import cx from "classnames" import { fromJS, Seq } from "immutable" import { getSampleSchema, fromJSOrdered } from "core/utils" @@ -46,13 +47,25 @@ export default class Response extends React.Component { getComponent: PropTypes.func.isRequired, specSelectors: PropTypes.object.isRequired, fn: PropTypes.object.isRequired, - contentType: PropTypes.string + contentType: PropTypes.string, + controlsAcceptHeader: PropTypes.bool, + onContentTypeChange: PropTypes.func } static defaultProps = { response: fromJS({}), + onContentTypeChange: () => {} }; + _onContentTypeChange = (value) => { + const { onContentTypeChange, controlsAcceptHeader } = this.props + this.setState({ responseContentType: value }) + onContentTypeChange({ + value: value, + controlsAcceptHeader + }) + } + render() { let { code, @@ -61,7 +74,8 @@ export default class Response extends React.Component { fn, getComponent, specSelectors, - contentType + contentType, + controlsAcceptHeader } = this.props let { inferSchema } = fn @@ -106,11 +120,18 @@ export default class Response extends React.Component { - { isOAS3 ? this.setState({ responseContentType: val })} - className="response-content-type" /> : null } + { isOAS3 ? +
+ + { controlsAcceptHeader ? Controls Accept header. : null } +
+ : null } { example ? ( this.props.specActions.changeProducesValue(this.props.pathMethod, val) + onResponseContentTypeChange = ({ controlsAcceptHeader, value }) => { + const { oas3Actions, pathMethod } = this.props + if(controlsAcceptHeader) { + oas3Actions.setResponseContentType({ + value, + pathMethod + }) + } + } + 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 ) const ContentType = getComponent( "contentType" ) @@ -39,6 +60,11 @@ export default class Responses extends React.Component { 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 (
@@ -78,7 +104,6 @@ export default class Responses extends React.Component { { responses.entrySeq().map( ([code, response]) => { - let className = tryItOutResponse && tryItOutResponse.get("status") == code ? "response_current" : "" return ( ) diff --git a/src/core/plugins/oas3/actions.js b/src/core/plugins/oas3/actions.js index 4a5bf9ac..ad81f3e7 100644 --- a/src/core/plugins/oas3/actions.js +++ b/src/core/plugins/oas3/actions.js @@ -4,6 +4,7 @@ export const UPDATE_SELECTED_SERVER = "oas3_set_servers" export const UPDATE_REQUEST_BODY_VALUE = "oas3_set_request_body_value" export const UPDATE_REQUEST_CONTENT_TYPE = "oas3_set_request_content_type" +export const UPDATE_RESPONSE_CONTENT_TYPE = "oas3_set_response_content_type" export const UPDATE_SERVER_VARIABLE_VALUE = "oas3_set_server_variable_value" export function setSelectedServer (selectedServerUrl) { @@ -27,6 +28,13 @@ export function setRequestContentType ({ value, pathMethod }) { } } +export function setResponseContentType ({ value, pathMethod }) { + return { + type: UPDATE_RESPONSE_CONTENT_TYPE, + payload: { value, pathMethod } + } +} + export function setServerVariableValue ({ server, key, val }) { return { type: UPDATE_SERVER_VARIABLE_VALUE, diff --git a/src/core/plugins/oas3/reducers.js b/src/core/plugins/oas3/reducers.js index 810c1ba0..149f55e3 100644 --- a/src/core/plugins/oas3/reducers.js +++ b/src/core/plugins/oas3/reducers.js @@ -2,7 +2,8 @@ import { UPDATE_SELECTED_SERVER, UPDATE_REQUEST_BODY_VALUE, UPDATE_REQUEST_CONTENT_TYPE, - UPDATE_SERVER_VARIABLE_VALUE + UPDATE_SERVER_VARIABLE_VALUE, + UPDATE_RESPONSE_CONTENT_TYPE } from "./actions" export default { @@ -17,6 +18,10 @@ export default { let [path, method] = pathMethod return state.setIn( [ "requestData", path, method, "requestContentType" ], value) }, + [UPDATE_RESPONSE_CONTENT_TYPE]: (state, { payload: { value, pathMethod } } ) =>{ + let [path, method] = pathMethod + return state.setIn( [ "requestData", path, method, "responseContentType" ], value) + }, [UPDATE_SERVER_VARIABLE_VALUE]: (state, { payload: { server, key, val } } ) =>{ return state.setIn( [ "serverVariableValues", server, key ], val) }, diff --git a/src/core/plugins/oas3/selectors.js b/src/core/plugins/oas3/selectors.js index a6c57761..86fc617f 100644 --- a/src/core/plugins/oas3/selectors.js +++ b/src/core/plugins/oas3/selectors.js @@ -30,6 +30,11 @@ export const requestContentType = onlyOAS3((state, path, method) => { } ) +export const responseContentType = onlyOAS3((state, path, method) => { + return state.getIn(["requestData", path, method, "responseContentType"]) || null + } +) + export const serverVariableValue = onlyOAS3((state, server, key) => { return state.getIn(["serverVariableValues", server, key]) || null } diff --git a/src/core/plugins/spec/actions.js b/src/core/plugins/spec/actions.js index 7465574c..98919880 100644 --- a/src/core/plugins/spec/actions.js +++ b/src/core/plugins/spec/actions.js @@ -218,6 +218,7 @@ export const executeRequest = (req) => req.server = oas3Selectors.selectedServer() req.serverVariables = oas3Selectors.serverVariables(req.server).toJS() req.requestContentType = oas3Selectors.requestContentType(pathName, method) + req.responseContentType = oas3Selectors.responseContentType(pathName, method) || "*/*" const requestBody = oas3Selectors.requestBodyValue(pathName, method) if(isJSONObject(requestBody)) { diff --git a/src/core/utils.js b/src/core/utils.js index b64b29cf..1a835d3f 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -652,5 +652,28 @@ export const shallowEqualKeys = (a,b, keys) => { }) } +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 +} + export const createDeepLinkPath = (str) => typeof str == "string" || str instanceof String ? str.trim().replace(/\s/g, "_") : "" -export const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str) ) \ No newline at end of file +export const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str) ) diff --git a/src/style/_layout.scss b/src/style/_layout.scss index edb21360..d070c96c 100644 --- a/src/style/_layout.scss +++ b/src/style/_layout.scss @@ -775,6 +775,17 @@ .response-content-type { padding-top: 1em; + + &.controls-accept-header { + select { + border-color: green; + } + + small { + color: green; + font-size: .7em; + } + } } @keyframes blinker diff --git a/test/core/utils.js b/test/core/utils.js index c9921ef7..b4be6c1b 100644 --- a/test/core/utils.js +++ b/test/core/utils.js @@ -1,7 +1,7 @@ /* eslint-env mocha */ import expect from "expect" -import { fromJS } from "immutable" -import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered, createDeepLinkPath, escapeDeepLinkPath } from "core/utils" +import { fromJS, OrderedMap } from "immutable" +import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered, getAcceptControllingResponse, createDeepLinkPath, escapeDeepLinkPath } from "core/utils" import win from "core/window" describe("utils", function() { @@ -583,6 +583,103 @@ describe("utils", function() { }) }) + 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) + }) + }) + describe("createDeepLinkPath", function() { it("creates a deep link path replacing spaces with underscores", function() { const result = createDeepLinkPath("tag id with spaces")