Skip to content

Commit 4557b24

Browse files
authored
feat: OpenAPI 3.1 support (#8367)
- New top-level field - `webhooks`. This allows describing out-of-band webhooks that are available as part of the API. - New top-level field - `jsonSchemaDialect`. This allows defining of a default `$schema` value for Schema Objects - The Info Object has a new `summary` field. - The License Object now has a new `identifier` field for SPDX licenses. This `identifier` field is mutually exclusive with the `url` field. Either can be used in OpenAPI 3.1 definitions. - Components Object now has a new entry `pathItems`, to allow for reusable Path Item Objects to be defined within a valid OpenAPI document. - `License` and `Contact` components are now exported and available via `getComponent` - New version predicates and selectors for `isOpenAPI30` and `isOpenAPI31`. This avoids needing to change the usage of `isOAS3` selector. - New OAS3 components: `Webhooks` - New OAS3 wrapped components: `Info`, `License`
1 parent f3c6a25 commit 4557b24

30 files changed

+1564
-164
lines changed

package-lock.json

Lines changed: 927 additions & 138 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"deps-license": "license-checker --production --csv --out $npm_package_config_deps_check_dir/licenses.csv && license-checker --development --csv --out $npm_package_config_deps_check_dir/licenses-dev.csv",
4141
"deps-size": "webpack -p --config webpack/bundle.babel.js --json | webpack-bundle-size-analyzer >| $npm_package_config_deps_check_dir/sizes.txt",
4242
"deps-check": "run-s deps-license deps-size",
43+
"link:apidom": "npm link @swagger-api/apidom-core @swagger-api/apidom-reference @swagger-api/apidom-ns-openapi-3-1 @swagger-api/apidom-ns-openapi-3-0 @swagger-api/apidom-ns-json-schema-draft-4 @swagger-api/apidom-json-pointer",
4344
"lint": "eslint --ext \".js,.jsx\" src test dev-helpers flavors",
4445
"lint-errors": "eslint --quiet --ext \".js,.jsx\" src test dev-helpers flavors",
4546
"lint-fix": "eslint --ext \".js,.jsx\" src test dev-helpers flavors --fix",
@@ -94,7 +95,7 @@
9495
"reselect": "^4.1.5",
9596
"serialize-error": "^8.1.0",
9697
"sha.js": "^2.4.11",
97-
"swagger-client": "^3.18.5",
98+
"swagger-client": "=3.19.0-alpha.4",
9899
"url-parse": "^1.5.8",
99100
"xml": "=1.0.1",
100101
"xml-but-prettier": "^1.0.1",
@@ -122,7 +123,7 @@
122123
"autoprefixer": "^10.4.12",
123124
"babel-loader": "^8.2.3",
124125
"babel-plugin-lodash": "=3.3.4",
125-
"babel-plugin-module-resolver": "=4.1.0",
126+
"babel-plugin-module-resolver": "=5.0.0",
126127
"babel-plugin-transform-react-remove-prop-types": "=0.4.24",
127128
"body-parser": "^1.19.0",
128129
"buffer": "^6.0.3",
@@ -187,6 +188,13 @@
187188
"webpack-node-externals": "=3.0.0",
188189
"webpack-stats-plugin": "=1.0.3"
189190
},
191+
"overrides": {
192+
"swagger-client": {
193+
"@swagger-api/apidom-reference": {
194+
"axios": "npm:[email protected]"
195+
}
196+
}
197+
},
190198
"config": {
191199
"deps_check_dir": ".deps_check"
192200
}

src/core/components/info.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class InfoBasePath extends React.Component {
2323
}
2424

2525

