Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/components/layouts/base.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,15 @@ export default class BaseLayout extends React.Component {
<Operations />
</Col>
</Row>

{isOAS31 && (
<Row className="webhooks-container">
<Col mobile={12} desktop={12}>
<Webhooks />
</Col>
</Row>
)}

<Row>
<Col mobile={12} desktop={12}>
<Models />
Expand Down
19 changes: 2 additions & 17 deletions src/core/components/operations.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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")
Expand All @@ -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
}

Expand Down
13 changes: 13 additions & 0 deletions src/core/plugins/oas3/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -279,3 +281,14 @@ export const validateShallowRequired = (
})
return missingRequiredKeys
}

export const validOperationMethods = createSelector(() => [
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace",
])
12 changes: 11 additions & 1 deletion src/core/plugins/oas3/spec-extensions/wrap-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
66 changes: 26 additions & 40 deletions src/core/plugins/oas31/components/webhooks.jsx
Original file line number Diff line number Diff line change
@@ -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
29 changes: 25 additions & 4 deletions src/core/plugins/oas31/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @prettier
*/

export const isOAS31 = (jsSpec) => {
const oasVersion = jsSpec.get("openapi")

Expand All @@ -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)
}
}
6 changes: 4 additions & 2 deletions src/core/plugins/oas31/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import {
selectInfoSummaryField,
selectInfoDescriptionField,
selectInfoTermsOfServiceField,
makeSelectInfoTermsOfServiceUrl as makeSelectTosUrl,
makeSelectInfoTermsOfServiceUrl,
selectExternalDocsDescriptionField,
selectExternalDocsUrlField,
makeSelectExternalDocsUrl,
makeSelectWebhooksOperations,
} from "./spec-extensions/selectors"
import {
isOAS3 as isOAS3Wrapper,
Expand All @@ -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)
},
Expand Down
35 changes: 34 additions & 1 deletion src/core/plugins/oas31/spec-extensions/selectors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @prettier
*/
import { Map } from "immutable"
import { List, Map } from "immutable"
import { createSelector } from "reselect"

import { safeBuildUrl } from "core/utils/url"
Expand All @@ -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)
}
Expand Down
14 changes: 5 additions & 9 deletions src/core/plugins/oas31/spec-extensions/wrap-selectors.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
/**
* @prettier
*/
import { onlyOAS31Wrap } from "../helpers"

export const isOAS3 =
(oriSelector, system) =>
(state, ...args) => {
const isOAS31 = system.specSelectors.isOAS31()
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()
})
2 changes: 2 additions & 0 deletions src/core/plugins/spec/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
2 changes: 2 additions & 0 deletions test/unit/components/operations.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe("<Operations/>", 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": {
Expand Down Expand Up @@ -83,6 +84,7 @@ describe("<Operations/>", 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": {
Expand Down