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

View File

@@ -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
}

View File

@@ -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",
])

View File

@@ -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

View File

@@ -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 <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>
})
if (pathItemNames.length === 0) return null
return (
<div className="webhooks">
<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>
)
}
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

View File

@@ -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)
}
}

View File

@@ -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)
},

View File

@@ -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)
}

View File

@@ -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()
})

View File

@@ -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 => {