From 26edaa5f0bccdfe293ac7bda770835a39bd42331 Mon Sep 17 00:00:00 2001 From: Kyle Shockey Date: Tue, 12 Sep 2017 17:21:15 -0700 Subject: [PATCH 1/5] OAS3 Accept header control: Component-side --- package.json | 1 + src/core/components/response.jsx | 24 ++++--- src/core/components/responses.jsx | 20 +++++- src/core/utils.js | 23 +++++++ src/style/_layout.scss | 11 ++++ test/core/utils.js | 101 +++++++++++++++++++++++++++++- 6 files changed, 169 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 709752ff..77f14ab7 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", "deep-extend": "0.4.1", "expect": "1.20.2", "getbase": "^2.8.2", diff --git a/src/core/components/response.jsx b/src/core/components/response.jsx index 02662f90..b2507a2e 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,7 +47,8 @@ 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 } static defaultProps = { @@ -61,7 +63,8 @@ export default class Response extends React.Component { fn, getComponent, specSelectors, - contentType + contentType, + controlsAcceptHeader } = this.props let { inferSchema } = fn @@ -106,11 +109,18 @@ export default class Response extends React.Component { - { isOAS3 ? this.setState({ responseContentType: val })} - className="response-content-type" /> : null } + { isOAS3 ? +
+ this.setState({ responseContentType: val })} + /> + { controlsAcceptHeader ? Controls Accept header. : null } +
+ : null } { example ? ( this.props.specActions.changeProducesValue(this.props.pathMethod, val) 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 +49,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 (
@@ -88,6 +103,7 @@ export default class Responses extends React.Component { code={ code } response={ response } specSelectors={ specSelectors } + controlsAcceptHeader={response === acceptControllingResponse} contentType={ producesValue } getComponent={ getComponent }/> ) diff --git a/src/core/utils.js b/src/core/utils.js index 743c4749..99cb9525 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -650,3 +650,26 @@ export const shallowEqualKeys = (a,b, keys) => { 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 +} 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 55dfeace..58e85241 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 } from "core/utils" +import { fromJS, OrderedMap } from "immutable" +import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered, getAcceptControllingResponse } from "core/utils" import win from "core/window" describe("utils", function() { @@ -581,5 +581,102 @@ describe("utils", function() { const result = fromJSOrdered(param).toJS() 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) + }) + }) }) From 277cc81a8c0535065052238d745bfd00cebdec39 Mon Sep 17 00:00:00 2001 From: Kyle Shockey Date: Tue, 12 Sep 2017 19:39:52 -0700 Subject: [PATCH 2/5] OAS3 Accept header control: State-side --- src/core/components/operation.jsx | 4 +++- src/core/components/operations.jsx | 4 ++++ src/core/components/response.jsx | 15 +++++++++++++-- src/core/components/responses.jsx | 12 +++++++++++- src/core/plugins/oas3/actions.js | 8 ++++++++ src/core/plugins/oas3/reducers.js | 7 ++++++- src/core/plugins/oas3/selectors.js | 5 +++++ src/core/plugins/spec/actions.js | 1 + 8 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index eb21b8db..b9a0bd22 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -117,7 +117,8 @@ export default class Operation extends PureComponent { specSelectors, authActions, authSelectors, - getConfigs + getConfigs, + oas3Actions } = this.props let summary = operation.get("summary") @@ -265,6 +266,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 2aec664a..4a0ea3ac 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 b2507a2e..8aad5109 100644 --- a/src/core/components/response.jsx +++ b/src/core/components/response.jsx @@ -48,13 +48,24 @@ export default class Response extends React.Component { specSelectors: PropTypes.object.isRequired, fn: PropTypes.object.isRequired, contentType: PropTypes.string, - controlsAcceptHeader: PropTypes.bool + 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, @@ -116,7 +127,7 @@ export default class Response extends React.Component { this.setState({ responseContentType: val })} + onChange={this._onContentTypeChange} /> { controlsAcceptHeader ? Controls Accept header. : null }
diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index 1f43f27f..a6ea4a34 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -29,6 +29,16 @@ export default class Responses extends React.Component { onChangeProducesWrapper = ( val ) => 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, @@ -93,7 +103,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)) { From ef8b37a58016a768be479f30f9d1eb6dceeeaedb Mon Sep 17 00:00:00 2001 From: Kyle Shockey Date: Fri, 15 Sep 2017 20:17:35 -0700 Subject: [PATCH 3/5] LINTING! --- src/core/components/operation.jsx | 1 + src/core/components/responses.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index b9a0bd22..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, diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index a6ea4a34..5dce1d9f 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -14,6 +14,7 @@ export default class Responses extends React.Component { getComponent: PropTypes.func.isRequired, specSelectors: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired, + oas3Actions: PropTypes.object.isRequired, pathMethod: PropTypes.array.isRequired, displayRequestDuration: PropTypes.bool.isRequired, fn: PropTypes.object.isRequired, From 60102452104849eef4f90bf15ab038ad7309495b Mon Sep 17 00:00:00 2001 From: Kyle Shockey Date: Fri, 15 Sep 2017 20:29:49 -0700 Subject: [PATCH 4/5] Roll back win import removal Lost in merge conflict.... --- test/core/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/utils.js b/test/core/utils.js index 38b42a27..b4be6c1b 100644 --- a/test/core/utils.js +++ b/test/core/utils.js @@ -2,7 +2,7 @@ import expect from "expect" 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() { From ae5ad484d9368cc2ba8f37beb330e4cee0655852 Mon Sep 17 00:00:00 2001 From: Kyle Shockey Date: Fri, 15 Sep 2017 20:33:16 -0700 Subject: [PATCH 5/5] More merge oversights... --- src/core/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/utils.js b/src/core/utils.js index fbfd3155..1a835d3f 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -675,5 +675,5 @@ export function getAcceptControllingResponse(responses) { return suitable2xxResponse || suitableDefaultResponse } -export const createDeepLinkPath = (str) => str ? str.replace(/\s/g, "_") : "" +export const createDeepLinkPath = (str) => typeof str == "string" || str instanceof String ? str.trim().replace(/\s/g, "_") : "" export const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str) )