From 47e12f1de3537896aeaaa52cd445b9004e463f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Fri, 17 Mar 2023 18:14:23 +0100 Subject: [PATCH] refactor(oas31): simplify Webhooks component by utilizing selectors (#8481) Refs #8474 --- src/core/components/layouts/base.jsx | 2 + src/core/components/operations.jsx | 19 +----- src/core/plugins/oas3/selectors.js | 13 ++++ .../oas3/spec-extensions/wrap-selectors.js | 12 +++- .../plugins/oas31/components/webhooks.jsx | 66 ++++++++----------- src/core/plugins/oas31/helpers.js | 29 ++++++-- src/core/plugins/oas31/index.js | 6 +- .../oas31/spec-extensions/selectors.js | 35 +++++++++- .../oas31/spec-extensions/wrap-selectors.js | 14 ++-- src/core/plugins/spec/selectors.js | 2 + test/unit/components/operations.jsx | 2 + 11 files changed, 126 insertions(+), 74 deletions(-) diff --git a/src/core/components/layouts/base.jsx b/src/core/components/layouts/base.jsx index 2df9edbe..8208b6a4 100644 --- a/src/core/components/layouts/base.jsx +++ b/src/core/components/layouts/base.jsx @@ -126,6 +126,7 @@ export default class BaseLayout extends React.Component { + {isOAS31 && ( @@ -133,6 +134,7 @@ export default class BaseLayout extends React.Component { )} + diff --git a/src/core/components/operations.jsx b/src/core/components/operations.jsx index d5de287d..ee9c02a8 100644 --- a/src/core/components/operations.jsx +++ b/src/core/components/operations.jsx @@ -2,13 +2,6 @@ import React from "react" import PropTypes from "prop-types" import Im from "immutable" -const SWAGGER2_OPERATION_METHODS = [ - "get", "put", "post", "delete", "options", "head", "patch" -] - -const OAS3_OPERATION_METHODS = SWAGGER2_OPERATION_METHODS.concat(["trace"]) - - export default class Operations extends React.Component { static propTypes = { @@ -53,6 +46,7 @@ export default class Operations extends React.Component { layoutActions, getConfigs, } = this.props + const validOperationMethods = specSelectors.validOperationMethods() const OperationContainer = getComponent("OperationContainer", true) const OperationTag = getComponent("OperationTag") const operations = tagObj.get("operations") @@ -74,16 +68,7 @@ export default class Operations extends React.Component { const method = op.get("method") const specPath = Im.List(["paths", path, method]) - - // FIXME: (someday) this logic should probably be in a selector, - // but doing so would require further opening up - // selectors to the plugin system, to allow for dynamic - // overriding of low-level selectors that other selectors - // rely on. --KS, 12/17 - const validMethods = specSelectors.isOAS3() ? - OAS3_OPERATION_METHODS : SWAGGER2_OPERATION_METHODS - - if (validMethods.indexOf(method) === -1) { + if (validOperationMethods.indexOf(method) === -1) { return null } diff --git a/src/core/plugins/oas3/selectors.js b/src/core/plugins/oas3/selectors.js index ba2e7d2e..d401abd7 100644 --- a/src/core/plugins/oas3/selectors.js +++ b/src/core/plugins/oas3/selectors.js @@ -2,6 +2,8 @@ * @prettier */ import { OrderedMap, Map, List } from "immutable" +import { createSelector } from "reselect" + import { getDefaultRequestBodyValue } from "./components/request-body" import { stringify } from "../../utils" @@ -279,3 +281,14 @@ export const validateShallowRequired = ( }) return missingRequiredKeys } + +export const validOperationMethods = createSelector(() => [ + "get", + "put", + "post", + "delete", + "options", + "head", + "patch", + "trace", +]) diff --git a/src/core/plugins/oas3/spec-extensions/wrap-selectors.js b/src/core/plugins/oas3/spec-extensions/wrap-selectors.js index 231d6c32..d6ba9d8d 100644 --- a/src/core/plugins/oas3/spec-extensions/wrap-selectors.js +++ b/src/core/plugins/oas3/spec-extensions/wrap-selectors.js @@ -16,7 +16,7 @@ function onlyOAS3(selector) { (...args) => { if (system.getSystem().specSelectors.isOAS3()) { const result = selector(...args) - return typeof result === "function" ? result(system, ...args) : result + return typeof result === "function" ? result(system) : result } else { return ori(...args) } @@ -49,6 +49,16 @@ export const securityDefinitions = onlyOAS3( ) ) +export const validOperationMethods = + (oriSelector, system) => + (state, ...args) => { + if (system.specSelectors.isOAS3()) { + return system.oas3Selectors.validOperationMethods() + } + + return oriSelector(...args) + } + export const host = OAS3NullSelector export const basePath = OAS3NullSelector export const consumes = OAS3NullSelector diff --git a/src/core/plugins/oas31/components/webhooks.jsx b/src/core/plugins/oas31/components/webhooks.jsx index b4e7f7cc..539b126b 100644 --- a/src/core/plugins/oas31/components/webhooks.jsx +++ b/src/core/plugins/oas31/components/webhooks.jsx @@ -1,59 +1,45 @@ +/** + * @prettier + */ import React from "react" import PropTypes from "prop-types" -import { fromJS } from "immutable" -import ImPropTypes from "react-immutable-proptypes" -// Todo: nice to have: similar to operation-tags, could have an expand/collapse button -// to show/hide all webhook items -const Webhooks = (props) => { - const { specSelectors, getComponent, specPath } = props +const Webhooks = ({ specSelectors, getComponent }) => { + const operationDTOs = specSelectors.selectWebhooksOperations() + const pathItemNames = Object.keys(operationDTOs) - const webhooksPathItems = specSelectors.webhooks() - if (!webhooksPathItems || webhooksPathItems?.size < 1) { - return null - } const OperationContainer = getComponent("OperationContainer", true) - const pathItemsElements = webhooksPathItems.entrySeq().map(([pathItemName, pathItem], i) => { - const operationsElements = pathItem.entrySeq().map(([operationMethod, operation], j) => { - const op = fromJS({ - operation - }) - // using defaultProps for `specPath`; may want to remove from props - // and/or if extract to separate PathItem component, allow for use - // with both OAS3.1 "webhooks" and "components.pathItems" features - return - }) - return
- {operationsElements} -
- }) + if (pathItemNames.length === 0) return null return (

Webhooks

- {pathItemsElements} + + {pathItemNames.map((pathItemName) => ( +
+ {operationDTOs[pathItemName].map((operationDTO) => ( + + ))} +
+ ))}
) } Webhooks.propTypes = { - specSelectors: PropTypes.object.isRequired, + specSelectors: PropTypes.shape({ + selectWebhooksOperations: PropTypes.func.isRequired, + }).isRequired, getComponent: PropTypes.func.isRequired, - specPath: ImPropTypes.list, -} - -Webhooks.defaultProps = { - specPath: fromJS([]) } export default Webhooks diff --git a/src/core/plugins/oas31/helpers.js b/src/core/plugins/oas31/helpers.js index ac7fd3e3..c9357310 100644 --- a/src/core/plugins/oas31/helpers.js +++ b/src/core/plugins/oas31/helpers.js @@ -1,6 +1,7 @@ /** * @prettier */ + export const isOAS31 = (jsSpec) => { const oasVersion = jsSpec.get("openapi") @@ -9,14 +10,34 @@ export const isOAS31 = (jsSpec) => { ) } +/** + * Selector maker the only calls the passed selector + * when spec is of OpenAPI 3.1.0 version. + */ export const onlyOAS31 = (selector) => - () => - (system, ...args) => { + (state, ...args) => + (system) => { if (system.getSystem().specSelectors.isOAS31()) { - const result = selector(...args) - return typeof result === "function" ? result(system, ...args) : result + const result = selector(state, ...args) + return typeof result === "function" ? result(system) : result } else { return null } } + +/** + * Selector wrapper maker the only wraps the passed selector + * when spec is of OpenAPI 3.1.0 version. + */ +export const onlyOAS31Wrap = + (selector) => + (oriSelector, system) => + (state, ...args) => { + if (system.getSystem().specSelectors.isOAS31()) { + const result = selector(state, ...args) + return typeof result === "function" ? result(system) : result + } else { + return oriSelector(...args) + } + } diff --git a/src/core/plugins/oas31/index.js b/src/core/plugins/oas31/index.js index d01378cd..d20f7116 100644 --- a/src/core/plugins/oas31/index.js +++ b/src/core/plugins/oas31/index.js @@ -25,10 +25,11 @@ import { selectInfoSummaryField, selectInfoDescriptionField, selectInfoTermsOfServiceField, - makeSelectInfoTermsOfServiceUrl as makeSelectTosUrl, + makeSelectInfoTermsOfServiceUrl, selectExternalDocsDescriptionField, selectExternalDocsUrlField, makeSelectExternalDocsUrl, + makeSelectWebhooksOperations, } from "./spec-extensions/selectors" import { isOAS3 as isOAS3Wrapper, @@ -45,8 +46,9 @@ const OAS31Plugin = () => { specSelectors.isOAS31 = makeIsOAS31(system) specSelectors.selectLicenseUrl = makeSelectLicenseUrl(system) specSelectors.selectContactUrl = makeSelectContactUrl(system) - specSelectors.selectInfoTermsOfServiceUrl = makeSelectTosUrl(system) + specSelectors.selectInfoTermsOfServiceUrl = makeSelectInfoTermsOfServiceUrl(system) // prettier-ignore specSelectors.selectExternalDocsUrl = makeSelectExternalDocsUrl(system) + specSelectors.selectWebhooksOperations = makeSelectWebhooksOperations(system) // prettier-ignore oas31Selectors.selectLicenseUrl = makeOAS31SelectLicenseUrl(system) }, diff --git a/src/core/plugins/oas31/spec-extensions/selectors.js b/src/core/plugins/oas31/spec-extensions/selectors.js index 363cc958..acf2092b 100644 --- a/src/core/plugins/oas31/spec-extensions/selectors.js +++ b/src/core/plugins/oas31/spec-extensions/selectors.js @@ -1,7 +1,7 @@ /** * @prettier */ -import { Map } from "immutable" +import { List, Map } from "immutable" import { createSelector } from "reselect" import { safeBuildUrl } from "core/utils/url" @@ -16,6 +16,39 @@ export const webhooks = onlyOAS31(() => (system) => { return system.specSelectors.specJson().get("webhooks", map) }) +/** + * `specResolvedSubtree` selector is needed as input selector, + * so that we regenerate the selected result whenever the lazy + * resolution happens. + */ +export const makeSelectWebhooksOperations = (system) => + onlyOAS31( + createSelector( + () => system.specSelectors.webhooks(), + () => system.specSelectors.validOperationMethods(), + () => system.specSelectors.specResolvedSubtree(["webhooks"]), + (webhooks, validOperationMethods) => { + return webhooks + .reduce((allOperations, pathItem, pathItemName) => { + const pathItemOperations = pathItem + .entrySeq() + .filter(([key]) => validOperationMethods.includes(key)) + .map(([method, operation]) => ({ + operation: Map({ operation }), + method, + path: pathItemName, + specPath: List(["webhooks", pathItemName, method]), + })) + + return allOperations.concat(pathItemOperations) + }, List()) + .groupBy((operation) => operation.path) + .map((operations) => operations.toArray()) + .toObject() + } + ) + ) + export const license = () => (system) => { return system.specSelectors.info().get("license", map) } diff --git a/src/core/plugins/oas31/spec-extensions/wrap-selectors.js b/src/core/plugins/oas31/spec-extensions/wrap-selectors.js index af207436..0631d49c 100644 --- a/src/core/plugins/oas31/spec-extensions/wrap-selectors.js +++ b/src/core/plugins/oas31/spec-extensions/wrap-selectors.js @@ -1,6 +1,8 @@ /** * @prettier */ +import { onlyOAS31Wrap } from "../helpers" + export const isOAS3 = (oriSelector, system) => (state, ...args) => { @@ -8,12 +10,6 @@ export const isOAS3 = return isOAS31 || oriSelector(...args) } -export const selectLicenseUrl = - (oriSelector, system) => - (state, ...args) => { - if (system.specSelectors.isOAS31()) { - return system.oas31Selectors.selectLicenseUrl() - } - - return oriSelector(...args) - } +export const selectLicenseUrl = onlyOAS31Wrap(() => (system) => { + return system.oas31Selectors.selectLicenseUrl() +}) diff --git a/src/core/plugins/spec/selectors.js b/src/core/plugins/spec/selectors.js index 97a4788b..2647fef6 100644 --- a/src/core/plugins/spec/selectors.js +++ b/src/core/plugins/spec/selectors.js @@ -114,6 +114,8 @@ export const paths = createSelector( spec => spec.get("paths") ) +export const validOperationMethods = createSelector(() => ["get", "put", "post", "delete", "options", "head", "patch"]) + export const operations = createSelector( paths, paths => { diff --git a/test/unit/components/operations.jsx b/test/unit/components/operations.jsx index d8295aa3..4afeb61f 100644 --- a/test/unit/components/operations.jsx +++ b/test/unit/components/operations.jsx @@ -28,6 +28,7 @@ describe("", function(){ specSelectors: { isOAS3() { return false }, url() { return "https://petstore.swagger.io/v2/swagger.json" }, + validOperationMethods() { return ["get", "put", "post", "delete", "options", "head", "patch"] }, taggedOperations() { return fromJS({ "default": { @@ -83,6 +84,7 @@ describe("", function(){ specSelectors: { isOAS3() { return true }, url() { return "https://petstore.swagger.io/v2/swagger.json" }, + validOperationMethods() { return ["get", "put", "post", "delete", "options", "head", "patch", "trace"] }, taggedOperations() { return fromJS({ "default": {