26-
class Contact extends React.Component {
26+
export class Contact extends React.Component {
2727
static propTypes = {
2828
data: PropTypes.object,
2929
getComponent: PropTypes.func.isRequired,
@@ -53,7 +53,7 @@ class Contact extends React.Component {
5353
}
5454
}
5555

56-
class License extends React.Component {
56+
export class License extends React.Component {
5757
static propTypes = {
5858
license: PropTypes.object,
5959
getComponent: PropTypes.func.isRequired,
@@ -64,7 +64,6 @@ class License extends React.Component {
6464

6565
render(){
6666
let { license, getComponent, selectedServer, url: specUrl } = this.props
67-
6867
const Link = getComponent("Link")
6968
let name = license.get("name") || "License"
7069
let url = safeBuildUrl(license.get("url"), specUrl, {selectedServer})
@@ -125,6 +124,7 @@ export default class Info extends React.Component {
125124
const VersionStamp = getComponent("VersionStamp")
126125
const InfoUrl = getComponent("InfoUrl")
127126
const InfoBasePath = getComponent("InfoBasePath")
127+
const License = getComponent("License")
128128

129129
return (
130130
<div className="info">

src/core/components/layouts/base.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default class BaseLayout extends React.Component {
2020
let VersionPragmaFilter = getComponent("VersionPragmaFilter")
2121
let Operations = getComponent("operations", true)
2222
let Models = getComponent("Models", true)
23+
let Webhooks = getComponent("Webhooks", true)
2324
let Row = getComponent("Row")
2425
let Col = getComponent("Col")
2526
let Errors = getComponent("errors", true)
@@ -30,6 +31,7 @@ export default class BaseLayout extends React.Component {
3031
const FilterContainer = getComponent("FilterContainer", true)
3132
let isSwagger2 = specSelectors.isSwagger2()
3233
let isOAS3 = specSelectors.isOAS3()
34+
const isOpenAPI31 = specSelectors.selectIsOpenAPI31()
3335

3436
const isSpecEmpty = !specSelectors.specStr()
3537

@@ -112,6 +114,13 @@ export default class BaseLayout extends React.Component {
112114
<Operations/>
113115
</Col>
114116
</Row>
117+
{ isOpenAPI31 &&
118+
<Row className="webhooks-container">
119+
<Col mobile={12} desktop={12} >
120+
<Webhooks />
121+
</Col>
122+
</Row>
123+
}
115124
<Row>
116125
<Col mobile={12} desktop={12} >
117126
<Models/>

src/core/plugins/oas3/components/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ServersContainer from "./servers-container"
66
import RequestBodyEditor from "./request-body-editor"
77
import HttpAuth from "./http-auth"
88
import OperationServers from "./operation-servers"
9+
import Webhooks from "./webhooks"
910

1011
export default {
1112
Callbacks,
@@ -15,5 +16,6 @@ export default {
1516
ServersContainer,
1617
RequestBodyEditor,
1718
OperationServers,
18-
operationLink: OperationLink
19+
operationLink: OperationLink,
20+
Webhooks
1921
}

src/core/plugins/oas3/components/request-body.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { getCommonExtensions, getSampleSchema, stringify, isEmptyValue } from "c
66
import { getKnownSyntaxHighlighterLanguage } from "core/utils/jsonParse"
77

88
export const getDefaultRequestBodyValue = (requestBody, mediaType, activeExamplesKey) => {
9-
const mediaTypeValue = requestBody.getIn(["content", mediaType])
10-
const schema = mediaTypeValue.get("schema").toJS()
9+
const mediaTypeValue = requestBody?.getIn(["content", mediaType])
10+
const schema = mediaTypeValue?.get("schema").toJS()
1111

12-
const hasExamplesKey = mediaTypeValue.get("examples") !== undefined
13-
const exampleSchema = mediaTypeValue.get("example")
12+
const hasExamplesKey = mediaTypeValue?.get("examples") !== undefined
13+
const exampleSchema = mediaTypeValue?.get("example")
1414
const mediaTypeExample = hasExamplesKey
15-
? mediaTypeValue.getIn([
15+
? mediaTypeValue?.getIn([
1616
"examples",
1717
activeExamplesKey,
1818
"value"
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// OpenAPI 3.1 feature
2+
import React from "react"
3+
import PropTypes from "prop-types"
4+
import { fromJS } from "immutable"
5+
import ImPropTypes from "react-immutable-proptypes"
6+
7+
// Todo: nice to have: similar to operation-tags, could have an expand/collapse button
8+
// to show/hide all webhook items
9+
const Webhooks = (props) => {
10+
const { specSelectors, getComponent, specPath } = props
11+
12+
const webhooksPathItems = specSelectors.selectWebhooks() // OrderedMap
13+
if (!webhooksPathItems || webhooksPathItems?.size < 1) {
14+
return null
15+
}
16+
const OperationContainer = getComponent("OperationContainer", true)
17+
18+
const pathItemsElements = webhooksPathItems.entrySeq().map(([pathItemName, pathItem], i) => {
19+
const operationsElements = pathItem.entrySeq().map(([operationMethod, operation], j) => {
20+
const op = fromJS({
21+
operation
22+
})
23+
// using defaultProps for `specPath`; may want to remove from props
24+
// and/or if extract to separate PathItem component, allow for use
25+
// with both OAS3.1 "webhooks" and "components.pathItems" features
26+
return <OperationContainer
27+
{...props}
28+
op={op}
29+
key={`${pathItemName}--${operationMethod}--${j}`}
30+
tag={""}
31+
method={operationMethod}
32+
path={pathItemName}
33+
specPath={specPath.push("webhooks", pathItemName, operationMethod)}
34+
allowTryItOut={false}
35+
/>
36+
})
37+
return <div key={`${pathItemName}-${i}`}>
38+
{operationsElements}
39+
</div>
40+
})
41+
42+
return (
43+
<div className="webhooks">
44+
<h2>Webhooks</h2>
45+
{pathItemsElements}
46+
</div>
47+
)
48+
}
49+
50+
Webhooks.propTypes = {
51+
specSelectors: PropTypes.object.isRequired,
52+
getComponent: PropTypes.func.isRequired,
53+
specPath: ImPropTypes.list,
54+
}
55+
56+
Webhooks.defaultProps = {
57+
specPath: fromJS([])
58+
}
59+
60+
export default Webhooks

src/core/plugins/oas3/helpers.jsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import React from "react"
22

3-
export function isOAS3(jsSpec) {
3+
export function isOpenAPI30(jsSpec) {
44
const oasVersion = jsSpec.get("openapi")
5-
if(typeof oasVersion !== "string") {
5+
if (typeof oasVersion !== "string") {
66
return false
77
}
8+
return oasVersion.startsWith("3.0.") && oasVersion.length > 4
9+
}
810

9-
// we gate against `3.1` because we want to explicitly opt into supporting it
10-
// at some point in the future -- KS, 7/2018
11+
export function isOpenAPI31(jsSpec) {
12+
const oasVersion = jsSpec.get("openapi")
13+
if (typeof oasVersion !== "string") {
14+
return false
15+
}
16+
return oasVersion.startsWith("3.1.") && oasVersion.length > 4
17+
}
1118

12-
// starts with, but is not `3.0.` exactly
13-
return oasVersion.startsWith("3.0.") && oasVersion.length > 4
19+
export function isOAS3(jsSpec) {
20+
const oasVersion = jsSpec.get("openapi")
21+
if(typeof oasVersion !== "string") {
22+
return false
23+
}
24+
return isOpenAPI30(jsSpec) || isOpenAPI31(jsSpec)
1425
}
1526

1627
export function isSwagger2(jsSpec) {

src/core/plugins/oas3/spec-extensions/selectors.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { createSelector } from "reselect"
22
import { Map } from "immutable"
3-
import { isOAS3 as isOAS3Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
3+
import { isOAS3 as isOAS3Helper, isOpenAPI31 as isOpenAPI31Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
44

55

66
// Helpers
77

8+
// 1/2023: as of now, more accurately, isAnyOAS3
89
function onlyOAS3(selector) {
910
return () => (system, ...args) => {
1011
const spec = system.getSystem().specSelectors.specJson()
@@ -16,6 +17,17 @@ function onlyOAS3(selector) {
1617
}
1718
}
1819

20+
function isOpenAPI31(selector) {
21+
return () => (system, ...args) => {
22+
const spec = system.getSystem().specSelectors.specJson()
23+
if (isOpenAPI31Helper(spec)) {
24+
return selector(...args)
25+
} else {
26+
return null
27+
}
28+
}
29+
}
30+
1931
const state = state => {
2032
return state || Map()
2133
}
@@ -48,3 +60,13 @@ export const isSwagger2 = (ori, system) => () => {
4860
const spec = system.getSystem().specSelectors.specJson()
4961
return isSwagger2Helper(spec)
5062
}
63+
64+
export const selectIsOpenAPI31 = (ori, system) => () => {
65+
const spec = system.getSystem().specSelectors.specJson()
66+
return isOpenAPI31Helper(spec)
67+
}
68+
69+
export const selectWebhooks = isOpenAPI31(createSelector(
70+
spec,
71+
spec => spec.getIn(["webhooks"]) || Map()
72+
))

src/core/plugins/oas3/spec-extensions/wrap-selectors.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { createSelector } from "reselect"
22
import { specJsonWithResolvedSubtrees } from "../../spec/selectors"
33
import { Map } from "immutable"
4-
import { isOAS3 as isOAS3Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
4+
import { isOAS3 as isOAS3Helper, isOpenAPI31 as isOpenAPI31Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
55

66

77
// Helpers
8-
8+
// 1/2023: as of now, more accurately, isAnyOAS3
99
function onlyOAS3(selector) {
1010
return (ori, system) => (...args) => {
1111
const spec = system.getSystem().specSelectors.specJson()
@@ -17,6 +17,17 @@ function onlyOAS3(selector) {
1717
}
1818
}
1919

20+
function isOpenAPI31(selector) {
21+
return (ori, system) => (...args) => {
22+
const spec = system.getSystem().specSelectors.specJson()
23+
if (isOpenAPI31Helper(spec)) {
24+
return selector(...args)
25+
} else {
26+
return null
27+
}
28+
}
29+
}
30+
2031
const state = state => {
2132
return state || Map()
2233
}
@@ -83,3 +94,14 @@ export const isSwagger2 = (ori, system) => () => {
8394
const spec = system.getSystem().specSelectors.specJson()
8495
return isSwagger2Helper(Map.isMap(spec) ? spec : Map())
8596
}
97+
98+
export const selectIsOpenAPI31 = (ori, system) => () => {
99+
const spec = system.getSystem().specSelectors.specJson()
100+
return isOpenAPI31Helper(Map.isMap(spec) ? spec : Map())
101+
}
102+
103+
export const selectWebhooks = isOpenAPI31(createSelector(
104+
spec,
105+
spec => spec.getIn(["webhooks"]) || Map()
106+
))
107+

0 commit comments

Comments
 (0)