diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 514a3193..40a678fc 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -6,7 +6,7 @@ jobs: build-and-e2e: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - run: npm ci - - run: npm run bundle - - run: npm run e2e + - uses: actions/checkout@v3 + - run: npm ci + - run: npm run bundle + - run: npm run e2e diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab74307f..71fa9366 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: contents: read steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Docker meta id: docker_meta diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 635eae5e..7827253d 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -11,10 +11,10 @@ jobs: if: needs.check-version-cli.outputs.changed == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS key: npm-${{ hashFiles('package-lock.json') }} @@ -24,7 +24,7 @@ jobs: - run: npm ci - run: npm run bundle - name: Store bundle artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: bundles-cli path: bundles @@ -34,17 +34,17 @@ jobs: if: needs.check-version-cli.outputs.changed == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: npm ci && npm ci --prefix cli - run: npm test e2e-tests: needs: [bundle] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: npm ci - name: Download bundled artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: bundles-cli path: bundles @@ -54,10 +54,10 @@ jobs: if: needs.check-version-cli.outputs.changed == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.npm key: npm-${{ hashFiles('package-lock.json') }} @@ -69,7 +69,7 @@ jobs: - name: Bundle run: npm run compile:cli - name: Store bundle artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: cli path: cli @@ -81,9 +81,9 @@ jobs: changed: ${{ steps.check.outputs.changed }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 - name: Check if version has been updated id: check uses: EndBug/version-check@v2.0.1 @@ -96,18 +96,18 @@ jobs: if: needs.check-version-cli.outputs.changed == 'true' runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: '14.x' registry-url: 'https://registry.npmjs.org' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download cli bundled artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: cli path: cli - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS key: npm-${{ hashFiles('package-lock.json') }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5146cb04..34ee6557 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,10 +9,10 @@ jobs: bundle: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS key: npm-${{ hashFiles('package-lock.json') }} @@ -22,7 +22,7 @@ jobs: - run: npm ci - run: npm run bundle - name: Store bundle artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: bundles path: bundles @@ -30,17 +30,17 @@ jobs: unit-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: npm ci && npm ci --prefix cli - run: npm test e2e-tests: needs: [bundle] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: npm ci - name: Download bundled artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: bundles path: bundles @@ -50,7 +50,7 @@ jobs: # needs: [bundle, unit-tests, e2e-tests] # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@v1 + # - uses: actions/checkout@v3 # - name: Configure AWS Credentials # uses: aws-actions/configure-aws-credentials@v1 # with: @@ -60,7 +60,7 @@ jobs: # - name: Install dependencies # run: npm ci # - name: Download bundled artifacts - # uses: actions/download-artifact@v2 + # uses: actions/download-artifact@v3 # with: # name: bundles # path: bundles @@ -71,21 +71,22 @@ jobs: # - name: Invalidate # run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DEMO_DISTRIBUTION_ID }} --paths "/*" publish: + name: Publish to NPM needs: [bundle, unit-tests, e2e-tests] runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: '14.x' registry-url: 'https://registry.npmjs.org' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download bundled artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: bundles path: bundles - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS key: npm-${{ hashFiles('package-lock.json') }} @@ -98,3 +99,21 @@ jobs: run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-cdn: + name: Publish to CDN + needs: [bundle, unit-tests, e2e-tests] + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Configure AWS + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + - name: Download all artifact + uses: actions/download-artifact@v3 + - name: Publish to S3 + run: npm run publish-cdn diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index ba673bd0..f8fdde7f 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@master + uses: actions/checkout@v3 - name: Run GitHub File Sync uses: Redocly/repo-file-sync-action@master with: GH_PAT: ${{ secrets.GH_PAT }} - COMMIT_PREFIX: "sync:" + COMMIT_PREFIX: 'sync:' SKIP_PR: true diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 3df1b925..ed1d672b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -6,7 +6,7 @@ jobs: build-and-unit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: npm ci && npm ci --prefix cli - run: npm run bundle - run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index c4825105..56cb1429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +# [2.0.0-rc.71](https://github.com/Redocly/redoc/compare/v2.0.0-rc.70...v2.0.0-rc.71) (2022-05-31) + + +### Bug Fixes + +* constraints label details ([eb0917d](https://github.com/Redocly/redoc/commit/eb0917d002e57353027fee9c8f07605de8f1ff6f)) +* merge allOf in correct order ([#2020](https://github.com/Redocly/redoc/issues/2020)) ([1e4ea03](https://github.com/Redocly/redoc/commit/1e4ea03d4a9b7eddf3e4cc7cbdbd4d913583e837)) + + +### Features + +* add hideSecuritySection option allowing to disable the Security panel ([#2027](https://github.com/Redocly/redoc/issues/2027)) ([49cc11d](https://github.com/Redocly/redoc/commit/49cc11d91795653ca870e9276a1e0cd617964e25)) +* add Redoc to Redocly CDN ([#2026](https://github.com/Redocly/redoc/issues/2026)) ([77104d6](https://github.com/Redocly/redoc/commit/77104d6c0d6f457aa08a158e93b52a45877be84e)) +* add support prefix items ([27a9dba](https://github.com/Redocly/redoc/commit/27a9dbaf46aded01a6512645dab27870a85cc73b)) +* remove auth section ([#2022](https://github.com/Redocly/redoc/issues/2022)) ([a863302](https://github.com/Redocly/redoc/commit/a863302cc803bdf27187c613157ba90af1040fc4)) +* show minProperties maxProperties ([#2015](https://github.com/Redocly/redoc/issues/2015)) ([82712c5](https://github.com/Redocly/redoc/commit/82712c5b408dc6bc142307d45fb962de2a43ffba)) + + + +# [2.0.0-rc.70](https://github.com/Redocly/redoc/compare/2.0.0-rc.69...2.0.0-rc.70) (2022-05-17) + + +### Features + +* display patternProperties ([#2008](https://github.com/Redocly/redoc/issues/2008)) ([660cc85](https://github.com/Redocly/redoc/commit/660cc857bc86787e16237b407fe5f5d7a493bb48)) +* support conditional operators ([#1939](https://github.com/Redocly/redoc/issues/1939)) ([291b62a](https://github.com/Redocly/redoc/commit/291b62a206b68f8b4d98e4b74b71c0cad20a8b9b)) +* theme add links textDecoration options ([#1599](https://github.com/Redocly/redoc/issues/1599)) ([ba06485](https://github.com/Redocly/redoc/commit/ba06485ece27acbb6b846500817f4bff3e4997ba)) + + + # [2.0.0-rc.69](https://github.com/Redocly/redoc/compare/v2.0.0-rc.68.1...v2.0.0-rc.69) (2022-05-12) diff --git a/README.md b/README.md index ddc7411d..5040347a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Build Status](https://travis-ci.com/Redocly/redoc.svg?branch=master)](https://travis-ci.com/Redocly/redoc) [![Coverage Status](https://coveralls.io/repos/Redocly/redoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Redocly/redoc?branch=master) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Redocly/redoc/blob/master/LICENSE) - [![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) [![Docker Build Status](https://img.shields.io/docker/build/redocly/redoc.svg)](https://hub.docker.com/r/redocly/redoc/) + [![bundle size](http://img.badgesize.io/https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) [![Docker Build Status](https://img.shields.io/docker/build/redocly/redoc.svg)](https://hub.docker.com/r/redocly/redoc/) **This is the README for the `2.x` version of Redoc (React-based).** @@ -102,9 +102,9 @@ Refer to the Redocly's documentation for more information on these products: ![](docs/images/code-samples-demo.gif) ## Releases -**Important:** all the 2.x releases are deployed to npm and can be used with jsdeliver: -- particular release, for example, `v2.0.0-alpha.15`: https://cdn.jsdelivr.net/npm/redoc@2.0.0-alpha.17/bundles/redoc.standalone.js -- `next` release: https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js +**Important:** all the 2.x releases are deployed to npm and can be used with Redocly-cdn: +- particular release, for example, `v2.0.0-rc.70`: https://cdn.redoc.ly/redoc/v2.0.0-rc.70/bundles/redoc.standalone.js +- `latest` release: https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(deprecated)**: - particular release, for example `v1.2.0`: https://rebilly.github.io/ReDoc/releases/v1.2.0/redoc.min.js @@ -166,7 +166,7 @@ replace the `spec-url` attribute with the url or local file address to your defi - + @@ -232,7 +232,6 @@ You can use all of the following options with the standalone version of the 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~ * `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `true`. * `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs). -* `noAutoAuth` - do not inject Authentication section automatically. * `onlyRequiredInSamples` - shows only required fields in request samples. * `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one. * `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array. @@ -289,6 +288,8 @@ You can use all of the following options with the standalone version of the { - // FIXME: remove skip after release - it.skip('should not fail on resolving url', () => { + it('should not fail on resolving url', () => { const r = spawnSync( 'ts-node', [ diff --git a/cli/npm-shrinkwrap.json b/cli/npm-shrinkwrap.json index 9ac75b6d..58fbfa27 100644 --- a/cli/npm-shrinkwrap.json +++ b/cli/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "redoc-cli", - "version": "0.13.12", + "version": "0.13.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "redoc-cli", - "version": "0.13.12", + "version": "0.13.14", "license": "MIT", "dependencies": { "chokidar": "^3.5.1", @@ -17,7 +17,7 @@ "node-libs-browser": "^2.2.1", "react": "^17.0.1", "react-dom": "^17.0.1", - "redoc": "2.0.0-rc.68", + "redoc": "2.0.0-rc.70", "styled-components": "^5.3.0", "yargs": "^17.3.1" }, @@ -216,9 +216,9 @@ } }, "node_modules/@redocly/openapi-core": { - "version": "1.0.0-beta.96", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.96.tgz", - "integrity": "sha512-tcy0q+9PRWV4rcnVx5uHII/9Cq9qpUzWNppupAaVgutxjQRPWH45e24NLinn6lA8Q4por6HuMYkk/0QAJE8d3A==", + "version": "1.0.0-beta.97", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.97.tgz", + "integrity": "sha512-3WW9/6flosJuRtU3GI0Vw39OYFZqqXMDCp5TLa3EjXOb7Nm6AZTWRb3Y+I/+UdNJ/NTszVJkQczoa1t476ekiQ==", "dependencies": { "@redocly/ajv": "^8.6.4", "@types/node": "^14.11.8", @@ -226,7 +226,7 @@ "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", "lodash.isequal": "^4.5.0", - "minimatch": "^3.0.4", + "minimatch": "^5.0.1", "node-fetch": "^2.6.1", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" @@ -236,9 +236,9 @@ } }, "node_modules/@redocly/openapi-core/node_modules/@types/node": { - "version": "14.18.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.16.tgz", - "integrity": "sha512-X3bUMdK/VmvrWdoTkz+VCn6nwKwrKCFTHtqwBIaQJNx4RUIBBUFXM00bqPz/DsDd+Icjmzm6/tyYZzeGVqb6/Q==" + "version": "14.18.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.17.tgz", + "integrity": "sha512-oajWz4kOajqpKJMPgnCvBajPq8QAvl2xIWoFjlAJPKGu6n7pjov5SxGE45a+0RxHDoo4ycOMoZw1SCOWtDERbw==" }, "node_modules/@types/chokidar": { "version": "2.1.3", @@ -646,12 +646,11 @@ "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -922,11 +921,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "peer": true }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -1253,9 +1247,9 @@ } }, "node_modules/foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -1686,14 +1680,14 @@ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/minimist": { @@ -1932,9 +1926,9 @@ } }, "node_modules/openapi-sampler": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.2.1.tgz", - "integrity": "sha512-mHrYmyvcLD0qrfqPkPRBAL2z16hGT2rW0d0B7nklfoTcc3pmkJLkSZlKSeFgerUM41E5c7jlxf0Y19xrM7mWQQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.2.3.tgz", + "integrity": "sha512-dH2QYXqakorV5dxkP/f1BV3Ku4yNn21YmBsqJunnyrHLw7mnCNZZldftgrEpv/66b1m5oaUAmiJoJN+FqBEkJg==", "dependencies": { "@types/json-schema": "^7.0.7", "json-pointer": "0.6.2" @@ -2207,11 +2201,11 @@ } }, "node_modules/redoc": { - "version": "2.0.0-rc.68", - "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.68.tgz", - "integrity": "sha512-sCz52OEhLDu2cIBimy4f6CaVoDxUzD16x63oZx4kpDQOTXYtk0hEOlph1s5VrgNg9pg+rJ9LCCfnwCuzx3Be8w==", + "version": "2.0.0-rc.70", + "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.70.tgz", + "integrity": "sha512-sdmZ8FX4JjF50hTSjHJ64Ccu9Ewa2O8+Fo8pCLg8GHFrbaFJ2E+KBDK9pGuAqNi61fm3Z5c91Ur7zqpITkUpNg==", "dependencies": { - "@redocly/openapi-core": "^1.0.0-beta.95", + "@redocly/openapi-core": "^1.0.0-beta.97", "classnames": "^2.3.1", "decko": "^1.2.0", "dompurify": "^2.2.8", @@ -2221,7 +2215,7 @@ "mark.js": "^8.11.1", "marked": "^4.0.15", "mobx-react": "^7.2.0", - "openapi-sampler": "^1.2.1", + "openapi-sampler": "^1.2.3", "path-browserify": "^1.0.1", "perfect-scrollbar": "^1.5.1", "polished": "^4.1.3", @@ -3135,9 +3129,9 @@ } }, "@redocly/openapi-core": { - "version": "1.0.0-beta.96", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.96.tgz", - "integrity": "sha512-tcy0q+9PRWV4rcnVx5uHII/9Cq9qpUzWNppupAaVgutxjQRPWH45e24NLinn6lA8Q4por6HuMYkk/0QAJE8d3A==", + "version": "1.0.0-beta.97", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.97.tgz", + "integrity": "sha512-3WW9/6flosJuRtU3GI0Vw39OYFZqqXMDCp5TLa3EjXOb7Nm6AZTWRb3Y+I/+UdNJ/NTszVJkQczoa1t476ekiQ==", "requires": { "@redocly/ajv": "^8.6.4", "@types/node": "^14.11.8", @@ -3145,16 +3139,16 @@ "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", "lodash.isequal": "^4.5.0", - "minimatch": "^3.0.4", + "minimatch": "^5.0.1", "node-fetch": "^2.6.1", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" }, "dependencies": { "@types/node": { - "version": "14.18.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.16.tgz", - "integrity": "sha512-X3bUMdK/VmvrWdoTkz+VCn6nwKwrKCFTHtqwBIaQJNx4RUIBBUFXM00bqPz/DsDd+Icjmzm6/tyYZzeGVqb6/Q==" + "version": "14.18.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.17.tgz", + "integrity": "sha512-oajWz4kOajqpKJMPgnCvBajPq8QAvl2xIWoFjlAJPKGu6n7pjov5SxGE45a+0RxHDoo4ycOMoZw1SCOWtDERbw==" } } }, @@ -3525,12 +3519,11 @@ "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "braces": { @@ -3771,11 +3764,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "peer": true }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -4060,9 +4048,9 @@ } }, "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" }, "fsevents": { "version": "2.3.2", @@ -4396,11 +4384,11 @@ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, "minimist": { @@ -4563,9 +4551,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "openapi-sampler": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.2.1.tgz", - "integrity": "sha512-mHrYmyvcLD0qrfqPkPRBAL2z16hGT2rW0d0B7nklfoTcc3pmkJLkSZlKSeFgerUM41E5c7jlxf0Y19xrM7mWQQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.2.3.tgz", + "integrity": "sha512-dH2QYXqakorV5dxkP/f1BV3Ku4yNn21YmBsqJunnyrHLw7mnCNZZldftgrEpv/66b1m5oaUAmiJoJN+FqBEkJg==", "requires": { "@types/json-schema": "^7.0.7", "json-pointer": "0.6.2" @@ -4805,11 +4793,11 @@ } }, "redoc": { - "version": "2.0.0-rc.68", - "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.68.tgz", - "integrity": "sha512-sCz52OEhLDu2cIBimy4f6CaVoDxUzD16x63oZx4kpDQOTXYtk0hEOlph1s5VrgNg9pg+rJ9LCCfnwCuzx3Be8w==", + "version": "2.0.0-rc.70", + "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.70.tgz", + "integrity": "sha512-sdmZ8FX4JjF50hTSjHJ64Ccu9Ewa2O8+Fo8pCLg8GHFrbaFJ2E+KBDK9pGuAqNi61fm3Z5c91Ur7zqpITkUpNg==", "requires": { - "@redocly/openapi-core": "^1.0.0-beta.95", + "@redocly/openapi-core": "^1.0.0-beta.97", "classnames": "^2.3.1", "decko": "^1.2.0", "dompurify": "^2.2.8", @@ -4819,7 +4807,7 @@ "mark.js": "^8.11.1", "marked": "^4.0.15", "mobx-react": "^7.2.0", - "openapi-sampler": "^1.2.1", + "openapi-sampler": "^1.2.3", "path-browserify": "^1.0.1", "perfect-scrollbar": "^1.5.1", "polished": "^4.1.3", diff --git a/cli/package.json b/cli/package.json index 9e1857e4..63433e5f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "redoc-cli", - "version": "0.13.12", + "version": "0.13.14", "description": "ReDoc's Command Line Interface", "main": "index.js", "bin": "index.js", @@ -19,7 +19,7 @@ "node-libs-browser": "^2.2.1", "react": "^17.0.1", "react-dom": "^17.0.1", - "redoc": "2.0.0-rc.68", + "redoc": "2.0.0-rc.70", "styled-components": "^5.3.0", "yargs": "^17.3.1" }, diff --git a/config/docker/Dockerfile b/config/docker/Dockerfile index bc54ee35..b46474ca 100644 --- a/config/docker/Dockerfile +++ b/config/docker/Dockerfile @@ -5,14 +5,14 @@ # npm i -g http-server # http-server -p 8000 --cors -FROM node:alpine +FROM node:12-alpine RUN apk update && apk add --no-cache git # Install dependencies WORKDIR /build COPY package.json package-lock.json /build/ -RUN npm ci --no-optional --ignore-scripts --force +RUN npm ci --no-optional --ignore-scripts # copy only required for the build files COPY src /build/src diff --git a/demo/openapi-3-1.yaml b/demo/openapi-3-1.yaml index 484fd33d..9c875fc6 100644 --- a/demo/openapi-3-1.yaml +++ b/demo/openapi-3-1.yaml @@ -960,6 +960,33 @@ components: schemas: ApiResponse: type: object + patternProperties: + ^S_\\w+\\.[1-9]{2,4}$: + description: The measured skill for hunting + if: + x-displayName: fieldName === 'status' + else: + minLength: 1 + maxLength: 10 + then: + format: url + type: string + enum: + - success + - failed + ^O_\\w+\\.[1-9]{2,4}$: + type: object + properties: + nestedProperty: + type: [string, boolean] + description: The measured skill for hunting + default: lazy + example: adventurous + enum: + - clueless + - lazy + - adventurous + - aggressive properties: code: type: integer @@ -975,7 +1002,7 @@ components: - type: object properties: huntingSkill: - type: string + type: [string, boolean] description: The measured skill for hunting default: lazy example: adventurous @@ -1099,15 +1126,26 @@ components: example: Guru photoUrls: description: The list of URL to a cute photos featuring pet - type: [string, integer, 'null', array] + type: [string, integer, 'null'] minItems: 1 - maxItems: 20 + maxItems: 10 xml: name: photoUrl wrapped: true items: type: string format: url + if: + x-displayName: isString + type: string + then: + minItems: 1 + maxItems: 15 + else: + x-displayName: notString + type: [integer, 'null'] + minItems: 1 + maxItems: 20 friend: $ref: '#/components/schemas/Pet' tags: @@ -1131,6 +1169,12 @@ components: petType: description: Type of a pet type: string + huntingSkill: + type: [integer] + enum: + - 0 + - 1 + - 2 xml: name: Pet Tag: @@ -1198,6 +1242,35 @@ components: type: string contentEncoding: base64 contentMediaType: image/png + addresses: + type: array + minItems: 0 + maxLength: 10 + prefixItems: + - type: object + properties: + city: + type: string + minLength: 0 + country: + type: string + minLength: 0 + street: + description: includes build/apartment number + type: string + minLength: 0 + - type: number + items: + type: string + if: + title: userStatus === 10 + properties: + userStatus: + enum: [10] + then: + required: ['phone'] + else: + required: [] xml: name: User requestBodies: diff --git a/demo/openapi.yaml b/demo/openapi.yaml index 5cf19340..4008f2e2 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -88,7 +88,7 @@ paths: parameters: - name: Accept-Language in: header - description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US" + description: 'The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US' example: en-US required: false schema: @@ -254,7 +254,7 @@ paths: required: false schema: type: string - example: "Bearer " + example: 'Bearer ' - name: petId in: path description: Pet id to delete @@ -401,6 +401,7 @@ paths: application/json: schema: type: object + minProperties: 2 additionalProperties: type: integer format: int32 @@ -429,7 +430,7 @@ paths: application/json: example: status: 400 - message: "Invalid Order" + message: 'Invalid Order' requestBody: content: application/json: @@ -877,11 +878,11 @@ paths: type: string examples: response: - value: OK + value: OK text/plain: examples: response: - value: OK + value: OK '400': description: Invalid username/password supplied /user/logout: @@ -1027,8 +1028,8 @@ components: properties: id: externalDocs: - description: "Find more info here" - url: "https://example.com" + description: 'Find more info here' + url: 'https://example.com' description: Pet ID allOf: - $ref: '#/components/schemas/Id' @@ -1134,6 +1135,26 @@ components: description: User status type: integer format: int32 + addresses: + type: array + minItems: 0 + maxLength: 10 + items: + - type: object + properties: + city: + type: string + minLength: 0 + country: + type: string + minLength: 0 + street: + description: includes build/apartment number + type: string + minLength: 0 + - type: number + additionalItems: + type: string xml: name: User requestBodies: @@ -1201,7 +1222,7 @@ x-webhooks: content: application/json: schema: - $ref: "#/components/schemas/Pet" + $ref: '#/components/schemas/Pet' responses: - "200": + '200': description: Return a 200 status to indicate that the data was received successfully diff --git a/docs/deployment/html.md b/docs/deployment/html.md index 604f7ce2..5c392be9 100644 --- a/docs/deployment/html.md +++ b/docs/deployment/html.md @@ -51,7 +51,7 @@ or the files located in your `node modules` folder. To reference the Redoc script with a CDN link: ```html - + ``` ### Node modules link @@ -97,7 +97,7 @@ Redoc.init(specOrSpecUrl, options, element, callback) ``` - `specOrSpecUrl`: Either a JSON object with the OpenAPI definition or a URL to the definition in JSON or YAML format. -- `options`: See [options object](https://redocly.com/docs/api-reference-docs/configuration/) reference. +- `options`: See [features.openapi object](/docs/api-reference-docs/configuration/functionality.mdx) reference. - `element`: DOM element Redoc will be inserted into. - `callback`(optional): Callback to be called after Redoc has been fully rendered. It is also called on errors with `error` as the first argument. diff --git a/docs/quickstart.md b/docs/quickstart.md index 7607c10f..1b01e0f1 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -38,7 +38,7 @@ replace the `spec-url` attribute with the URL or local file address to your defi - + ``` diff --git a/package-lock.json b/package-lock.json index bfd4e16a..5bd6d7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "redoc", - "version": "2.0.0-rc.69", + "version": "2.0.0-rc.71", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "redoc", - "version": "2.0.0-rc.69", + "version": "2.0.0-rc.71", "license": "MIT", "dependencies": { "@redocly/openapi-core": "^1.0.0-beta.97", @@ -19,7 +19,7 @@ "mark.js": "^8.11.1", "marked": "^4.0.15", "mobx-react": "^7.2.0", - "openapi-sampler": "^1.2.3", + "openapi-sampler": "^1.3.0", "path-browserify": "^1.0.1", "perfect-scrollbar": "^1.5.1", "polished": "^4.1.3", @@ -14217,9 +14217,9 @@ } }, "node_modules/openapi-sampler": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.2.3.tgz", - "integrity": "sha512-dH2QYXqakorV5dxkP/f1BV3Ku4yNn21YmBsqJunnyrHLw7mnCNZZldftgrEpv/66b1m5oaUAmiJoJN+FqBEkJg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.3.0.tgz", + "integrity": "sha512-2QfjK1oM9Sv0q82Ae1RrUe3yfFmAyjF548+6eAeb+h/cL1Uj51TW4UezraBEvwEdzoBgfo4AaTLVFGTKj+yYDw==", "dependencies": { "@types/json-schema": "^7.0.7", "json-pointer": "0.6.2" @@ -29872,9 +29872,9 @@ } }, "openapi-sampler": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.2.3.tgz", - "integrity": "sha512-dH2QYXqakorV5dxkP/f1BV3Ku4yNn21YmBsqJunnyrHLw7mnCNZZldftgrEpv/66b1m5oaUAmiJoJN+FqBEkJg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.3.0.tgz", + "integrity": "sha512-2QfjK1oM9Sv0q82Ae1RrUe3yfFmAyjF548+6eAeb+h/cL1Uj51TW4UezraBEvwEdzoBgfo4AaTLVFGTKj+yYDw==", "requires": { "@types/json-schema": "^7.0.7", "json-pointer": "0.6.2" diff --git a/package.json b/package.json index 3296cacb..954a1bfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redoc", - "version": "2.0.0-rc.69", + "version": "2.0.0-rc.71", "description": "ReDoc", "repository": { "type": "git", @@ -34,6 +34,7 @@ "start:benchmark": "webpack serve --mode=production --env.bench --config demo/webpack.config.ts", "test": "npm run unit && npm run license-check", "unit": "jest --coverage", + "test:update-snapshot": "jest --updateSnapshot", "e2e": "cypress run", "e2e-ci": "cypress run --record", "bundlesize": "size-limit", @@ -53,6 +54,7 @@ "start:demo": "webpack serve --hot --config demo/webpack.config.ts --mode=development", "compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs", "build:demo": "webpack --mode=production --config demo/webpack.config.ts", + "publish-cdn": "scripts/publish-cdn.sh", "deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read", "license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause;CC-BY-4.0;Python-2.0' --summary", "docker:build": "docker build -f config/docker/Dockerfile -t redoc .", @@ -148,7 +150,7 @@ "mark.js": "^8.11.1", "marked": "^4.0.15", "mobx-react": "^7.2.0", - "openapi-sampler": "^1.2.3", + "openapi-sampler": "^1.3.0", "path-browserify": "^1.0.1", "perfect-scrollbar": "^1.5.1", "polished": "^4.1.3", diff --git a/scripts/publish-cdn.sh b/scripts/publish-cdn.sh new file mode 100755 index 00000000..af930849 --- /dev/null +++ b/scripts/publish-cdn.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -e # exit on error + +# TODO: Update script! + +VERSION=$(node scripts/version.js) +VERSION_TAG=v${VERSION:0:1}.x + +copy_to_s3 () { + aws s3 cp bundles "s3://redocly-cdn/redoc/$1/bundles" --recursive "$2" + aws s3 cp CHANGELOG.md "s3://redocly-cdn/redoc/$1/CHANGELOG.md" "$2" + aws s3 cp LICENSE "s3://redocly-cdn/redoc/$1/LICENSE" "$2" + aws s3 cp package.json "s3://redocly-cdn/redoc/$1/package.json" "$2" + aws s3 cp README.md "s3://redocly-cdn/redoc/$1/README.md" "$2" +} + +if aws s3 ls "redocly-cdn/redoc/v$VERSION/" "$@"; then + echo "Version $VERSION already exists" + exit 1 +else + echo Releasing $VERSION + + echo Uploading to S3 $VERSION + copy_to_s3 "v$VERSION" $@ + + echo Uploading to S3 $VERSION_TAG + copy_to_s3 "$VERSION_TAG" $@ + + if [[ "$VERSION_TAG" == "v2.x" ]]; then + echo Uploading to S3 latest + copy_to_s3 latest $@ + fi + + echo + echo Deployed successfully + exit 0 +fi diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 00000000..8729268e --- /dev/null +++ b/scripts/version.js @@ -0,0 +1 @@ +console.log(require('../package.json').version); diff --git a/src/common-elements/fields-layout.ts b/src/common-elements/fields-layout.ts index a1fd214b..9e7fce0b 100644 --- a/src/common-elements/fields-layout.ts +++ b/src/common-elements/fields-layout.ts @@ -1,4 +1,4 @@ -import styled, { extensionsHook, media } from '../styled-components'; +import styled, { extensionsHook, media, css } from '../styled-components'; import { deprecatedCss } from './mixins'; export const PropertiesTableCaption = styled.caption` @@ -72,7 +72,26 @@ export const PropertyNameCell = styled(PropertyCell)` ${deprecatedCss}; } - ${({ kind }) => (kind !== 'field' ? 'font-style: italic' : '')}; + ${({ kind }) => + kind === 'patternProperties' && + css` + > span.property-name { + display: inline-table; + white-space: break-spaces; + margin-right: 20px; + + ::before, + ::after { + content: '/'; + filter: opacity(0.2); + } + } + `} + + ${({ kind = '' }) => + ['field', 'additionalProperties', 'patternProperties'].includes(kind) + ? '' + : 'font-style: italic'}; ${extensionsHook('PropertyNameCell')}; `; diff --git a/src/common-elements/fields.ts b/src/common-elements/fields.ts index 0a7275cf..d894e9a0 100644 --- a/src/common-elements/fields.ts +++ b/src/common-elements/fields.ts @@ -1,6 +1,6 @@ import { transparentize } from 'polished'; -import styled, { extensionsHook } from '../styled-components'; +import styled, { extensionsHook, css } from '../styled-components'; import { PropertyNameCell } from './fields-layout'; import { ShelfIcon } from './shelfs'; @@ -17,6 +17,27 @@ export const ClickablePropertyNameCell = styled(PropertyNameCell)` &:focus { font-weight: ${({ theme }) => theme.typography.fontWeightBold}; } + ${({ kind }) => + kind === 'patternProperties' && + css` + display: inline-flex; + margin-right: 20px; + + > span.property-name { + white-space: break-spaces; + text-align: left; + + ::before, + ::after { + content: '/'; + filter: opacity(0.2); + } + } + + > svg { + align-self: center; + } + `} } ${ShelfIcon} { height: ${({ theme }) => theme.schema.arrow.size}; @@ -56,6 +77,10 @@ export const RequiredLabel = styled(FieldLabel.withComponent('div'))` line-height: 1; `; +export const PropertyLabel = styled(RequiredLabel)` + color: ${props => props.theme.colors.primary.light}; +`; + export const RecursiveLabel = styled(FieldLabel)` color: ${({ theme }) => theme.colors.warning.main}; font-size: 13px; diff --git a/src/common-elements/shelfs.tsx b/src/common-elements/shelfs.tsx index f07c6bfc..18c26ebd 100644 --- a/src/common-elements/shelfs.tsx +++ b/src/common-elements/shelfs.tsx @@ -37,6 +37,7 @@ class IntShelfIcon extends React.PureComponent<{ export const ShelfIcon = styled(IntShelfIcon)` height: ${props => props.size || '18px'}; width: ${props => props.size || '18px'}; + min-width: ${props => props.size || '18px'}; vertical-align: middle; float: ${props => props.float || ''}; transition: transform 0.2s ease-out; diff --git a/src/components/Fields/ArrayItemDetails.tsx b/src/components/Fields/ArrayItemDetails.tsx index db8837d2..f56ab0a7 100644 --- a/src/components/Fields/ArrayItemDetails.tsx +++ b/src/components/Fields/ArrayItemDetails.tsx @@ -4,9 +4,20 @@ import { ConstraintsView } from './FieldContstraints'; import { Pattern } from './Pattern'; import { SchemaModel } from '../../services'; import styled from '../../styled-components'; +import { OptionsContext } from '../OptionsProvider'; export function ArrayItemDetails({ schema }: { schema: SchemaModel }) { - if (!schema || (schema.type === 'string' && !schema.constraints.length)) return null; + const { hideSchemaPattern } = React.useContext(OptionsContext); + if ( + !schema || + (schema.type === 'string' && !schema.constraints.length) || + ((!schema?.pattern || hideSchemaPattern) && + !schema.items && + !schema.displayFormat && + !schema.constraints.length) // return null for cases where all constraints are empty + ) { + return null; + } return ( diff --git a/src/components/Fields/Field.tsx b/src/components/Fields/Field.tsx index 2fb5865f..5c35b04b 100644 --- a/src/components/Fields/Field.tsx +++ b/src/components/Fields/Field.tsx @@ -1,9 +1,12 @@ import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClickablePropertyNameCell, RequiredLabel } from '../../common-elements/fields'; +import { + ClickablePropertyNameCell, + PropertyLabel, + RequiredLabel, +} from '../../common-elements/fields'; import { FieldDetails } from './FieldDetails'; - import { InnerPropertiesWrap, PropertyBullet, @@ -11,11 +14,11 @@ import { PropertyDetailsCell, PropertyNameCell, } from '../../common-elements/fields-layout'; - import { ShelfIcon } from '../../common-elements/'; +import { Schema } from '../Schema/Schema'; -import { FieldModel } from '../../services/models'; -import { Schema, SchemaOptions } from '../Schema/Schema'; +import type { SchemaOptions } from '../Schema/Schema'; +import type { FieldModel } from '../../services/models'; export interface FieldProps extends SchemaOptions { className?: string; @@ -46,12 +49,20 @@ export class Field extends React.Component { }; render() { - const { className, field, isLast, expandByDefault } = this.props; + const { className = '', field, isLast, expandByDefault } = this.props; const { name, deprecated, required, kind } = field; const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular; const expanded = field.expanded === undefined ? expandByDefault : field.expanded; + const labels = ( + <> + {kind === 'additionalProperties' && additional property} + {kind === 'patternProperties' && pattern property} + {required && required} + + ); + const paramName = withSubSchema ? ( { onKeyPress={this.handleKeyPress} aria-label="expand properties" > - {name} + {name} - {required && required } + {labels} ) : ( - {name} - {required && required } + {name} + {labels} ); diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index c04a5b0f..36a43d3e 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { observer } from 'mobx-react'; import { RecursiveLabel, @@ -24,7 +25,7 @@ import { OptionsContext } from '../OptionsProvider'; import { Pattern } from './Pattern'; import { ArrayItemDetails } from './ArrayItemDetails'; -function FieldDetailsComponent(props: FieldProps) { +export const FieldDetailsComponent = observer((props: FieldProps) => { const { enumSkipQuotes, hideSchemaTitles } = React.useContext(OptionsContext); const { showExamples, field, renderDiscriminatorSwitch } = props; @@ -107,6 +108,6 @@ function FieldDetailsComponent(props: FieldProps) { {(_const && ) || null} ); -} +}); export const FieldDetails = React.memo(FieldDetailsComponent); diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index 8464765f..7bf03de9 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -27,20 +27,20 @@ class Json extends React.PureComponent { } renderInner = ({ renderCopyButton }) => { - const showFoldingButtons = this.props.data && Object.values(this.props.data).some( - (value) => typeof value === 'object' && value !== null, - ); + const showFoldingButtons = + this.props.data && + Object.values(this.props.data).some(value => typeof value === 'object' && value !== null); return ( {renderCopyButton()} - {showFoldingButtons && + {showFoldingButtons && ( <> - } + )} {options => ( diff --git a/src/components/Markdown/styled.elements.tsx b/src/components/Markdown/styled.elements.tsx index 3c1b9210..7a67d31c 100644 --- a/src/components/Markdown/styled.elements.tsx +++ b/src/components/Markdown/styled.elements.tsx @@ -6,7 +6,7 @@ import { StyledComponent } from 'styled-components'; export const linksCss = css` a { - text-decoration: none; + text-decoration: ${props => props.theme.typography.links.textDecoration}; color: ${props => props.theme.typography.links.color}; &:visited { @@ -15,6 +15,7 @@ export const linksCss = css` &:hover { color: ${props => props.theme.typography.links.hover}; + text-decoration: ${props => props.theme.typography.links.hoverTextDecoration}; } } `; diff --git a/src/components/Parameters/Parameters.tsx b/src/components/Parameters/Parameters.tsx index ed9141bc..3f4275df 100644 --- a/src/components/Parameters/Parameters.tsx +++ b/src/components/Parameters/Parameters.tsx @@ -10,6 +10,7 @@ import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch'; import { Schema } from '../Schema'; import { Markdown } from '../Markdown/Markdown'; +import { ConstraintsView } from '../Fields/FieldContstraints'; function safePush(obj, prop, item) { if (!obj[prop]) { @@ -79,6 +80,9 @@ export function BodyContent(props: { return ( <> {description !== undefined && } + {schema?.type === 'object' && ( + + )} { render() { @@ -21,7 +22,14 @@ export class ResponseDetails extends React.PureComponent<{ response: ResponseMod {({ schema }) => { - return ; + return ( + <> + {schema?.type === 'object' && ( + + )} + + + ); }} diff --git a/src/components/Schema/ArraySchema.tsx b/src/components/Schema/ArraySchema.tsx index 25ac633e..84bd716a 100644 --- a/src/components/Schema/ArraySchema.tsx +++ b/src/components/Schema/ArraySchema.tsx @@ -6,6 +6,7 @@ import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements'; import styled from '../../styled-components'; import { humanizeConstraints } from '../../utils'; import { TypeName } from '../../common-elements/fields'; +import { ObjectSchema } from './ObjectSchema'; const PaddedSchema = styled.div` padding-left: ${({ theme }) => theme.spacing.unit * 2}px; @@ -21,6 +22,9 @@ export class ArraySchema extends React.PureComponent { ? '' : `(${humanizeConstraints(schema)})`; + if (schema.fields) { + return ; + } if (schema.displayType && !itemsSchema && !minMaxItems.length) { return (
diff --git a/src/components/Schema/OneOfSchema.tsx b/src/components/Schema/OneOfSchema.tsx index b0864c33..311a3258 100644 --- a/src/components/Schema/OneOfSchema.tsx +++ b/src/components/Schema/OneOfSchema.tsx @@ -8,6 +8,7 @@ import { } from '../../common-elements/schema'; import { Badge } from '../../common-elements/shelfs'; import { SchemaModel } from '../../services/models'; +import { ConstraintsView } from '../Fields/FieldContstraints'; import { Schema, SchemaProps } from './Schema'; export interface OneOfButtonProps { @@ -47,6 +48,8 @@ export class OneOfSchema extends React.Component { if (oneOf === undefined) { return null; } + const activeSchema = oneOf[schema.activeOneOf]; + return (
{schema.oneOfType} @@ -58,7 +61,8 @@ export class OneOfSchema extends React.Component {
{oneOf[schema.activeOneOf].deprecated && Deprecated}
- + +
); } diff --git a/src/components/SecurityRequirement/OAuthFlow.tsx b/src/components/SecurityRequirement/OAuthFlow.tsx new file mode 100644 index 00000000..4b20e1f7 --- /dev/null +++ b/src/components/SecurityRequirement/OAuthFlow.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { OpenAPISecurityScheme } from '../../types'; +import { SecurityRow } from './styled.elements'; +import { SeeMore } from '../SeeMore/SeeMore'; +import { Markdown } from '../Markdown/Markdown'; + +export interface OAuthFlowProps { + type: string; + flow: OpenAPISecurityScheme['flows'][keyof OpenAPISecurityScheme['flows']]; + RequiredScopes?: JSX.Element; +} + +export function OAuthFlowComponent(props: OAuthFlowProps) { + const { type, flow, RequiredScopes } = props; + const scopesNames = Object.keys(flow?.scopes || {}); + console.log('rended'); + return ( + <> + + Flow type: + {type} + + {(type === 'implicit' || type === 'authorizationCode') && ( + + Authorization URL: + + + {(flow as any).authorizationUrl} + + + + )} + {(type === 'password' || type === 'clientCredentials' || type === 'authorizationCode') && ( + + Token URL: + {(flow as any).tokenUrl} + + )} + {flow!.refreshUrl && ( + + Refresh URL: + {flow!.refreshUrl} + + )} + {!!scopesNames.length && ( + <> + {RequiredScopes || null} + + Scopes: + + +
    + {scopesNames.map(scope => ( +
  • + {scope} -{' '} + +
  • + ))} +
+
+ + )} + + ); +} + +export const OAuthFlow = React.memo(OAuthFlowComponent); diff --git a/src/components/SecurityRequirement/RequiredScopesRow.tsx b/src/components/SecurityRequirement/RequiredScopesRow.tsx new file mode 100644 index 00000000..8f899b60 --- /dev/null +++ b/src/components/SecurityRequirement/RequiredScopesRow.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +export const RequiredScopesRow = ({ scopes }: { scopes: string[] }): JSX.Element | null => { + if (!scopes.length) return null; + + return ( +
+ Required scopes: + {scopes.map((scope, idx) => { + return ( + + {scope}{' '} + + ); + })} +
+ ); +}; diff --git a/src/components/SecurityRequirement/SecurityDetails.tsx b/src/components/SecurityRequirement/SecurityDetails.tsx new file mode 100644 index 00000000..54cd61f8 --- /dev/null +++ b/src/components/SecurityRequirement/SecurityDetails.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { SecuritySchemeModel } from '../../services'; +import { titleize } from '../../utils'; +import { StyledMarkdownBlock } from '../Markdown/styled.elements'; +import { SecurityRow } from './styled.elements'; +import { OAuthFlow } from './OAuthFlow'; + +interface SecuritySchemaProps { + RequiredScopes?: JSX.Element; + scheme: SecuritySchemeModel; +} +export function SecurityDetails(props: SecuritySchemaProps) { + const { RequiredScopes, scheme } = props; + + return ( + + {scheme.apiKey ? ( + <> + + {titleize(scheme.apiKey.in || '')} parameter name: + {scheme.apiKey.name} + + {RequiredScopes} + + ) : scheme.http ? ( + <> + + HTTP Authorization Scheme: + {scheme.http.scheme} + + + {scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && ( + <> + Bearer format: + {scheme.http.bearerFormat} + + )} + + {RequiredScopes} + + ) : scheme.openId ? ( + <> + + Connect URL: + + + {scheme.openId.connectUrl} + + + + {RequiredScopes} + + ) : scheme.flows ? ( + Object.keys(scheme.flows).map(type => ( + + )) + ) : null} + + ); +} diff --git a/src/components/SecurityRequirement/SecurityHeader.tsx b/src/components/SecurityRequirement/SecurityHeader.tsx new file mode 100644 index 00000000..c542b51c --- /dev/null +++ b/src/components/SecurityRequirement/SecurityHeader.tsx @@ -0,0 +1,43 @@ +import { SecurityRequirementModel } from '../../services/models/SecurityRequirement'; +import { + ScopeName, + SecurityRequirementAndWrap, + SecurityRequirementOrWrap, +} from './styled.elements'; +import * as React from 'react'; +import { AUTH_TYPES } from '../SecuritySchemes/SecuritySchemes'; + +export interface SecurityRequirementProps { + security: SecurityRequirementModel; + showSecuritySchemeType?: boolean; + expanded: boolean; +} + +export function SecurityHeader(props: SecurityRequirementProps) { + const { security, showSecuritySchemeType, expanded } = props; + + const grouping = security.schemes.length > 1; + return ( + + {grouping && '('} + {security.schemes.map(scheme => { + return ( + + {showSecuritySchemeType && `${AUTH_TYPES[scheme.type] || scheme.type}: `} + {scheme.displayName} + {expanded && scheme.scopes.length + ? [ + ' (', + scheme.scopes.map(scope => ( + {scope} + )), + ') ', + ] + : null} + + ); + })} + {grouping && ') '} + + ); +} diff --git a/src/components/SecurityRequirement/SecurityRequirement.tsx b/src/components/SecurityRequirement/SecurityRequirement.tsx index 633bdeaa..5ae43782 100644 --- a/src/components/SecurityRequirement/SecurityRequirement.tsx +++ b/src/components/SecurityRequirement/SecurityRequirement.tsx @@ -1,153 +1,102 @@ import * as React from 'react'; - -import styled, { media } from '../../styled-components'; - -import { Link, UnderlinedHeader } from '../../common-elements/'; +import { useState } from 'react'; import { SecurityRequirementModel } from '../../services/models/SecurityRequirement'; -import { linksCss } from '../Markdown/styled.elements'; - -const ScopeNameList = styled.ul` - display: inline; - list-style: none; - padding: 0; - - li { - display: inherit; - - &:after { - content: ','; - } - &:last-child:after { - content: none; - } - } -`; - -const ScopeName = styled.code` - font-size: ${props => props.theme.typography.code.fontSize}; - font-family: ${props => props.theme.typography.code.fontFamily}; - border: 1px solid ${({ theme }) => theme.colors.border.dark}; - margin: 0 3px; - padding: 0.2em; - display: inline-block; - line-height: 1; -`; - -const SecurityRequirementAndWrap = styled.span` - &:after { - content: ' AND '; - font-weight: bold; - } - - &:last-child:after { - content: none; - } - - ${linksCss}; -`; - -const SecurityRequirementOrWrap = styled.span` - &:before { - content: '( '; - font-weight: bold; - } - &:after { - content: ' ) OR '; - font-weight: bold; - } - &:last-child:after { - content: ' )'; - } - - &:only-child:before, - &:only-child:after { - content: none; - } - - ${linksCss}; -`; - -export interface SecurityRequirementProps { - security: SecurityRequirementModel; -} - -export class SecurityRequirement extends React.PureComponent { - render() { - const security = this.props.security; - return ( - - {security.schemes.length ? ( - security.schemes.map(scheme => { - return ( - - {scheme.displayName} - {scheme.scopes.length > 0 && ' ('} - - {scheme.scopes.map(scope => ( -
  • - {scope} -
  • - ))} -
    - {scheme.scopes.length > 0 && ') '} -
    - ); - }) - ) : ( - None - )} -
    - ); - } -} - -const AuthHeaderColumn = styled.div` - flex: 1 1 auto; -`; - -const SecuritiesColumn = styled.div` - width: ${props => props.theme.schema.defaultDetailsWidth}; - ${media.lessThan('small')` - margin-top: 10px; - `} -`; - -const AuthHeader = styled(UnderlinedHeader)` - display: inline-block; - margin: 0; -`; - -const Wrap = styled.div` - width: 100%; - display: flex; - margin: 1em 0; - - ${media.lessThan('small')` - flex-direction: column; - `} -`; +import { + AuthHeader, + AuthHeaderColumn, + SecuritiesColumn, + SecurityDetailsStyle, + Wrap, +} from './styled.elements'; +import { useStore } from '../StoreBuilder'; +import { SecurityHeader } from './SecurityHeader'; +import { RequiredScopesRow } from './RequiredScopesRow'; +import { AUTH_TYPES } from '../SecuritySchemes/SecuritySchemes'; +import { Markdown } from '../Markdown/Markdown'; +import { SecurityDetails } from './SecurityDetails'; +import { ShelfIcon } from '../../common-elements'; export interface SecurityRequirementsProps { securities: SecurityRequirementModel[]; } -export class SecurityRequirements extends React.PureComponent { - render() { - const securities = this.props.securities; - if (!securities.length) { - return null; - } - return ( - - - Authorizations: +export function SecurityRequirements(props: SecurityRequirementsProps) { + const store = useStore(); + const showSecuritySchemeType = store?.options.showSecuritySchemeType; + const [expanded, setExpanded] = useState(false); + + const { securities } = props; + + if (!securities?.length || store?.options.hideSecuritySection) { + return null; + } + + const operationSecuritySchemes = store?.spec.securitySchemes.schemes.filter(({ id }) => { + return securities.find(security => security.schemes.find(scheme => scheme.id === id)); + }); + + return ( + <> + + setExpanded(!expanded)}> + Authorizations: + - + {securities.map((security, idx) => ( - + ))} - ); - } + {expanded && + operationSecuritySchemes?.length && + operationSecuritySchemes.map((scheme, idx) => ( + +
    + {AUTH_TYPES[scheme.type] || scheme.type}: {scheme.id} +
    + + + } + /> +
    + ))} + + ); +} + +const LockIcon = () => ( + + + +); + +function getRequiredScopes(id: string, securities: SecurityRequirementModel[]): string[] { + const allScopes: string[] = []; + let securitiesLength = securities.length; + + while (securitiesLength--) { + const security = securities[securitiesLength]; + let schemesLength = security.schemes.length; + while (schemesLength--) { + const scheme = security.schemes[schemesLength]; + if (scheme.id === id) { + allScopes.push(...scheme.scopes); + } + } + } + + return Array.from(new Set(allScopes)); } diff --git a/src/components/SecurityRequirement/styled.elements.ts b/src/components/SecurityRequirement/styled.elements.ts new file mode 100644 index 00000000..d7e93546 --- /dev/null +++ b/src/components/SecurityRequirement/styled.elements.ts @@ -0,0 +1,129 @@ +import styled from 'styled-components'; +import { linksCss } from '../Markdown/styled.elements'; +import { media } from '../../styled-components'; +import { UnderlinedHeader } from '../../common-elements'; + +export const Header = styled.div` + background-color: #e4e7eb; +`; + +export const ScopeNameList = styled.ul` + display: inline; + list-style: none; + padding: 0; + + li { + display: inherit; + + &:after { + content: ','; + } + &:last-child:after { + content: none; + } + } +`; + +export const ScopeName = styled.code` + font-size: ${props => props.theme.typography.code.fontSize}; + font-family: ${props => props.theme.typography.code.fontFamily}; + margin: 0 3px; + padding: 0.2em; + display: inline-block; + line-height: 1; + + &:after { + content: ','; + font-weight: normal; + } + + &:last-child:after { + content: none; + } +`; + +export const SecurityRequirementAndWrap = styled.span` + &:after { + content: ' and '; + font-weight: normal; + } + + &:last-child:after { + content: none; + } + + ${linksCss}; +`; + +export const SecurityRequirementOrWrap = styled.span<{ expanded?: boolean }>` + ${p => !p.expanded && `white-space: nowrap;`} + &:after { + content: ' or '; + ${p => p.expanded && `content: ' or \\a';`} + white-space: pre; + } + + &:last-child:after, + &:only-child:after { + content: none; + } + + ${linksCss}; +`; + +export const AuthHeaderColumn = styled.div` + flex: 1 1 auto; + cursor: pointer; +`; + +export const SecuritiesColumn = styled.div<{ expanded?: boolean }>` + width: ${props => props.theme.schema.defaultDetailsWidth}; + text-overflow: ellipsis; + border-radius: 4px; + overflow: hidden; + ${p => + p.expanded && + `background: ${p.theme.colors.gray['100']}; + padding: 8px 9.6px; + margin: 20px 0; + width: 100%; + `}; + ${media.lessThan('small')` + margin-top: 10px; + `} +`; + +export const AuthHeader = styled(UnderlinedHeader)` + display: inline-block; + margin: 0; +`; + +export const Wrap = styled.div<{ expanded?: boolean }>` + width: 100%; + display: flex; + margin: 1em 0; + flex-direction: ${p => (p.expanded ? 'column' : 'row')}; + ${media.lessThan('small')` + flex-direction: column; + `} +`; + +export const SecurityRow = styled.div` + margin: 0.5em 0; +`; + +export const SecurityDetailsStyle = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.colors.border.dark}; + margin-bottom: 1.5em; + padding-bottom: 0.7em; + + h5 { + line-height: 1em; + margin: 0 0 0.6em; + font-size: ${({ theme }) => theme.typography.fontSize}; + } + + .redoc-markdown p:first-child { + display: inline; + } +`; diff --git a/src/components/SecuritySchemes/SecuritySchemes.tsx b/src/components/SecuritySchemes/SecuritySchemes.tsx index 817b0666..34a92ada 100644 --- a/src/components/SecuritySchemes/SecuritySchemes.tsx +++ b/src/components/SecuritySchemes/SecuritySchemes.tsx @@ -1,72 +1,18 @@ import * as React from 'react'; -import { SecuritySchemesModel } from '../../services/models'; - -import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; -import { OpenAPISecurityScheme } from '../../types'; -import { titleize } from '../../utils/helpers'; +import { SecuritySchemesModel } from '../../services'; +import { H2, Row, ShareLink, MiddlePanel, Section } from '../../common-elements'; import { Markdown } from '../Markdown/Markdown'; -import { StyledMarkdownBlock } from '../Markdown/styled.elements'; +import { SecurityDetails } from '../SecurityRequirement/SecurityDetails'; +import { SecurityDetailsStyle, SecurityRow } from '../SecurityRequirement/styled.elements'; -const AUTH_TYPES = { +export const AUTH_TYPES = { oauth2: 'OAuth2', apiKey: 'API Key', http: 'HTTP', openIdConnect: 'OpenID Connect', }; -export interface OAuthFlowProps { - type: string; - flow: OpenAPISecurityScheme['flows'][keyof OpenAPISecurityScheme['flows']]; -} - -export class OAuthFlow extends React.PureComponent { - render() { - const { type, flow } = this.props; - const scopesNames = Object.keys(flow?.scopes || {}); - return ( - - {type} OAuth Flow - - {type === 'implicit' || type === 'authorizationCode' ? ( -
    - Authorization URL: - {(flow as any).authorizationUrl} -
    - ) : null} - {type === 'password' || type === 'clientCredentials' || type === 'authorizationCode' ? ( -
    - Token URL: - {(flow as any).tokenUrl} -
    - ) : null} - {flow!.refreshUrl && ( -
    - Refresh URL: - {flow!.refreshUrl} -
    - )} - {!!scopesNames.length && ( - <> -
    - Scopes: -
    -
      - {scopesNames.map(scope => ( -
    • - {scope} -{' '} - -
    • - ))} -
    - - )} - - - ); - } -} - export interface SecurityDefsProps { securitySchemes: SecuritySchemesModel; } @@ -82,52 +28,13 @@ export class SecurityDefs extends React.PureComponent { {scheme.displayName} - - - - - - - - {scheme.apiKey ? ( - - - - - ) : scheme.http ? ( - [ - - - - , - scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && ( - - - - - ), - ] - ) : scheme.openId ? ( - - - - - ) : scheme.flows ? ( - Object.keys(scheme.flows).map(type => ( - - )) - ) : null} - -
    Security Scheme Type {AUTH_TYPES[scheme.type] || scheme.type}
    {titleize(scheme.apiKey.in || '')} parameter name: {scheme.apiKey.name}
    HTTP Authorization Scheme {scheme.http.scheme}
    Bearer format "{scheme.http.bearerFormat}"
    Connect URL - - {scheme.openId.connectUrl} - -
    -
    + + + Security Scheme Type: + {AUTH_TYPES[scheme.type] || scheme.type} + + + diff --git a/src/components/SeeMore/SeeMore.tsx b/src/components/SeeMore/SeeMore.tsx new file mode 100644 index 00000000..8e3dc2e7 --- /dev/null +++ b/src/components/SeeMore/SeeMore.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +const TOLERANCE_PX = 20; + +interface SeeMoreProps { + children?: React.ReactNode; + height: string; +} + +export function SeeMore({ children, height }: SeeMoreProps): JSX.Element { + const ref = React.createRef() as React.RefObject; + const [showMore, setShowMore] = React.useState(false); + const [showLink, setShowLink] = React.useState(false); + + React.useEffect(() => { + if (ref.current && ref.current.clientHeight + TOLERANCE_PX < ref.current.scrollHeight) { + setShowLink(true); + } + }, [ref]); + + const onClickMore = () => { + setShowMore(!showMore); + }; + + return ( + <> + + {children} + + + {showLink && ( + + {showMore ? 'See less' : 'See more'} + + )} + + + ); +} + +const Container = styled.div` + overflow-y: hidden; +`; + +const ButtonContainer = styled.div<{ dimmed?: boolean }>` + text-align: center; + line-height: 1.5em; + ${({ dimmed }) => + dimmed && + `background-image: linear-gradient(to bottom, transparent,rgb(255 255 255)); + position: relative; + top: -0.5em; + padding-top: 0.5em; + background-position-y: -1em; + `} +`; + +const ButtonLinkStyled = styled.a` + cursor: pointer; +`; diff --git a/src/components/StoreBuilder.ts b/src/components/StoreBuilder.ts index 99630539..14f8446d 100644 --- a/src/components/StoreBuilder.ts +++ b/src/components/StoreBuilder.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { createContext } from 'react'; +import { createContext, useContext } from 'react'; import { AppStore } from '../services/'; import { RedocRawOptions } from '../services/RedocNormalizedOptions'; @@ -79,3 +79,7 @@ export function StoreBuilder(props: StoreBuilderProps) { store, }); } + +export function useStore(): AppStore | undefined { + return useContext(StoreContext); +} diff --git a/src/components/__tests__/OneOfSchema.test.tsx b/src/components/__tests__/OneOfSchema.test.tsx index e9425db7..28b1b265 100644 --- a/src/components/__tests__/OneOfSchema.test.tsx +++ b/src/components/__tests__/OneOfSchema.test.tsx @@ -53,5 +53,30 @@ describe('Components', () => { expect(component.render()).toMatchSnapshot(); }); }); + + describe('Show minProperties/maxProperties constraints oneOf', () => { + const schema = new SchemaModel( + parser, + { + oneOf: [ + { + type: 'object', + description: 'Test description', + minProperties: 1, + maxProperties: 1, + additionalProperties: { + type: 'string', + description: 'The name and value o', + }, + }, + ], + }, + '', + options, + ); + + const component = shallow(withTheme()); + expect(component.html().includes('= 1 properties')).toBe(true); + }); }); }); diff --git a/src/components/__tests__/Schema.test.tsx b/src/components/__tests__/Schema.test.tsx new file mode 100644 index 00000000..919e416f --- /dev/null +++ b/src/components/__tests__/Schema.test.tsx @@ -0,0 +1,67 @@ +/* tslint:disable:no-implicit-dependencies */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; + +import { Schema } from '../'; +import { OpenAPIParser, SchemaModel } from '../../services'; +import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions'; +import { withTheme } from '../testProviders'; + +const options = new RedocNormalizedOptions({}); +describe('Components', () => { + describe('SchemaView', () => { + const parser = new OpenAPIParser( + { openapi: '3.0', info: { title: 'test', version: '0' }, paths: {} }, + undefined, + options, + ); + + describe('Show minProperties/maxProperties constraints', () => { + const schema = new SchemaModel( + parser, + { + properties: { + name: { + type: 'object', + minProperties: 1, + properties: { + address: { + type: 'string', + }, + }, + }, + }, + }, + '', + options, + ); + const component = shallow(withTheme()); + expect(component.html().includes('non-empty')).toBe(true); + }); + + describe('Show range minProperties/maxProperties constraints', () => { + const schema = new SchemaModel( + parser, + { + properties: { + name: { + type: 'object', + minProperties: 2, + maxProperties: 10, + additionalProperties: { + type: 'string', + }, + }, + }, + }, + '', + options, + ); + it('should includes [ 2 .. 10 ] properties', () => { + const component = shallow(withTheme()); + expect(component.html().includes('[ 2 .. 10 ] properties')).toBe(true); + }); + }); + }); +}); diff --git a/src/components/__tests__/SecurityRequirement.test.tsx b/src/components/__tests__/SecurityRequirement.test.tsx index 097777a6..8d5e5761 100644 --- a/src/components/__tests__/SecurityRequirement.test.tsx +++ b/src/components/__tests__/SecurityRequirement.test.tsx @@ -1,28 +1,72 @@ import * as React from 'react'; -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; -import { OpenAPIParser } from '../../services'; -import { SecurityRequirementModel } from '../../services/models/SecurityRequirement'; -import { SecurityRequirement } from '../SecurityRequirement/SecurityRequirement'; -import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions'; +import { + createStore, + OpenAPIParser, + OperationModel, + RedocNormalizedOptions, + SecuritySchemesModel, +} from '../../services'; +import { StoreProvider } from '../StoreBuilder'; +import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement'; +import { withTheme } from '../testProviders'; +import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; +import * as simpleSecurityFixture from './fixtures/simple-security-fixture.json'; -const options = new RedocNormalizedOptions({}); -describe('Components', () => { - describe('SecurityRequirement', () => { - describe('SecurityRequirement', () => { - it("should render 'None' when empty object in security open api", () => { - const parser = new OpenAPIParser( - { openapi: '3.0', info: { title: 'test', version: '0' }, paths: {} }, - undefined, - options, +describe('SecurityRequirement', () => { + it('should render authDefinition', async () => { + const store = await createStore(simpleSecurityFixture, undefined, { + showSecuritySchemeType: true, + }); + + store.spec.contentItems.forEach((item: OperationModel) => { + if (item.security) { + const component = mount( + withTheme( + + , + , + ), ); - const securityRequirement = new SecurityRequirementModel({}, parser); - const securityElement = shallow( - , - ).getElement(); - expect(securityElement.props.children.type.target).toEqual('span'); - expect(securityElement.props.children.props.children).toEqual('None'); - }); + expect(component.html()).toMatchSnapshot(); + component.find('svg').simulate('click'); + //Security expanded + expect(component.html()).toMatchSnapshot(); + } + }); + }); + + it('should render SecurityDefs', async () => { + const parser = new OpenAPIParser( + simpleSecurityFixture, + undefined, + new RedocNormalizedOptions({}), + ); + + const component = mount( + withTheme(), + ); + expect(component.html()).toMatchSnapshot(); + }); + + it('should hide authDefinition', async () => { + const store = await createStore(simpleSecurityFixture, undefined, { + hideSecuritySection: true, + }); + + store.spec.contentItems.forEach((item: OperationModel) => { + if (item.security) { + const component = mount( + withTheme( + + , + , + ), + ); + expect(component.html().includes('Authorizations')).toBe(false); + expect(component.html().includes('svg')).toBe(false); + } }); }); }); diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index 75f0ac05..11ed8b7b 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -89,6 +89,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -96,7 +97,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -107,6 +107,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -273,6 +274,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -345,6 +348,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -352,7 +356,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -363,6 +366,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -529,6 +533,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -576,6 +582,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -583,7 +590,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -594,6 +600,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -760,6 +767,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -874,6 +883,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -881,7 +891,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -892,6 +901,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -1058,6 +1068,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -1130,6 +1142,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -1137,7 +1150,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -1148,6 +1160,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -1314,6 +1327,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -1361,6 +1376,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -1368,7 +1384,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -1379,6 +1394,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -1545,6 +1561,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -1615,6 +1633,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -1622,7 +1641,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -1633,6 +1651,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -1799,6 +1818,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -1910,6 +1931,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -1917,7 +1939,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -1928,6 +1949,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -2094,6 +2116,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -2166,6 +2190,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -2173,7 +2198,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -2184,6 +2208,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -2350,6 +2375,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, @@ -2397,6 +2424,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "hideHostname": false, "hideSchemaPattern": false, "hideSchemaTitles": false, + "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, "jsonSampleExpandLevel": 2, @@ -2404,7 +2432,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "menuToggle": true, "minCharacterLengthToInitSearch": 3, "nativeScrollbars": false, - "noAutoAuth": false, "nonce": undefined, "onlyRequiredInSamples": false, "pathInMiddlePanel": false, @@ -2415,6 +2442,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sectionsAtTheEnd": Array [], "showExtensions": false, "showObjectSchemaExamples": false, + "showSecuritySchemeType": false, "showWebhookVerb": false, "sideNavStyle": "summary-only", "simpleOneOfTypeLabel": false, @@ -2581,6 +2609,8 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "links": Object { "color": "#32329f", "hover": "#6868cf", + "hoverTextDecoration": "auto", + "textDecoration": "auto", "visited": "#32329f", }, "optimizeSpeed": true, diff --git a/src/components/__tests__/__snapshots__/OneOfSchema.test.tsx.snap b/src/components/__tests__/__snapshots__/OneOfSchema.test.tsx.snap index aa2d28b9..79b0c2df 100644 --- a/src/components/__tests__/__snapshots__/OneOfSchema.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/OneOfSchema.test.tsx.snap @@ -44,7 +44,7 @@ exports[`Components SchemaView OneOf deprecated should match snapshot 1`] = `
    diff --git a/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap new file mode 100644 index 00000000..506b6dd9 --- /dev/null +++ b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SecurityRequirement should render SecurityDefs 1`] = ` +"

    petstore_auth

    Get access to data while protecting your account credentials. +OAuth2 is also a safer and more secure way to give you access.

    +
    Security Scheme Type: OAuth2
    Flow type: implicit
    Scopes:
    • write:pets -

      modify pets in your account

      +
    • read:pets -

      read your pets

      +

    GitLab_PersonalAccessToken

    GitLab Personal Access Token description

    +
    Security Scheme Type: API Key
    Header parameter name: PRIVATE-TOKEN

    GitLab_OpenIdConnect

    GitLab OpenIdConnect description

    +
    Security Scheme Type: OpenID Connect

    basicAuth

    Security Scheme Type: HTTP
    HTTP Authorization Scheme: basic
    " +`; + +exports[`SecurityRequirement should render authDefinition 1`] = `"
    Authorizations:
    (API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth
    ,"`; + +exports[`SecurityRequirement should render authDefinition 2`] = ` +"
    Authorizations:
    (API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth (write:petsread:pets)
    OAuth2: petstore_auth

    Get access to data while protecting your account credentials. +OAuth2 is also a safer and more secure way to give you access.

    +
    Flow type: implicit
    Required scopes: write:pets read:pets
    Scopes:
    • write:pets -

      modify pets in your account

      +
    • read:pets -

      read your pets

      +
    API Key: GitLab_PersonalAccessToken

    GitLab Personal Access Token description

    +
    Header parameter name: PRIVATE-TOKEN
    OpenID Connect: GitLab_OpenIdConnect

    GitLab OpenIdConnect description

    +
    HTTP: basicAuth
    HTTP Authorization Scheme: basic
    ," +`; diff --git a/src/components/__tests__/fixtures/simple-security-fixture.json b/src/components/__tests__/fixtures/simple-security-fixture.json new file mode 100644 index 00000000..084ef84a --- /dev/null +++ b/src/components/__tests__/fixtures/simple-security-fixture.json @@ -0,0 +1,67 @@ +{ + "openapi": "3.0", + "info": { + "title": "test", + "version": "0" + }, + "paths": { + "/pet": { + "put": { + "summary": "Add a new pet to the store", + "description": "Add new pet to the store inventory.", + "operationId": "updatePet", + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "GitLab_PersonalAccessToken": [], + "GitLab_OpenIdConnect": [], + "basicAuth": [] + }, + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "petstore_auth": { + "description": "Get access to data while protecting your account credentials.\nOAuth2 is also a safer and more secure way to give you access.\n", + "type": "oauth2", + "bearerFormat": "", + "flows": { + "implicit": { + "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "GitLab_PersonalAccessToken": { + "description": "GitLab Personal Access Token description", + "type": "apiKey", + "name": "PRIVATE-TOKEN", + "in": "header", + "bearerFormat": "", + "flows": {} + }, + "GitLab_OpenIdConnect": { + "description": "GitLab OpenIdConnect description", + "bearerFormat": "", + "type": "openIdConnect", + "openIdConnectUrl": "https://gitlab.com/.well-known/openid-configuration" + }, + "basicAuth": { + "type": "http", + "scheme": "basic" + } + } + } +} diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts index 76805419..6c3457cd 100644 --- a/src/services/AppStore.ts +++ b/src/services/AppStore.ts @@ -12,11 +12,7 @@ import { SearchStore } from './SearchStore'; import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition'; import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes'; -import { - SCHEMA_DEFINITION_JSX_NAME, - SECURITY_DEFINITIONS_COMPONENT_NAME, - SECURITY_DEFINITIONS_JSX_NAME, -} from '../utils/openapi'; +import { SCHEMA_DEFINITION_JSX_NAME, SECURITY_DEFINITIONS_JSX_NAME } from '../utils/openapi'; import { IS_BROWSER } from '../utils'; @@ -158,12 +154,6 @@ export class AppStore { const DEFAULT_OPTIONS: RedocRawOptions = { allowedMdComponents: { - [SECURITY_DEFINITIONS_COMPONENT_NAME]: { - component: SecurityDefs, - propsSelector: (store: AppStore) => ({ - securitySchemes: store.spec.securitySchemes, - }), - }, [SECURITY_DEFINITIONS_JSX_NAME]: { component: SecurityDefs, propsSelector: (store: AppStore) => ({ diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 2ab4c5ee..7dd8c621 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -7,13 +7,7 @@ import { OpenAPIServer, OpenAPIPaths, } from '../types'; -import { - isOperationName, - SECURITY_DEFINITIONS_COMPONENT_NAME, - setSecuritySchemePrefix, - JsonPointer, - alphabeticallyByProp, -} from '../utils'; +import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils'; import { MarkdownRenderer } from './MarkdownRenderer'; import { GroupModel, OperationModel } from './models'; import { OpenAPIParser } from './OpenAPIParser'; @@ -110,14 +104,7 @@ export class MenuBuilder { if (heading.items) { group.items = mapHeadingsDeep(group, heading.items, depth + 1); } - if ( - MarkdownRenderer.containsComponent( - group.description || '', - SECURITY_DEFINITIONS_COMPONENT_NAME, - ) - ) { - setSecuritySchemePrefix(group.id + '/'); - } + return group; }); diff --git a/src/services/MenuStore.ts b/src/services/MenuStore.ts index b100b139..70671436 100644 --- a/src/services/MenuStore.ts +++ b/src/services/MenuStore.ts @@ -147,6 +147,7 @@ export class MenuStore { let item: IMenuItem | undefined; item = this.flatItems.find(i => i.id === id); + if (item) { this.activateAndScroll(item, false); } else { diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index 66536997..15cd5239 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -1,14 +1,8 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types'; -import { appendToMdHeading, isArray, IS_BROWSER } from '../utils/'; +import { isArray, isBoolean, IS_BROWSER } from '../utils/'; import { JsonPointer } from '../utils/JsonPointer'; -import { - getDefinitionName, - isNamedDefinition, - SECURITY_DEFINITIONS_COMPONENT_NAME, - SECURITY_DEFINITIONS_JSX_NAME, -} from '../utils/openapi'; -import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer'; +import { getDefinitionName, isNamedDefinition } from '../utils/openapi'; import { RedocNormalizedOptions } from './RedocNormalizedOptions'; export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] }; @@ -53,7 +47,6 @@ export class OpenAPIParser { private options: RedocNormalizedOptions = new RedocNormalizedOptions({}), ) { this.validate(spec); - this.preprocess(spec); this.spec = spec; this.allowMergeRefs = spec.openapi.startsWith('3.1'); @@ -70,25 +63,6 @@ export class OpenAPIParser { } } - preprocess(spec: OpenAPISpec) { - if ( - !this.options.noAutoAuth && - spec.info && - spec.components && - spec.components.securitySchemes - ) { - // Automatically inject Authentication section with SecurityDefinitions component - const description = spec.info.description || ''; - if ( - !MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME) && - !MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_JSX_NAME) - ) { - const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME); - spec.info.description = appendToMdHeading(description, 'Authentication', comment); - } - } - } - /** * get spec part by JsonPointer ($ref) */ @@ -268,29 +242,47 @@ export class OpenAPIParser { }>; for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) { - if ( - receiver.type !== subSchema.type && - receiver.type !== undefined && - subSchema.type !== undefined - ) { - console.warn( - `Incompatible types in allOf at "${$ref}": "${receiver.type}" and "${subSchema.type}"`, - ); + const { + type, + enum: enumProperty, + properties, + items, + required, + oneOf, + anyOf, + title, + ...otherConstraints + } = subSchema; + + if (receiver.type !== type && receiver.type !== undefined && type !== undefined) { + console.warn(`Incompatible types in allOf at "${$ref}": "${receiver.type}" and "${type}"`); } - if (subSchema.type !== undefined) { - receiver.type = subSchema.type; + if (type !== undefined) { + if (Array.isArray(type) && Array.isArray(receiver.type)) { + receiver.type = [...type, ...receiver.type]; + } else { + receiver.type = type; + } } - if (subSchema.properties !== undefined) { + if (enumProperty !== undefined) { + if (Array.isArray(enumProperty) && Array.isArray(receiver.enum)) { + receiver.enum = [...enumProperty, ...receiver.enum]; + } else { + receiver.enum = enumProperty; + } + } + + if (properties !== undefined) { receiver.properties = receiver.properties || {}; - for (const prop in subSchema.properties) { + for (const prop in properties) { if (!receiver.properties[prop]) { - receiver.properties[prop] = subSchema.properties[prop]; + receiver.properties[prop] = properties[prop]; } else { // merge inner properties const mergedProp = this.mergeAllOf( - { allOf: [receiver.properties[prop], subSchema.properties[prop]] }, + { allOf: [receiver.properties[prop], properties[prop]] }, $ref + '/properties/' + prop, ); receiver.properties[prop] = mergedProp; @@ -299,22 +291,37 @@ export class OpenAPIParser { } } - if (subSchema.items !== undefined) { - receiver.items = receiver.items || {}; + if (items !== undefined) { + const receiverItems = isBoolean(receiver.items) + ? { items: receiver.items } + : receiver.items + ? (Object.assign({}, receiver.items) as OpenAPISchema) + : {}; + const subSchemaItems = isBoolean(items) + ? { items } + : (Object.assign({}, items) as OpenAPISchema); // merge inner properties receiver.items = this.mergeAllOf( - { allOf: [receiver.items, subSchema.items] }, + { allOf: [receiverItems, subSchemaItems] }, $ref + '/items', ); } - if (subSchema.required !== undefined) { - receiver.required = (receiver.required || []).concat(subSchema.required); + if (required !== undefined) { + receiver.required = (receiver.required || []).concat(required); + } + + if (oneOf !== undefined) { + receiver.oneOf = oneOf; + } + + if (anyOf !== undefined) { + receiver.anyOf = anyOf; } // merge rest of constraints // TODO: do more intelligent merge - receiver = { ...subSchema, ...receiver }; + receiver = { ...receiver, title: receiver.title || title, ...otherConstraints }; if (subSchemaRef) { receiver.parentRefs!.push(subSchemaRef); diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 9ad5b559..2f7666cb 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -21,7 +21,6 @@ export interface RedocRawOptions { sortEnumValuesAlphabetically?: boolean | string; sortOperationsAlphabetically?: boolean | string; sortTagsAlphabetically?: boolean | string; - noAutoAuth?: boolean | string; nativeScrollbars?: boolean | string; pathInMiddlePanel?: boolean | string; untrustedSpec?: boolean | string; @@ -42,6 +41,8 @@ export interface RedocRawOptions { expandSingleSchemaField?: boolean | string; schemaExpansionLevel?: number | string | 'all'; showObjectSchemaExamples?: boolean | string; + showSecuritySchemeType?: boolean; + hideSecuritySection?: boolean; sectionsAtTheEnd?: string | string[]; unstable_ignoreMimeParameters?: boolean; @@ -233,7 +234,6 @@ export class RedocNormalizedOptions { sortEnumValuesAlphabetically: boolean; sortOperationsAlphabetically: boolean; sortTagsAlphabetically: boolean; - noAutoAuth: boolean; nativeScrollbars: boolean; pathInMiddlePanel: boolean; untrustedSpec: boolean; @@ -254,6 +254,8 @@ export class RedocNormalizedOptions { expandSingleSchemaField: boolean; schemaExpansionLevel: number; showObjectSchemaExamples: boolean; + showSecuritySchemeType?: boolean; + hideSecuritySection?: boolean; sectionsAtTheEnd: string[]; /* tslint:disable-next-line */ @@ -304,7 +306,6 @@ export class RedocNormalizedOptions { this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically); this.sortOperationsAlphabetically = argValueToBoolean(raw.sortOperationsAlphabetically); this.sortTagsAlphabetically = argValueToBoolean(raw.sortTagsAlphabetically); - this.noAutoAuth = argValueToBoolean(raw.noAutoAuth); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); this.untrustedSpec = argValueToBoolean(raw.untrustedSpec); @@ -327,6 +328,8 @@ export class RedocNormalizedOptions { this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField); this.schemaExpansionLevel = argValueToExpandLevel(raw.schemaExpansionLevel); this.showObjectSchemaExamples = argValueToBoolean(raw.showObjectSchemaExamples); + this.showSecuritySchemeType = argValueToBoolean(raw.showSecuritySchemeType); + this.hideSecuritySection = argValueToBoolean(raw.hideSecuritySection); this.sectionsAtTheEnd = RedocNormalizedOptions.normalizeSectionsAtTheEnd(raw.sectionsAtTheEnd); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); diff --git a/src/services/__tests__/OpenAPIParser.test.ts b/src/services/__tests__/OpenAPIParser.test.ts index 7942b7b1..24fbc330 100644 --- a/src/services/__tests__/OpenAPIParser.test.ts +++ b/src/services/__tests__/OpenAPIParser.test.ts @@ -9,14 +9,50 @@ describe('Models', () => { let parser; test('should hoist oneOfs when mergin allOf', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const spec = require('./fixtures/oneOfHoist.json'); parser = new OpenAPIParser(spec, undefined, opts); expect(parser.mergeAllOf(spec.components.schemas.test)).toMatchSnapshot(); }); + test('should get schema name from named schema', () => { + const spec = require('./fixtures/mergeAllOf.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = parser.mergeAllOf(spec.components.schemas.Case1, '#/components/schemas/Case1'); + expect(schema.title).toEqual('Case1'); + }); + + test('should get schema name from first allOf', () => { + const spec = require('./fixtures/mergeAllOf.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = parser.mergeAllOf( + spec.components.schemas.Case2.properties.a, + '#components/schemas/Case2/properties/a', + ); + expect(schema.title).toEqual('Bar'); + }); + + test('should get schema name from named schema', () => { + const spec = require('./fixtures/mergeAllOf.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = parser.mergeAllOf( + spec.components.schemas.Case3.schemas.Foo, + '#components/schemas/Case3/schemas/Foo', + ); + expect(schema.title).toEqual('Foo'); + }); + + test('should merge oneOff to inside allOff', () => { + // TODO: should hoist + const spec = require('./fixtures/mergeAllOf.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = parser.mergeAllOf(spec.components.schemas.Case4); + expect(schema.title).toEqual('Foo'); + expect(schema.parentRefs).toHaveLength(1); + expect(schema.parentRefs[0]).toEqual('#/components/schemas/Ref'); + expect(schema.oneOf).toEqual([{ title: 'Bar' }, { title: 'Baz' }]); + }); + test('should override description from $ref of the referenced component, when sibling description exists ', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const spec = require('./fixtures/siblingRefDescription.json'); parser = new OpenAPIParser(spec, undefined, opts); const schemaOrRef: Referenced = { @@ -28,7 +64,6 @@ describe('Models', () => { }); test('should correct resolve double $ref if no need sibling', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const spec = require('./fixtures/3.1/schemaDefinition.json'); parser = new OpenAPIParser(spec, undefined, opts); const schemaOrRef: Referenced = { diff --git a/src/services/__tests__/fixtures/3.1/conditionalField.json b/src/services/__tests__/fixtures/3.1/conditionalField.json new file mode 100644 index 00000000..3c010f5d --- /dev/null +++ b/src/services/__tests__/fixtures/3.1/conditionalField.json @@ -0,0 +1,40 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Schema definition field with conditional operators", + "version": "1.0.0" + }, + "components": { + "schemas": { + "Test": { + "type": "object", + "properties": { + "test": { + "type": ["string", "integer", "null"], + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "format": "url" + }, + "if": { + "x-displayName": "isString", + "type": "string" + }, + "then": { + "type": "string", + "minItems": 1, + "maxItems": 20 + }, + "else": { + "x-displayName": "notString", + "minItems": 1, + "maxItems": 10, + "pattern": "\\d+" + } + } + } + } + } + } +} diff --git a/src/services/__tests__/fixtures/3.1/conditionalSchema.json b/src/services/__tests__/fixtures/3.1/conditionalSchema.json new file mode 100644 index 00000000..452747ae --- /dev/null +++ b/src/services/__tests__/fixtures/3.1/conditionalSchema.json @@ -0,0 +1,40 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Schema definition with conditional operators", + "version": "1.0.0" + }, + "components": { + "schemas": { + "Test": { + "type": "object", + "properties": { + "test": { + "description": "The list of URL to a cute photos featuring pet", + "type": ["string", "integer", "null"], + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "format": "url" + } + } + }, + "if": { + "title": "=== 10", + "properties": { + "test": { + "enum": [10] + } + } + }, + "then": { + "maxItems": 2 + }, + "else": { + "maxItems": 20 + } + } + } + } +} diff --git a/src/services/__tests__/fixtures/3.1/patternProperties.json b/src/services/__tests__/fixtures/3.1/patternProperties.json new file mode 100644 index 00000000..ec686421 --- /dev/null +++ b/src/services/__tests__/fixtures/3.1/patternProperties.json @@ -0,0 +1,32 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Schema definition with unevaluatedProperties", + "version": "1.0.0" + }, + "servers": [ + { + "url": "example.com" + } + ], + "components": { + "schemas": { + "Patterns": { + "type": "object", + "patternProperties": { + "^S_\\w+\\.[1-9]{2,4}$": { + "type": "string" + }, + "^O_\\w+\\.[1-9]{2,4}$": { + "type": "object", + "properties": { + "x-nestedProperty": { + "type": "string" + } + } + } + } + } + } + } +} diff --git a/src/services/__tests__/fixtures/3.1/prefixItems.json b/src/services/__tests__/fixtures/3.1/prefixItems.json new file mode 100644 index 00000000..63a4d078 --- /dev/null +++ b/src/services/__tests__/fixtures/3.1/prefixItems.json @@ -0,0 +1,154 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Schema definition with prefixItems", + "version": "1.0.0" + }, + "servers": [ + { + "url": "example.com" + } + ], + "components": { + "schemas": { + "Case1": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "prefixItems": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "items": false + }, + "Case2": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "prefixItems": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "items": true + }, + "Case3": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "prefixItems": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "items": { + "$ref": "#/components/schemas/Dog" + } + }, + "Case4": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "prefixItems": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "items": { + "type": "object", + "properties": { + "firstItem": { + "type": "string" + } + } + } + }, + "Case5": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "prefixItems": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "items": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 0 + } + ] + } + }, + "Cat": { + "type": "object", + "properties": { + "color": { + "type": "string" + } + } + }, + "Dog": { + "type": "object", + "properties": { + "size": { + "type": "string" + } + } + } + } + } +} diff --git a/src/services/__tests__/fixtures/arrayItems.json b/src/services/__tests__/fixtures/arrayItems.json new file mode 100644 index 00000000..ebe25f34 --- /dev/null +++ b/src/services/__tests__/fixtures/arrayItems.json @@ -0,0 +1,154 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Schema definition with array items", + "version": "1.0.0" + }, + "servers": [ + { + "url": "example.com" + } + ], + "components": { + "schemas": { + "Case1": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "additionalItems": false + }, + "Case2": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "additionalItems": true + }, + "Case3": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "additionalItems": { + "$ref": "#/components/schemas/Dog" + } + }, + "Case4": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "additionalItems": { + "type": "object", + "properties": { + "firstItem": { + "type": "string" + } + } + } + }, + "Case5": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": [ + { + "type": "string", + "minLength": 0, + "maxLength": 10 + }, + { + "type": "number", + "minimum": 0, + "maximum": 10 + }, + { + "$ref": "#/components/schemas/Cat" + } + ], + "additionalItems": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 0 + } + ] + } + }, + "Cat": { + "type": "object", + "properties": { + "color": { + "type": "string" + } + } + }, + "Dog": { + "type": "object", + "properties": { + "size": { + "type": "string" + } + } + } + } + } +} diff --git a/src/services/__tests__/fixtures/mergeAllOf.json b/src/services/__tests__/fixtures/mergeAllOf.json new file mode 100644 index 00000000..604a015c --- /dev/null +++ b/src/services/__tests__/fixtures/mergeAllOf.json @@ -0,0 +1,70 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0", + "title": "Foo" + }, + "components": { + "schemas": { + "Case1": { + "allOf": [ + { + "title": "Bar" + }, + { + "title": "Baz" + } + ] + }, + "Case2": { + "properties": { + "a": { + "allOf": [ + { + "title": "Bar" + }, + { + "title": "Baz" + } + ] + } + } + }, + "Case3": { + "schemas": { + "Foo": { + "title": "Foo", + "allOf": [ + { + "title": "Bar" + }, + { + "title": "Baz" + } + ] + } + } + }, + "Case4": { + "allOf": [ + { + "title": "Foo" + }, + { + "$ref": "#/components/schemas/Ref" + } + ] + }, + "Ref": { + "oneOf": [ + { + "title": "Bar" + }, + { + "title": "Baz" + } + ] + } + } + } +} diff --git a/src/services/__tests__/models/Schema.test.ts b/src/services/__tests__/models/Schema.test.ts index 966de1af..3dfbf6f8 100644 --- a/src/services/__tests__/models/Schema.test.ts +++ b/src/services/__tests__/models/Schema.test.ts @@ -49,6 +49,32 @@ describe('Models', () => { expect(schema.pointer).toBe('#/components/schemas/Child'); }); + test('schemaDefinition should resolve schema with conditional operators', () => { + const spec = require('../fixtures/3.1/conditionalSchema.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts); + expect(schema.oneOf).toHaveLength(2); + + expect(schema.oneOf![0].schema.title).toBe('=== 10'); + expect(schema.oneOf![1].schema.title).toBe('case 2'); + + expect(schema.oneOf![0].schema).toMatchSnapshot(); + expect(schema.oneOf![1].schema).toMatchSnapshot(); + }); + + test('schemaDefinition should resolve field with conditional operators', () => { + const spec = require('../fixtures/3.1/conditionalField.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts); + expect(schema.fields).toHaveLength(1); + expect(schema.fields && schema.fields[0].schema.oneOf).toHaveLength(2); + expect(schema.fields && schema.fields[0].schema.oneOf![0].schema.title).toBe('isString'); + expect(schema.fields && schema.fields[0].schema.oneOf![1].schema.title).toBe('notString'); + + expect(schema.fields && schema.fields[0].schema.oneOf![0].schema).toMatchSnapshot(); + expect(schema.fields && schema.fields[0].schema.oneOf![1].schema).toMatchSnapshot(); + }); + test('schemaDefinition should resolve unevaluatedProperties in properties', () => { const spec = require('../fixtures/3.1/unevaluatedProperties.json'); parser = new OpenAPIParser(spec, undefined, opts); @@ -76,5 +102,116 @@ describe('Models', () => { expect(schema.fields![1].kind).toEqual('additionalProperties'); expect(schema.fields![1].schema.type).toEqual('boolean'); }); + + test('schemaDefinition should resolve patternProperties', () => { + const spec = require('../fixtures/3.1/patternProperties.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Patterns, '', opts); + expect(schema.fields).toHaveLength(2); + expect(schema.fields![0].kind).toEqual('patternProperties'); + expect(schema.fields![0].schema.type).toEqual('string'); + expect(schema.fields![1].kind).toEqual('patternProperties'); + expect(schema.fields![1].schema.type).toEqual('object'); + }); + + describe('type array', () => { + function testImmutablePart(schema: SchemaModel) { + expect(schema.minItems).toEqual(1); + expect(schema.maxItems).toEqual(10); + expect(schema.fields![0].schema.type).toEqual('string'); + expect(schema.fields![1].schema.type).toEqual('number'); + } + const eachArray = ['../fixtures/3.1/prefixItems.json', '../fixtures/arrayItems.json']; + + test.each(eachArray)( + 'schemaDefinition should resolve prefixItems without additional items', + specFixture => { + const spec = require(specFixture); + const parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Case1, '', opts); + + testImmutablePart(schema); + + expect(schema.fields).toHaveLength(3); + expect(schema.fields![2].name).toEqual('[2]'); + expect(schema.fields![2].schema.pointer).toEqual('#/components/schemas/Cat'); + expect(schema.fields![2].schema.type).toEqual('object'); + }, + ); + + test.each(eachArray)( + 'schemaDefinition should resolve prefixItems with additional items', + specFixture => { + const spec = require(specFixture); + const parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Case2, '', opts); + + testImmutablePart(schema); + + expect(schema.fields).toHaveLength(4); + expect(schema.fields![3].name).toEqual('[3...]'); + expect(schema.fields![2].schema.type).toEqual('object'); + expect(schema.fields![2].schema.pointer).toEqual('#/components/schemas/Cat'); + expect(schema.fields![3].schema.type).toEqual('any'); + }, + ); + + test.each(eachArray)( + 'schemaDefinition should resolve prefixItems with additional items with $ref', + specFixture => { + const spec = require(specFixture); + const parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Case3, '', opts); + + testImmutablePart(schema); + + expect(schema.fields).toHaveLength(4); + expect(schema.fields![3].name).toEqual('[3...]'); + expect(schema.fields![2].schema.type).toEqual('object'); + expect(schema.fields![2].schema.pointer).toEqual('#/components/schemas/Cat'); + expect(schema.fields![3].schema.type).toEqual('object'); + expect(schema.fields![3].schema.pointer).toEqual('#/components/schemas/Dog'); + }, + ); + + test.each(eachArray)( + 'schemaDefinition should resolve prefixItems with additional schema items', + specFixture => { + const spec = require(specFixture); + const parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Case4, '', opts); + + testImmutablePart(schema); + + expect(schema.fields).toHaveLength(4); + expect(schema.fields![3].name).toEqual('[3...]'); + expect(schema.fields![2].schema.type).toEqual('object'); + expect(schema.fields![2].schema.pointer).toEqual('#/components/schemas/Cat'); + expect(schema.fields![3].schema.type).toEqual('object'); + }, + ); + + test.each(eachArray)( + 'schemaDefinition should resolve prefixItems with additional array items', + specFixture => { + const spec = require(specFixture); + const parser = new OpenAPIParser(spec, undefined, opts); + const schema = new SchemaModel(parser, spec.components.schemas.Case5, '', opts); + + testImmutablePart(schema); + + expect(schema.fields).toHaveLength(4); + expect(schema.fields![3].name).toEqual('[3...]'); + expect(schema.fields![2].schema.type).toEqual('object'); + expect(schema.fields![2].schema.pointer).toEqual('#/components/schemas/Cat'); + expect(schema.fields![3].schema.type).toEqual('array'); + expect(schema.fields![3].schema.fields).toHaveLength(1); + expect(schema.fields![3].schema.fields![0].schema.type).toEqual('string'); + expect(schema.fields![3].schema.fields![0].schema.constraints).toEqual([ + '>= 0 characters', + ]); + }, + ); + }); }); }); diff --git a/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap new file mode 100644 index 00000000..b0f6c73e --- /dev/null +++ b/src/services/__tests__/models/__snapshots__/Schema.test.ts.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Models Schema schemaDefinition should resolve field with conditional operators 1`] = ` +Object { + "allOf": undefined, + "default": undefined, + "items": Object { + "allOf": undefined, + "format": "url", + "parentRefs": Array [], + "title": undefined, + "type": "string", + }, + "maxItems": 20, + "minItems": 1, + "parentRefs": Array [], + "title": "isString", + "type": "string", + "x-displayName": "isString", +} +`; + +exports[`Models Schema schemaDefinition should resolve field with conditional operators 2`] = ` +Object { + "allOf": undefined, + "default": undefined, + "items": Object { + "allOf": undefined, + "format": "url", + "parentRefs": Array [], + "title": undefined, + "type": "string", + }, + "maxItems": 10, + "minItems": 1, + "parentRefs": Array [], + "pattern": "\\\\d+", + "title": "notString", + "type": Array [ + "string", + "integer", + "null", + ], + "x-displayName": "notString", +} +`; + +exports[`Models Schema schemaDefinition should resolve schema with conditional operators 1`] = ` +Object { + "allOf": undefined, + "maxItems": 2, + "parentRefs": Array [], + "properties": Object { + "test": Object { + "allOf": undefined, + "description": "The list of URL to a cute photos featuring pet", + "enum": Array [ + 10, + ], + "items": Object { + "allOf": undefined, + "format": "url", + "parentRefs": Array [], + "title": undefined, + "type": "string", + }, + "maxItems": 20, + "minItems": 1, + "parentRefs": Array [], + "title": undefined, + "type": Array [ + "string", + "integer", + "null", + ], + }, + }, + "title": "=== 10", + "type": "object", +} +`; + +exports[`Models Schema schemaDefinition should resolve schema with conditional operators 2`] = ` +Object { + "allOf": undefined, + "maxItems": 20, + "parentRefs": Array [], + "properties": Object { + "test": Object { + "description": "The list of URL to a cute photos featuring pet", + "items": Object { + "format": "url", + "type": "string", + }, + "maxItems": 20, + "minItems": 1, + "type": Array [ + "string", + "integer", + "null", + ], + }, + }, + "title": "case 2", + "type": "object", +} +`; diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index c0754b4a..4816a149 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -12,7 +12,9 @@ import { extractExtensions, humanizeConstraints, isArray, + isBoolean, isNamedDefinition, + isObject, isPrimitiveType, JsonPointer, pluralizeType, @@ -152,6 +154,11 @@ export class SchemaModel { return; } + if ((schema.if && schema.then) || (schema.if && schema.else)) { + this.initConditionalOperators(schema, parser); + return; + } + if (!isChild && getDiscriminator(schema) !== undefined) { this.initDiscriminator(schema, parser); return; @@ -183,17 +190,31 @@ export class SchemaModel { if (this.hasType('object')) { this.fields = buildFields(parser, schema, this.pointer, this.options); - } else if (this.hasType('array') && schema.items) { - this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options); - this.displayType = pluralizeType(this.items.displayType); - this.displayFormat = this.items.format; - this.typePrefix = this.items.typePrefix + l('arrayOf'); - this.title = this.title || this.items.title; - this.isPrimitive = this.items.isPrimitive; - if (this.example === undefined && this.items.example !== undefined) { + } else if (this.hasType('array')) { + if (isArray(schema.items) || isArray(schema.prefixItems)) { + this.fields = buildFields(parser, schema, this.pointer, this.options); + } else if (isObject(schema.items)) { + this.items = new SchemaModel( + parser, + schema.items as OpenAPISchema, + this.pointer + '/items', + this.options, + ); + } + + this.displayType = + schema.prefixItems || isArray(schema.items) + ? 'items' + : pluralizeType(this.items?.displayType || this.displayType); + this.displayFormat = this.items?.format || ''; + this.typePrefix = this.items?.typePrefix || '' + l('arrayOf'); + this.title = this.title || this.items?.title || ''; + this.isPrimitive = this.items?.isPrimitive || this.isPrimitive; + + if (this.example === undefined && this.items?.example !== undefined) { this.example = [this.items.example]; } - if (this.items.isPrimitive) { + if (this.items?.isPrimitive) { this.enum = this.items.enum; } if (isArray(this.type)) { @@ -355,6 +376,38 @@ export class SchemaModel { return innerSchema; }); } + + private initConditionalOperators(schema: OpenAPISchema, parser: OpenAPIParser) { + const { + if: ifOperator, + else: elseOperator = {}, + then: thenOperator = {}, + ...restSchema + } = schema; + const groupedOperators = [ + { + allOf: [restSchema, thenOperator, ifOperator], + title: (ifOperator && ifOperator['x-displayName']) || ifOperator?.title || 'case 1', + }, + { + allOf: [restSchema, elseOperator], + title: (elseOperator && elseOperator['x-displayName']) || elseOperator?.title || 'case 2', + }, + ]; + + this.oneOf = groupedOperators.map( + (variant, idx) => + new SchemaModel( + parser, + { + ...variant, + } as OpenAPISchema, + this.pointer + '/oneOf/' + idx, + this.options, + ), + ); + this.oneOfType = 'One of'; + } } function buildFields( @@ -363,8 +416,10 @@ function buildFields( $ref: string, options: RedocNormalizedOptions, ): FieldModel[] { - const props = schema.properties || {}; + const props = schema.properties || schema.prefixItems || schema.items || {}; + const patternProps = schema.patternProperties || {}; const additionalProps = schema.additionalProperties || schema.unevaluatedProperties; + const itemsProps = schema.prefixItems ? schema.items : schema.additionalItems; const defaults = schema.default; let fields = Object.keys(props || []).map(fieldName => { let field = props[fieldName]; @@ -382,7 +437,7 @@ function buildFields( return new FieldModel( parser, { - name: fieldName, + name: schema.properties ? fieldName : `[${fieldName}]`, required, schema: { ...field, @@ -402,6 +457,31 @@ function buildFields( fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined); } + fields.push( + ...Object.keys(patternProps).map(fieldName => { + let field = patternProps[fieldName]; + + if (!field) { + console.warn( + `Field "${fieldName}" is invalid, skipping.\n Field must be an object but got ${typeof field} at "${$ref}"`, + ); + field = {}; + } + + return new FieldModel( + parser, + { + name: fieldName, + required: false, + schema: field, + kind: 'patternProperties', + }, + `${$ref}/patternProperties/${fieldName}`, + options, + ); + }), + ); + if (typeof additionalProps === 'object' || additionalProps === true) { fields.push( new FieldModel( @@ -421,9 +501,82 @@ function buildFields( ); } + fields.push( + ...buildAdditionalItems({ + parser, + schema: itemsProps, + fieldsCount: fields.length, + $ref, + options, + }), + ); + return fields; } +function buildAdditionalItems({ + parser, + schema = false, + fieldsCount, + $ref, + options, +}: { + parser: OpenAPIParser; + schema?: OpenAPISchema | OpenAPISchema[] | boolean; + fieldsCount: number; + $ref: string; + options: RedocNormalizedOptions; +}) { + if (isBoolean(schema)) { + return schema + ? [ + new FieldModel( + parser, + { + name: `[${fieldsCount}...]`, + schema: {}, + }, + `${$ref}/additionalItems`, + options, + ), + ] + : []; + } + + if (isArray(schema)) { + return [ + ...schema.map( + (field, idx) => + new FieldModel( + parser, + { + name: `[${fieldsCount + idx}]`, + schema: field, + }, + `${$ref}/additionalItems`, + options, + ), + ), + ]; + } + + if (isObject(schema)) { + return [ + new FieldModel( + parser, + { + name: `[${fieldsCount}...]`, + schema: schema, + }, + `${$ref}/additionalItems`, + options, + ), + ]; + } + + return []; +} + function getDiscriminator(schema: OpenAPISchema): OpenAPISchema['discriminator'] { return schema.discriminator || schema['x-discriminator']; } diff --git a/src/services/models/SecurityRequirement.ts b/src/services/models/SecurityRequirement.ts index 8525f499..f991abf0 100644 --- a/src/services/models/SecurityRequirement.ts +++ b/src/services/models/SecurityRequirement.ts @@ -1,5 +1,4 @@ import { OpenAPISecurityRequirement, OpenAPISecurityScheme } from '../../types'; -import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils/openapi'; import { OpenAPIParser } from '../OpenAPIParser'; export interface SecurityScheme extends OpenAPISecurityScheme { @@ -29,7 +28,7 @@ export class SecurityRequirementModel { return { ...scheme, id, - sectionId: SECURITY_SCHEMES_SECTION_PREFIX + id, + sectionId: id, displayName, scopes, }; diff --git a/src/services/models/SecuritySchemes.ts b/src/services/models/SecuritySchemes.ts index 67055cad..9b157d13 100644 --- a/src/services/models/SecuritySchemes.ts +++ b/src/services/models/SecuritySchemes.ts @@ -1,5 +1,5 @@ import { OpenAPISecurityScheme, Referenced } from '../../types'; -import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils/openapi'; +import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils'; import { OpenAPIParser } from '../OpenAPIParser'; export class SecuritySchemeModel { diff --git a/src/theme.ts b/src/theme.ts index e8f2bf2e..791ab4f4 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -128,6 +128,8 @@ const defaultTheme: ThemeInterface = { color: ({ colors }) => colors.primary.main, visited: ({ typography }) => typography.links.color, hover: ({ typography }) => lighten(0.2, typography.links.color), + textDecoration: 'auto', + hoverTextDecoration: 'auto', }, }, sidebar: { @@ -315,6 +317,8 @@ export interface ResolvedThemeInterface { color: string; visited: string; hover: string; + textDecoration: string; + hoverTextDecoration: string; }; }; sidebar: { diff --git a/src/types/open-api.ts b/src/types/open-api.ts index 98ce1f9e..afa98642 100644 --- a/src/types/open-api.ts +++ b/src/types/open-api.ts @@ -113,11 +113,12 @@ export interface OpenAPISchema { $ref?: string; type?: string | string[]; properties?: { [name: string]: OpenAPISchema }; + patternProperties?: { [name: string]: OpenAPISchema }; additionalProperties?: boolean | OpenAPISchema; unevaluatedProperties?: boolean | OpenAPISchema; description?: string; default?: any; - items?: OpenAPISchema; + items?: OpenAPISchema | OpenAPISchema[] | boolean; required?: string[]; readOnly?: boolean; writeOnly?: boolean; @@ -147,10 +148,16 @@ export interface OpenAPISchema { minProperties?: number; enum?: any[]; example?: any; + + if?: OpenAPISchema; + else?: OpenAPISchema; + then?: OpenAPISchema; examples?: any[]; const?: string; contentEncoding?: string; contentMediaType?: string; + prefixItems?: OpenAPISchema[]; + additionalItems?: OpenAPISchema | boolean; } export interface OpenAPIDiscriminator { diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index 8459b133..17fda88e 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -352,6 +352,37 @@ Object { }, "User": Object { "properties": Object { + "addresses": Object { + "additionalItems": Object { + "type": "string", + }, + "items": Array [ + Object { + "properties": Object { + "city": Object { + "minLength": 0, + "type": "string", + }, + "country": Object { + "minLength": 0, + "type": "string", + }, + "street": Object { + "description": "includes build/apartment number", + "minLength": 0, + "type": "string", + }, + }, + "type": "object", + }, + Object { + "type": "number", + }, + ], + "maxLength": 10, + "minItems": 0, + "type": "array", + }, "email": Object { "description": "User email address", "example": "john.smith@example.com", @@ -966,6 +997,7 @@ try { "format": "int32", "type": "integer", }, + "minProperties": 2, "type": "object", }, }, @@ -1903,6 +1935,46 @@ Object { }, "schemas": Object { "ApiResponse": Object { + "patternProperties": Object { + "^O_\\\\\\\\w+\\\\\\\\.[1-9]{2,4}$": Object { + "properties": Object { + "nestedProperty": Object { + "default": "lazy", + "description": "The measured skill for hunting", + "enum": Array [ + "clueless", + "lazy", + "adventurous", + "aggressive", + ], + "example": "adventurous", + "type": Array [ + "string", + "boolean", + ], + }, + }, + "type": "object", + }, + "^S_\\\\\\\\w+\\\\\\\\.[1-9]{2,4}$": Object { + "description": "The measured skill for hunting", + "else": Object { + "maxLength": 10, + "minLength": 1, + }, + "if": Object { + "x-displayName": "fieldName === 'status'", + }, + "then": Object { + "enum": Array [ + "success", + "failed", + ], + "format": "url", + "type": "string", + }, + }, + }, "properties": Object { "code": Object { "format": "int32", @@ -1934,7 +2006,10 @@ Object { "aggressive", ], "example": "adventurous", - "type": "string", + "type": Array [ + "string", + "boolean", + ], }, }, "required": Array [ @@ -2086,6 +2161,16 @@ Object { "friend": Object { "$ref": "#/components/schemas/Pet", }, + "huntingSkill": Object { + "enum": Array [ + 0, + 1, + 2, + ], + "type": Array [ + "integer", + ], + }, "id": Object { "$ref": "#/components/schemas/Id", "description": "Pet ID", @@ -2105,17 +2190,33 @@ Object { }, "photoUrls": Object { "description": "The list of URL to a cute photos featuring pet", + "else": Object { + "maxItems": 20, + "minItems": 1, + "type": Array [ + "integer", + "null", + ], + "x-displayName": "notString", + }, + "if": Object { + "type": "string", + "x-displayName": "isString", + }, "items": Object { "format": "url", "type": "string", }, - "maxItems": 20, + "maxItems": 10, "minItems": 1, + "then": Object { + "maxItems": 15, + "minItems": 1, + }, "type": Array [ "string", "integer", "null", - "array", ], "xml": Object { "name": "photoUrl", @@ -2173,7 +2274,51 @@ Object { }, }, "User": Object { + "else": Object { + "required": Array [], + }, + "if": Object { + "properties": Object { + "userStatus": Object { + "enum": Array [ + 10, + ], + }, + }, + "title": "userStatus === 10", + }, "properties": Object { + "addresses": Object { + "items": Object { + "type": "string", + }, + "maxLength": 10, + "minItems": 0, + "prefixItems": Array [ + Object { + "properties": Object { + "city": Object { + "minLength": 0, + "type": "string", + }, + "country": Object { + "minLength": 0, + "type": "string", + }, + "street": Object { + "description": "includes build/apartment number", + "minLength": 0, + "type": "string", + }, + }, + "type": "object", + }, + Object { + "type": "number", + }, + ], + "type": "array", + }, "email": Object { "description": "User email address", "example": "john.smith@example.com", @@ -2238,6 +2383,11 @@ Object { "type": "string", }, }, + "then": Object { + "required": Array [ + "phone", + ], + }, "type": "object", "xml": Object { "name": "User", diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index e7d73b51..8e7aa3d8 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -277,7 +277,7 @@ describe('Utils', () => { expect(isPrimitiveType(schema)).toEqual(true); }); - it('Should return false for array of string which include the null value', () => { + it('Should return true for array of string which include the null value', () => { const schema = { type: ['object', 'string', 'null'], }; @@ -553,7 +553,7 @@ describe('Utils', () => { }); it('should have a humanized constraint when minItems and maxItems are the same', () => { - expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('7 items'); + expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('= 7 items'); }); it('should have a humanized constraint when justMinItems is set, and it is equal to 1', () => { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 35b19e52..16ed57c1 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -107,7 +107,7 @@ export const mergeObjects = (target: any, ...sources: any[]): any => { return mergeObjects(target, ...sources); }; -const isObject = (item: any): boolean => { +export const isObject = (item: unknown): item is Record => { return item !== null && typeof item === 'object'; }; @@ -210,6 +210,10 @@ export function unescapeHTMLChars(str: string): string { .replace(/"/g, '"'); } -export function isArray(value: unknown): value is Array { +export function isArray(value: unknown): value is any[] { return Array.isArray(value); } + +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index e8a0cc05..93eeb3d9 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -16,7 +16,7 @@ import { Referenced, } from '../types'; import { IS_BROWSER } from './dom'; -import { isNumeric, removeQueryString, resolveUrl, isArray } from './helpers'; +import { isNumeric, removeQueryString, resolveUrl, isArray, isBoolean } from './helpers'; function isWildcardStatusCode(statusCode: string | number): statusCode is string { return typeof statusCode === 'string' && /\dxx/i.test(statusCode); @@ -99,6 +99,7 @@ const schemaKeywordTypes = { additionalProperties: 'object', unevaluatedProperties: 'object', properties: 'object', + patternProperties: 'object', }; export function detectType(schema: OpenAPISchema): string { @@ -124,6 +125,10 @@ export function isPrimitiveType( return false; } + if ((schema.if && schema.then) || (schema.if && schema.else)) { + return false; + } + let isPrimitive = true; const isArrayType = isArray(type); @@ -134,8 +139,13 @@ export function isPrimitiveType( : schema.additionalProperties === undefined && schema.unevaluatedProperties === undefined; } + if (isArray(schema.items) || isArray(schema.prefixItems)) { + return false; + } + if ( schema.items !== undefined && + !isBoolean(schema.items) && (type === 'array' || (isArrayType && type?.includes('array'))) ) { isPrimitive = isPrimitiveType(schema.items, schema.items.type); @@ -418,7 +428,7 @@ function humanizeRangeConstraint( let stringRange; if (min !== undefined && max !== undefined) { if (min === max) { - stringRange = `${min} ${description}`; + stringRange = `= ${min} ${description}`; } else { stringRange = `[ ${min} .. ${max} ] ${description}`; } @@ -471,6 +481,15 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] { res.push(arrayRange); } + const propertiesRange = humanizeRangeConstraint( + 'properties', + schema.minProperties, + schema.maxProperties, + ); + if (propertiesRange !== undefined) { + res.push(propertiesRange); + } + const multipleOfConstraint = humanizeMultipleOfConstraint(schema.multipleOf); if (multipleOfConstraint !== undefined) { res.push(multipleOfConstraint); @@ -596,7 +615,6 @@ export function normalizeServers( }); } -export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions'; export const SECURITY_DEFINITIONS_JSX_NAME = 'SecurityDefinitions'; export const SCHEMA_DEFINITION_JSX_NAME = 'SchemaDefinition';