refactor(oas31): simplify Webhooks component by utilizing selectors (#8481)

Refs #8474
This commit is contained in:
Vladimír Gorej
2023-03-17 18:14:23 +01:00
committed by GitHub
parent 865d98d6be
commit 47e12f1de3
11 changed files with 126 additions and 74 deletions

View File

@@ -126,6 +126,7 @@ export default class BaseLayout extends React.Component {
<Operations /> <Operations />
</Col> </Col>
</Row> </Row>
{isOAS31 && ( {isOAS31 && (
<Row className="webhooks-container"> <Row className="webhooks-container">
<Col mobile={12} desktop={12}> <Col mobile={12} desktop={12}>
@@ -133,6 +134,7 @@ export default class BaseLayout extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
<Row> <Row>
<Col mobile={12} desktop={12}> <Col mobile={12} desktop={12}>
<Models /> <Models />

View File

@@ -2,13 +2,6 @@ import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import Im from "immutable" 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 { export default class Operations extends React.Component {
static propTypes = { static propTypes = {
@@ -53,6 +46,7 @@ export default class Operations extends React.Component {
layoutActions, layoutActions,
getConfigs, getConfigs,
} = this.props } = this.props
const validOperationMethods = specSelectors.validOperationMethods()
const OperationContainer = getComponent("OperationContainer", true) const OperationContainer = getComponent("OperationContainer", true)
const OperationTag = getComponent("OperationTag") const OperationTag = getComponent("OperationTag")
const operations = tagObj.get("operations") const operations = tagObj.get("operations")
@@ -74,16 +68,7 @@ export default class Operations extends React.Component {
const method = op.get("method") const method = op.get("method")
const specPath = Im.List(["paths", path, method]) const specPath = Im.List(["paths", path, method])
if (validOperationMethods.indexOf(method) === -1) {
// 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) {
return null return null
} }

View File

@@ -2,6 +2,8 @@
* @prettier * @prettier
*/ */
import { OrderedMap, Map, List } from "immutable" import { OrderedMap, Map, List } from "immutable"
import { createSelector } from "reselect"
import { getDefaultRequestBodyValue } from "./components/request-body" import { getDefaultRequestBodyValue } from "./components/request-body"
import { stringify } from "../../utils" import { stringify } from "../../utils"
@@ -279,3 +281,14 @@ export const validateShallowRequired = (
}) })
return missingRequiredKeys return missingRequiredKeys
} }
export const validOperationMethods = createSelector(() => [
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace",
])

View File

@@ -16,7 +16,7 @@ function onlyOAS3(selector) {
(...args) => { (...args) => {
if (system.getSystem().specSelectors.isOAS3()) { if (system.getSystem().specSelectors.isOAS3()) {
const result = selector(...args) const result = selector(...args)
return typeof result === "function" ? result(system, ...args) : result return typeof result === "function" ? result(system) : result
} else { } else {
return ori(...args) 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 host = OAS3NullSelector
export const basePath = OAS3NullSelector export const basePath = OAS3NullSelector
export const consumes = OAS3NullSelector export const consumes = OAS3NullSelector

View File

@@ -1,59 +1,45 @@
/**
* @prettier
*/
import React from "react" import React from "react"
import PropTypes from "prop-types" 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 const Webhooks = ({ specSelectors, getComponent }) => {
// to show/hide all webhook items const operationDTOs = specSelectors.selectWebhooksOperations()
const Webhooks = (props) => { const pathItemNames = Object.keys(operationDTOs)
const { specSelectors, getComponent, specPath } = props
const webhooksPathItems = specSelectors.webhooks()
if (!webhooksPathItems || webhooksPathItems?.size < 1) {
return null
}
const OperationContainer = getComponent("OperationContainer", true) const OperationContainer = getComponent("OperationContainer", true)
const pathItemsElements = webhooksPathItems.entrySeq().map(([pathItemName, pathItem], i) => { if (pathItemNames.length === 0) return null
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 <OperationContainer
{...props}
op={op}
key={`${pathItemName}--${operationMethod}--${j}`}
tag={""}
method={operationMethod}
path={pathItemName}
specPath={specPath.push("webhooks", pathItemName, operationMethod)}
allowTryItOut={false}
/>
})
return <div key={`${pathItemName}-${i}`}>
{operationsElements}
</div>
})
return ( return (
<div className="webhooks"> <div className="webhooks">
<h2>Webhooks</h2> <h2>Webhooks</h2>
{pathItemsElements}
{pathItemNames.map((pathItemName) => (
<div key={`${pathItemName}-webhook`}>
{operationDTOs[pathItemName].map((operationDTO) => (
<OperationContainer
key={`${pathItemName}-${operationDTO.method}-webhook`}
op={operationDTO.operation}
tag=""
method={operationDTO.method}
path={pathItemName}
specPath={operationDTO.specPath}
allowTryItOut={false}
/>
))}
</div>
))}
</div> </div>
) )
} }
Webhooks.propTypes = { Webhooks.propTypes = {
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.shape({
selectWebhooksOperations: PropTypes.func.isRequired,
}).isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
specPath: ImPropTypes.list,
}
Webhooks.defaultProps = {
specPath: fromJS([])
} }
export default Webhooks export default Webhooks

View File

@@ -1,6 +1,7 @@
/** /**
* @prettier * @prettier
*/ */
export const isOAS31 = (jsSpec) => { export const isOAS31 = (jsSpec) => {
const oasVersion = jsSpec.get("openapi") 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 = export const onlyOAS31 =
(selector) => (selector) =>
() => (state, ...args) =>
(system, ...args) => { (system) => {
if (system.getSystem().specSelectors.isOAS31()) { if (system.getSystem().specSelectors.isOAS31()) {
const result = selector(...args) const result = selector(state, ...args)
return typeof result === "function" ? result(system, ...args) : result return typeof result === "function" ? result(system) : result
} else { } else {
return null 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)
}
}

View File

@@ -25,10 +25,11 @@ import {
selectInfoSummaryField, selectInfoSummaryField,
selectInfoDescriptionField, selectInfoDescriptionField,
selectInfoTermsOfServiceField, selectInfoTermsOfServiceField,
makeSelectInfoTermsOfServiceUrl as makeSelectTosUrl, makeSelectInfoTermsOfServiceUrl,
selectExternalDocsDescriptionField, selectExternalDocsDescriptionField,
selectExternalDocsUrlField, selectExternalDocsUrlField,
makeSelectExternalDocsUrl, makeSelectExternalDocsUrl,
makeSelectWebhooksOperations,
} from "./spec-extensions/selectors" } from "./spec-extensions/selectors"
import { import {
isOAS3 as isOAS3Wrapper, isOAS3 as isOAS3Wrapper,
@@ -45,8 +46,9 @@ const OAS31Plugin = () => {
specSelectors.isOAS31 = makeIsOAS31(system) specSelectors.isOAS31 = makeIsOAS31(system)
specSelectors.selectLicenseUrl = makeSelectLicenseUrl(system) specSelectors.selectLicenseUrl = makeSelectLicenseUrl(system)
specSelectors.selectContactUrl = makeSelectContactUrl(system) specSelectors.selectContactUrl = makeSelectContactUrl(system)
specSelectors.selectInfoTermsOfServiceUrl = makeSelectTosUrl(system) specSelectors.selectInfoTermsOfServiceUrl = makeSelectInfoTermsOfServiceUrl(system) // prettier-ignore
specSelectors.selectExternalDocsUrl = makeSelectExternalDocsUrl(system) specSelectors.selectExternalDocsUrl = makeSelectExternalDocsUrl(system)
specSelectors.selectWebhooksOperations = makeSelectWebhooksOperations(system) // prettier-ignore
oas31Selectors.selectLicenseUrl = makeOAS31SelectLicenseUrl(system) oas31Selectors.selectLicenseUrl = makeOAS31SelectLicenseUrl(system)
}, },

View File

@@ -1,7 +1,7 @@
/** /**
* @prettier * @prettier
*/ */
import { Map } from "immutable" import { List, Map } from "immutable"
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { safeBuildUrl } from "core/utils/url" import { safeBuildUrl } from "core/utils/url"
@@ -16,6 +16,39 @@ export const webhooks = onlyOAS31(() => (system) => {
return system.specSelectors.specJson().get("webhooks", map) 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) => { export const license = () => (system) => {
return system.specSelectors.info().get("license", map) return system.specSelectors.info().get("license", map)
} }

View File

@@ -1,6 +1,8 @@
/** /**
* @prettier * @prettier
*/ */
import { onlyOAS31Wrap } from "../helpers"
export const isOAS3 = export const isOAS3 =
(oriSelector, system) => (oriSelector, system) =>
(state, ...args) => { (state, ...args) => {
@@ -8,12 +10,6 @@ export const isOAS3 =
return isOAS31 || oriSelector(...args) return isOAS31 || oriSelector(...args)
} }
export const selectLicenseUrl = export const selectLicenseUrl = onlyOAS31Wrap(() => (system) => {
(oriSelector, system) => return system.oas31Selectors.selectLicenseUrl()
(state, ...args) => { })
if (system.specSelectors.isOAS31()) {
return system.oas31Selectors.selectLicenseUrl()
}
return oriSelector(...args)
}

View File

@@ -114,6 +114,8 @@ export const paths = createSelector(
spec => spec.get("paths") spec => spec.get("paths")
) )
export const validOperationMethods = createSelector(() => ["get", "put", "post", "delete", "options", "head", "patch"])
export const operations = createSelector( export const operations = createSelector(
paths, paths,
paths => { paths => {

View File

@@ -28,6 +28,7 @@ describe("<Operations/>", function(){
specSelectors: { specSelectors: {
isOAS3() { return false }, isOAS3() { return false },
url() { return "https://petstore.swagger.io/v2/swagger.json" }, url() { return "https://petstore.swagger.io/v2/swagger.json" },
validOperationMethods() { return ["get", "put", "post", "delete", "options", "head", "patch"] },
taggedOperations() { taggedOperations() {
return fromJS({ return fromJS({
"default": { "default": {
@@ -83,6 +84,7 @@ describe("<Operations/>", function(){
specSelectors: { specSelectors: {
isOAS3() { return true }, isOAS3() { return true },
url() { return "https://petstore.swagger.io/v2/swagger.json" }, url() { return "https://petstore.swagger.io/v2/swagger.json" },
validOperationMethods() { return ["get", "put", "post", "delete", "options", "head", "patch", "trace"] },
taggedOperations() { taggedOperations() {
return fromJS({ return fromJS({
"default": { "default": {