update redoc

This commit is contained in:
Yurov Dmitry 2019-11-12 12:54:33 +03:00
parent 7a8a289c71
commit 13bd59be41
93 changed files with 8613 additions and 8002 deletions

View File

@ -90,5 +90,5 @@ There are some other scripts available in the `scripts` section of the `package.
- **`src/services/models`**: contains classes for OpenAPI entities (e.g. Response, Operations, etc)
- **`src/types`**: contains extra typescript typings including OpenAPI doc typings
- **`src/utils`**: utility functions
- **`src/styled-components.ts`**: - reexprots styled-components with proper typescript annotations using theme
- **`src/styled-components.ts`**: - reexports styled-components with proper typescript annotations using theme
- **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components

21
.github/workflows/unit-tests.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Unit Tests
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: yarn install, build, and test
run: |
npm install -g yarn
yarn install
yarn bundle
yarn test

View File

@ -1,10 +1,14 @@
language: node_js
node_js:
- '8'
cache: yarn
- '10'
cache:
yarn: true
directories:
# we also need to cache folder with Cypress binary
- ~/.cache
env:
global:
- GH_REF: github.com/Rebilly/ReDoc.git
- GH_REF: github.com/Redocly/redoc.git
- GIT_AUTHOR_EMAIL: redoc-bot@users.noreply.github.com
- GIT_AUTHOR_NAME: RedocBot
- secure: H2GClDJ7TEQaWgnk8d2fIVDpLwG3rTmN8TalUzrCqXGoG6ylCVmlwzKLgfPPWrVgSA7QTdfNV0ab7c2KyPoZBinHmeGMSuKNLVbhOXRc2VFxTBntBTuyJhfCgQEpUKvJesJUuv5RuBn//wC7VZcVVNc06TZDEe8+aDVYQuCRMXZJ4X3e6nSZ64QX2veyVcr+TjnRsZPkeBVcK9hngvuaxLb/hbJ85CvjiseZRt47PGIcrEpMn9n2GMw1m0fNnPoN+MBTSCnIklTmdjPG7t4GUSLmD6H0lNLdXuehYqmQAHgYrLec1aiFlV57QaDoDZrq2hSf4vDmCB/FVydGhD5JunI67pujoV2OnD1V80eUZhYNWOYsJ2Nfp4NxgXsPUcE6zWLYsLfktMPZADhOXInQRACt1cnx8zMYKLnch1RY/ZqjSg0nPtRjLzQ0lNsw5leixvBdBnMjxYHVyAWVwg8WiJMaLO9vog2Qnxg1NTacHO2CsOmm2rw6stpg7ndp/+nOleRlfUKggjt0Tn3FjwCIXeGup2P2EBa+WW2YMAaoMFofYviR5vRlKBgdKo9fsAruaO1r6nm2EdAjOlniyw92bEfU/qOey1nVp/oK2S82uT5In8KB7vl6rF3ak7WAsT9Q5vZUhsrG+eE4PVyIyWNBhs4A7pSwZGHDR/MYtp0E2ug=
@ -14,6 +18,9 @@ env:
- secure: SEqTg6WoGPPpcWzJ03ZfcSBb3nZ2Mdhug0ec2PszuzYO3libCb9usiqi+jils9z6qyXsL6ecz8HYazDGOUepnubhIpI5otLgfn9XiapjMT06Bj//AjbKpH7eu3TJSpJMzoRHZrKIE1y9ZKIBqKwl9Xs7ko+1oa+MLhrLuxXkoi0JqRB5UzkQtJRDoxVNjysnLQn+hsfnm+yuqPHZd2+Loy++q//WHuf9bwJrlkXn2ICYQIX5oQGlxNO6ui+OZklb0YknvyO5GdQeoKaHYru3MMKKCIS6I7AG9wLmPs5Ou3T0Ia0Xx4/7xazs0rH4NCVpIceSYc3v6evR37pp8MsFTC3BzjL1V3slTnmitC1KSNM8ndGRUg1nsCBkJysnR3HpX6SHuCH+UzOuMxEjwiPdSRnzJPEbTHa1HqMfTkTJMbm4zhp7W4/ozX4TtjUB0ql6NoQE2n0Z3aYgR2C78TmzaPQun8EgredWnCID1FedyexaNcw4HyZ2rXlcvG3rBzSwLHH5PePT9skyqy6KtIaL0MlAP556ilgUeyCZfCNdTmzCvPDZuqaeLRezWDdsKnRfTkxIW80QWlmZ6sW0hynJV5JN2Oghk9Tr+QzgV4ZF68FHwoU9YXCTyX4w5iTYq/GjvfTBqB3VSGPOz3PwU7r47tmaYzPj+I44zqktgxyuxDo=
addons:
chrome: stable
apt:
packages:
- libgconf-2-4 # for cypress
before_script: npm run bundle
script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm
run e2e)
@ -27,9 +34,6 @@ deploy:
api_key: "$NPM_TOKEN"
on:
tags: true
- provider: script
skip_cleanup: true
script: cd cli && yarn install && yarn ci-publish || true
- provider: script
skip_cleanup: true
script: yarn deploy:demo

View File

@ -1,3 +1,211 @@
# [2.0.0-rc.18](https://github.com/Redocly/redoc/compare/v2.0.0-rc.17...v2.0.0-rc.18) (2019-10-16)
### Bug Fixes
* add oneOf buttons vertical space when wrapped to new line ([cd9fd61](https://github.com/Redocly/redoc/commit/cd9fd61))
* improve mime-type dropdown font ([ce885f8](https://github.com/Redocly/redoc/commit/ce885f8))
# [2.0.0-rc.17](https://github.com/Redocly/redoc/compare/v2.0.0-rc.16...v2.0.0-rc.17) (2019-10-16)
### Bug Fixes
* active menu item scroll into view ([0a01e9a](https://github.com/Redocly/redoc/commit/0a01e9a))
* changed several components style font-family to monospace ([#1063](https://github.com/Redocly/redoc/issues/1063)) ([0c20e64](https://github.com/Redocly/redoc/commit/0c20e64)), closes [#909](https://github.com/Redocly/redoc/issues/909)
* no quotes for default values in header fields. ([#1059](https://github.com/Redocly/redoc/issues/1059)) ([b5af71d](https://github.com/Redocly/redoc/commit/b5af71d))
* types over-pluralization ([#1057](https://github.com/Redocly/redoc/issues/1057)) ([4494f80](https://github.com/Redocly/redoc/commit/4494f80)), closes [#1053](https://github.com/Redocly/redoc/issues/1053)
### Features
* added support for file paths as --options cli argument ([#1049](https://github.com/Redocly/redoc/issues/1049)) ([4adb927](https://github.com/Redocly/redoc/commit/4adb927))
# [2.0.0-rc.16](https://github.com/Redocly/redoc/compare/v2.0.0-rc.15...v2.0.0-rc.16) (2019-09-30)
### Bug Fixes
* fix scrollYOffset when SSR ([d09c1c1](https://github.com/Redocly/redoc/commit/d09c1c1))
# [2.0.0-rc.15](https://github.com/Redocly/redoc/compare/v2.0.0-rc.14...v2.0.0-rc.15) (2019-09-30)
### Bug Fixes
* auth section appears twice ([5aa7784](https://github.com/Redocly/redoc/commit/5aa7784)), closes [#818](https://github.com/Redocly/redoc/issues/818)
* clicking on group title breaks first tag ([4649683](https://github.com/Redocly/redoc/commit/4649683)), closes [#1034](https://github.com/Redocly/redoc/issues/1034)
* do not crash on empty scopes ([e787d9e](https://github.com/Redocly/redoc/commit/e787d9e)), closes [#1044](https://github.com/Redocly/redoc/issues/1044)
* false-positive recursive detection with allOf at the same level ([faa74d6](https://github.com/Redocly/redoc/commit/faa74d6))
* fix scrollYOffset when SSR ([21258a5](https://github.com/Redocly/redoc/commit/21258a5))
* left menu item before group is not highligted ([67e2a8f](https://github.com/Redocly/redoc/commit/67e2a8f)), closes [#1033](https://github.com/Redocly/redoc/issues/1033)
* remove excessive whitespace between md sections on small screens ([e318fb3](https://github.com/Redocly/redoc/commit/e318fb3)), closes [#874](https://github.com/Redocly/redoc/issues/874)
* use url-template dependency ([#1008](https://github.com/Redocly/redoc/issues/1008)) ([32a464a](https://github.com/Redocly/redoc/commit/32a464a)), closes [#1007](https://github.com/Redocly/redoc/issues/1007)
### Features
* **cli:** added support for JSON string value for --options CLI argument ([#1047](https://github.com/Redocly/redoc/issues/1047)) ([2a28130](https://github.com/Redocly/redoc/commit/2a28130)), closes [#797](https://github.com/Redocly/redoc/issues/797)
* **cli:** add `disableGoogleFont` parameter to cli ([#1045](https://github.com/Redocly/redoc/issues/1045)) ([aceb343](https://github.com/Redocly/redoc/commit/aceb343))
* new option expandDefaultServerVariables ([#1014](https://github.com/Redocly/redoc/issues/1014)) ([0360dce](https://github.com/Redocly/redoc/commit/0360dce))
# [2.0.0-rc.14](https://github.com/Redocly/redoc/compare/v2.0.0-rc.13...v2.0.0-rc.14) (2019-08-07)
### Bug Fixes
* fix escaping JSON string values ([58cb20d](https://github.com/Redocly/redoc/commit/58cb20d)), closes [#999](https://github.com/Redocly/redoc/issues/999)
* revert expanding default server variables ([7849f7f](https://github.com/Redocly/redoc/commit/7849f7f))
# [2.0.0-rc.13](https://github.com/Redocly/redoc/compare/v2.0.0-rc.12...v2.0.0-rc.13) (2019-08-01)
### Bug Fixes
* enum list doesn't wrap ([bfbb0c1](https://github.com/Redocly/redoc/commit/bfbb0c1)), closes [#993](https://github.com/Redocly/redoc/issues/993)
* incorrect serialization of some parameter samples ([aba45db](https://github.com/Redocly/redoc/commit/aba45db)), closes [#992](https://github.com/Redocly/redoc/issues/992)
* support json serialization for parameter examples ([1367380](https://github.com/Redocly/redoc/commit/1367380)), closes [#934](https://github.com/Redocly/redoc/issues/934)
* unify accordion icons for responses section ([2afc2e4](https://github.com/Redocly/redoc/commit/2afc2e4)), closes [#975](https://github.com/Redocly/redoc/issues/975)
* update to core.js 3 ([9e3375d](https://github.com/Redocly/redoc/commit/9e3375d)), closes [#997](https://github.com/Redocly/redoc/issues/997)
# [2.0.0-rc.12](https://github.com/Redocly/redoc/compare/v2.0.0-rc.11...v2.0.0-rc.12) (2019-07-30)
### Bug Fixes
* rename ObjectDescription to SchemaDefinition as discussed ([4496622](https://github.com/Redocly/redoc/commit/4496622))
# [2.0.0-rc.11](https://github.com/Redocly/redoc/compare/v2.0.0-rc.10...v2.0.0-rc.11) (2019-07-30)
### Bug Fixes
* do not add extra slashes to pattern ([70d1ee9](https://github.com/Redocly/redoc/commit/70d1ee9)), closes [#983](https://github.com/Redocly/redoc/issues/983)
* dropdown fixes related to object description ([0504ad4](https://github.com/Redocly/redoc/commit/0504ad4))
* incorrect serialization of parameter sample with hypen ([f7dd658](https://github.com/Redocly/redoc/commit/f7dd658))
* redoc-cli: Add missing content type header on compressed responses of `/` path
### Features
* menu items from tags + md extension for Schema Definition ([#681](https://github.com/Redocly/redoc/pull/681))
* new option `menuToggle` - fold active MenuItem if clicked ([#963](https://github.com/Redocly/redoc/issues/963))
* Add option for skipping quotes in enums `enumSkipQuotes` ([#968](https://github.com/Redocly/redoc/issues/968)) ([afc7e36](https://github.com/Redocly/redoc/commit/afc7e36))
* add `sampleCollapseLevel` option ([#937](https://github.com/Redocly/redoc/issues/937)) ([d3f1c16](https://github.com/Redocly/redoc/commit/d3f1c16))
# [2.0.0-rc.10](https://github.com/Redocly/redoc/compare/v2.0.0-rc.9...v2.0.0-rc.10) (2019-07-08)
### Bug Fixes
* broken headings with single quote ([51d3b9b](https://github.com/Redocly/redoc/commit/51d3b9b)), closes [#955](https://github.com/Redocly/redoc/issues/955)
* fix fields table overflow if deeply nested with long title ([12b7057](https://github.com/Redocly/redoc/commit/12b7057))
* hide empty example when it is not defined ([4bd499f](https://github.com/Redocly/redoc/commit/4bd499f))
* markdown in examples descriptions + minor ui tweaks ([f52d9e8](https://github.com/Redocly/redoc/commit/f52d9e8))
* organize response examples in dropdown and display description ([995e557](https://github.com/Redocly/redoc/commit/995e557))
# [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27)
### Bug Fixes
* fix regression double slashes added to full URL display ([f29a4fe](https://github.com/Redocly/redoc/commit/f29a4fe))
* IE11, add missing Object.assign polyfill ([888f04e](https://github.com/Redocly/redoc/commit/888f04e))
* serialize parameter example values according to the spec ([#917](https://github.com/Redocly/redoc/issues/917)) ([3939286](https://github.com/Redocly/redoc/commit/3939286))
* styled-component style error in tabs ([#946](https://github.com/Redocly/redoc/issues/946)) ([c488bbf](https://github.com/Redocly/redoc/commit/c488bbf))
### Features
* add x-additionalPropertiesName ([#622](https://github.com/Redocly/redoc/issues/622)) ([#944](https://github.com/Redocly/redoc/issues/944)) ([0eb1e66](https://github.com/Redocly/redoc/commit/0eb1e66))
# [2.0.0-rc.8-1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.8...v2.0.0-rc.8-1) (2019-05-13)
### Bug Fixes
* crash with empty servers with redoc-cli ([3d52b39](https://github.com/Rebilly/ReDoc/commit/3d52b39))
# [2.0.0-rc.8](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.7...v2.0.0-rc.8) (2019-05-13)
### Bug Fixes
* fix broken CLI again ([4e12b5d](https://github.com/Rebilly/ReDoc/commit/4e12b5d))
* fix logo gutter bg ([81896d3](https://github.com/Rebilly/ReDoc/commit/81896d3))
# [2.0.0-rc.7](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.6...v2.0.0-rc.7) (2019-05-13)
### Bug Fixes
* crash in node due to broken URL parsing ([8df2b97](https://github.com/Rebilly/ReDoc/commit/8df2b97))
# [2.0.0-rc.6](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.5...v2.0.0-rc.6) (2019-05-13)
### Bug Fixes
* broken schema talbes with long enums ([3a74b74](https://github.com/Rebilly/ReDoc/commit/3a74b74))
* deep linking sometimes not working when sent over messengers ([2491d97](https://github.com/Rebilly/ReDoc/commit/2491d97))
# [2.0.0-rc.5](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.4...v2.0.0-rc.5) (2019-05-13)
### Bug Fixes
* change fontFamily for EndpointInfo ([#866](https://github.com/Rebilly/ReDoc/issues/866)) ([851b133](https://github.com/Rebilly/ReDoc/commit/851b133))
* clean up field values display ([#855](https://github.com/Rebilly/ReDoc/issues/855)) ([5c91590](https://github.com/Rebilly/ReDoc/commit/5c91590))
* discriminator and oneOf title fix ([a3d7d7a](https://github.com/Rebilly/ReDoc/commit/a3d7d7a))
* encode x-www-form-urlencoded examples correctly ([65930ad](https://github.com/Rebilly/ReDoc/commit/65930ad)), closes [#870](https://github.com/Rebilly/ReDoc/issues/870)
* fix redoc-cli broken dependencies ([81a7568](https://github.com/Rebilly/ReDoc/commit/81a7568))
* IE11 add missing fetch and URL polyfills ([d2ce1bd](https://github.com/Rebilly/ReDoc/commit/d2ce1bd)), closes [#875](https://github.com/Rebilly/ReDoc/issues/875)
* ignore empty x-tagGroups array ([#869](https://github.com/Rebilly/ReDoc/issues/869)) ([4366a0d](https://github.com/Rebilly/ReDoc/commit/4366a0d))
* incorrect detected schema title for deeply inherited schemas ([7d7b4e3](https://github.com/Rebilly/ReDoc/commit/7d7b4e3))
* pluralize arrray of types ([fdcac30](https://github.com/Rebilly/ReDoc/commit/fdcac30))
* remove huge space after Authentication section ([548fae3](https://github.com/Rebilly/ReDoc/commit/548fae3)), closes [#872](https://github.com/Rebilly/ReDoc/issues/872)
* remove query string from server URL ([#895](https://github.com/Rebilly/ReDoc/issues/895)) ([64453ff](https://github.com/Rebilly/ReDoc/commit/64453ff))
* remove tabs top margin ([5c187f3](https://github.com/Rebilly/ReDoc/commit/5c187f3))
* right panel code samples bg color ([de2aed2](https://github.com/Rebilly/ReDoc/commit/de2aed2))
* tidy up non-redoc vendor extension presentation ([#847](https://github.com/Rebilly/ReDoc/issues/847)) ([b21cd3d](https://github.com/Rebilly/ReDoc/commit/b21cd3d))
* update apiKey in to be titleize ([#902](https://github.com/Rebilly/ReDoc/issues/902)) ([35df477](https://github.com/Rebilly/ReDoc/commit/35df477))
* **cli:** add node-libs-browser to the deps ([6c79901](https://github.com/Rebilly/ReDoc/commit/6c79901)), closes [#850](https://github.com/Rebilly/ReDoc/issues/850)
### Features
* add hideSingleRequestSampleTab option ([4550e4d](https://github.com/Rebilly/ReDoc/commit/4550e4d))
* add lineHeight config for headings ([#894](https://github.com/Rebilly/ReDoc/issues/894)) ([5dd5d6d](https://github.com/Rebilly/ReDoc/commit/5dd5d6d))
* basic UI labels configuration ([b0e660e](https://github.com/Rebilly/ReDoc/commit/b0e660e)). Can be used for translations later.
* add logo gutter to the theme ([82c0cb1a](https://github.com/Rebilly/ReDoc/commit/82c0cb1a)).
# [2.0.0-rc.4](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.3...v2.0.0-rc.4) (2019-03-15)

View File

@ -1,27 +1,27 @@
<div align="center">
<img alt="ReDoc logo" src="https://raw.githubusercontent.com/Rebilly/ReDoc/master/docs/images/redoc-logo.png" width="400px" />
<img alt="ReDoc logo" src="https://raw.githubusercontent.com/Redocly/redoc/master/docs/images/redoc-logo.png" width="400px" />
**OpenAPI/Swagger-generated API Reference Documentation**
[![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![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/Rebilly/ReDoc/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/Redocly/redoc.svg?branch=master)](https://travis-ci.org/Redocly/redoc) [![Coverage Status](https://coveralls.io/repos/Redocly/redoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Redocly/redoc?branch=master) [![dependencies Status](https://david-dm.org/Redocly/redoc/status.svg)](https://david-dm.org/Redocly/redoc) [![devDependencies Status](https://david-dm.org/Redocly/redoc/dev-status.svg)](https://david-dm.org/Redocly/redoc#info=devDependencies) [![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/)
</div>
**This is README for `2.0` version of ReDoc (React based). README for `1.x` version is on the branch [v1.x](https://github.com/Rebilly/ReDoc/tree/v1.x)**
**This is README for `2.0` version of ReDoc (React based). README for `1.x` version is on the branch [v1.x](https://github.com/Redocly/redoc/tree/v1.x)**
![ReDoc demo](https://raw.githubusercontent.com/Rebilly/ReDoc/master/demo/redoc-demo.png)
![ReDoc demo](https://raw.githubusercontent.com/Redocly/redoc/master/demo/redoc-demo.png)
## [Live demo](http://rebilly.github.io/ReDoc/)
## [Live demo](http://redocly.github.io/redoc/)
[<img alt="Deploy to Github" src="http://i.imgur.com/YZmaqk3.png" height="60px">](https://github.com/Rebilly/generator-openapi-repo#generator-openapi-repo--) [<img alt="ReDoc as a service" src="http://i.imgur.com/edqdCv6.png" height="60px">](https://redoc.ly) [<img alt="Customization services" src="http://i.imgur.com/c4sUF7M.png" height="60px">](https://redoc.ly/#services)
## Features
- Extremely easy deployment
- [redoc-cli](https://github.com/Rebilly/ReDoc/blob/master/cli/README.md) with ability to bundle your docs into **zero-dependency** HTML file
- [redoc-cli](https://github.com/Redocly/redoc/blob/master/cli/README.md) with ability to bundle your docs into **zero-dependency** HTML file
- Server Side Rendering ready
- The widest OpenAPI v2.0 features support (yes, it supports even `discriminator`) <br>
![](docs/images/discriminator-demo.gif)
@ -37,7 +37,7 @@
- Branding/customizations via [`theme` option](#redoc-options-object)
## Roadmap
- [x] ~~[OpenAPI v3.0 support](https://github.com/Rebilly/ReDoc/issues/312)~~
- [x] ~~[OpenAPI v3.0 support](https://github.com/Redocly/redoc/issues/312)~~
- [x] ~~performance optimizations~~
- [x] ~~better navigation (menu improvements + search)~~
- [x] ~~React rewrite~~
@ -50,7 +50,7 @@
- particular release, e.g. `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
Additionally, all the 1.x releases are hosted on our GitHub Pages-based **CDN**:
Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(deprecated)**:
- particular release, e.g. `v1.2.0`: https://rebilly.github.io/ReDoc/releases/v1.2.0/redoc.min.js
- `v1.x.x` release: https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js
- `latest` release: https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js - it will point to latest 1.x.x release since 2.x releases are not hosted on this CDN but on unpkg.
@ -138,7 +138,7 @@ For npm:
Install peer dependencies required by ReDoc if you don't have them installed already:
npm i react react-dom mobx@^4.2.0 styled-components
npm i react react-dom mobx@^4.2.0 styled-components core-js
Import `RedocStandalone` component from 'redoc' module:
@ -165,7 +165,7 @@ Also you can pass options:
specUrl="http://rebilly.github.io/RebillyAPI/openapi.json"
options={{
nativeScrollbars: true,
theme: { colors: { primary { main: '#dd5522' } } },
theme: { colors: { primary: { main: '#dd5522' } } },
}}
/>
```
@ -185,6 +185,8 @@ You can also specify `onLoaded` callback which will be called each time Redoc ha
/>
```
[**IE11 Support Notes**](docs/usage-with-ie11.md)
## The Docker way
ReDoc is available as pre-built Docker image in official [Docker Hub repository](https://hub.docker.com/r/redocly/redoc/). You may simply pull & run it:
@ -198,7 +200,7 @@ Also you may rewrite some predefined environment variables defined in [Dockerfil
## ReDoc CLI
[See here](https://github.com/Rebilly/ReDoc/blob/master/cli/README.md)
[See here](https://github.com/Redocly/redoc/blob/master/cli/README.md)
## Configuration
@ -216,31 +218,36 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
* [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu
* [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0)
* [`x-ignoredHeaderParameters`](docs/redoc-vendor-extensions.md#x-ignoredHeaderParameters) - ability to specify header parameter names to ignore
* [`x-additionalPropertiesName`](docs/redoc-vendor-extensions.md#x-additionalPropertiesName) - ability to supply a descriptive name for the additional property keys
### `<redoc>` options object
You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`.
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
* `disableSearch` - disable search indexing and search box.
* `expandDefaultServerVariables` - enable expanding default server variables, default `false`.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `hideLoading` - do not show loading animation. Useful for small docs.
* `hideSingleRequestSampleTab` - do not show the request sample tab for requests with only one sample.
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 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 `false`.
* `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.
* `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc;
`scrollYOffset` can be specified in various ways:
* **number**: A fixed number of pixels to be used as offset;
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset;
* **function**: A getter function. Must return a number representing the offset (in pixels);
* **number**: A fixed number of pixels to be used as offset.
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset.
* **function**: A getter function. Must return a number representing the offset (in pixels).
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display.
* `sortPropsAlphabetically` - sort properties alphabetically.
* `suppressWarnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 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](\\rebilly.github.io/ReDoc) for the example.~~
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
* `sortPropsAlphabetically` - sort properties alphabetically
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display
* `noAutoAuth` - do not inject Authentication section automatically
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one
* `hideLoading` - do not show loading animation. Useful for small docs
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs)
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `disableSearch` - disable search indexing and search box
* `onlyRequiredInSamples` - shows only required fields in request samples.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Rebilly/ReDoc/blob/master/src/theme.ts)
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts).
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
## Advanced usage of standalone version
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
# redoc-cli
**[ReDoc](https://github.com/Rebilly/ReDoc)'s Command Line Interface**
**[ReDoc](https://github.com/Redocly/redoc)'s Command Line Interface**
## Installation
You can use redoc cli by installing `redoc-cli` globally or using [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b).
@ -16,6 +16,7 @@ Some examples:
- Bundle with main color changed to `orange`: <br> `$ redoc-cli bundle [spec] --options.theme.colors.primary.main=orange`
- Serve with `nativeScrollbars` option set to true: <br> `$ redoc-cli serve [spec] --options.nativeScrollbars`
- Bundle using custom template (check [default template](https://github.com/Rebilly/ReDoc/blob/master/cli/template.hbs) for reference): <br> `$ redoc-cli bundle [spec] -t custom.hbs`
- Bundle using custom template (check [default template](https://github.com/Redocly/redoc/blob/master/cli/template.hbs) for reference): <br> `$ redoc-cli bundle [spec] -t custom.hbs`
- Bundle using custom template and add custom `templateOptions`: <br> `$ redoc-cli bundle [spec] -t custom.hbs --templateOptions.metaDescription "Page meta description"`
For more details run `redoc-cli --help`.

View File

@ -14,7 +14,7 @@ import * as zlib from 'zlib';
import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
import { watch } from 'chokidar';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync, lstatSync } from 'fs';
import * as mkdirp from 'mkdirp';
import * as YargsParser from 'yargs';
@ -25,7 +25,10 @@ interface Options {
cdn?: boolean;
output?: string;
title?: string;
disableGoogleFont?: boolean;
port?: number;
templateFileName?: string;
templateOptions?: any;
redocOptions?: any;
}
@ -61,13 +64,16 @@ YargsParser.command(
return yargs;
},
async argv => {
const config = {
ssr: argv.ssr,
watch: argv.watch,
templateFileName: argv.template,
redocOptions: argv.options || {},
const config: Options = {
ssr: argv.ssr as boolean,
watch: argv.watch as boolean,
templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {},
redocOptions: getObjectOrJSON(argv.options),
};
console.log(config);
try {
await serve(argv.port, argv.spec, config);
} catch (e) {
@ -96,6 +102,12 @@ YargsParser.command(
default: 'ReDoc documentation',
});
yargs.options('disableGoogleFont', {
describe: 'Disable Google Font',
type: 'boolean',
default: false,
});
yargs.option('cdn', {
describe: 'Do not include ReDoc source code into html page, use link to CDN instead',
type: 'boolean',
@ -105,14 +117,16 @@ YargsParser.command(
yargs.demandOption('spec');
return yargs;
},
async argv => {
async (argv: any) => {
const config = {
ssr: true,
output: argv.o,
cdn: argv.cdn,
title: argv.title,
templateFileName: argv.template,
redocOptions: argv.options || {},
output: argv.o as string,
cdn: argv.cdn as boolean,
title: argv.title as string,
disableGoogleFont: argv.disableGoogleFont as boolean,
templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {},
redocOptions: getObjectOrJSON(argv.options),
};
try {
@ -148,7 +162,9 @@ async function serve(port: any, pathToSpec: any, options: any = {}) {
},
);
} else if (request.url === '/') {
respondWithGzip(pageHTML, request, response);
respondWithGzip(pageHTML, request, response, {
'Content-Type': 'text/html',
});
} else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, {
@ -170,14 +186,14 @@ async function serve(port: any, pathToSpec: any, options: any = {}) {
if (options.watch && existsSync(pathToSpec)) {
const pathToSpecDirectory = resolve(dirname(pathToSpec));
const watchOptions = {
ignored: /(^|[\/\\])\../,
ignored: [/(^|[\/\\])\../, /___jb_[a-z]+___$/],
ignoreInitial: true,
};
const watcher = watch(pathToSpecDirectory, watchOptions);
const log = console.log.bind(console);
watcher
.on('change', async path => {
log(`${path} changed, updating docs`);
const handlePath = async path => {
try {
spec = await loadAndBundleSpec(pathToSpec);
pageHTML = await getPageHTML(spec, pathToSpec, options);
@ -185,6 +201,19 @@ async function serve(port: any, pathToSpec: any, options: any = {}) {
} catch (e) {
console.error('Error while updating: ', e.message);
}
};
watcher
.on('change', async path => {
log(`${path} changed, updating docs`);
handlePath(path);
})
.on('add', async path => {
log(`File ${path} added, updating docs`);
handlePath(path);
})
.on('addDir', path => {
log(`↗ Directory ${path} added. Files in here will trigger reload.`);
})
.on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
@ -208,7 +237,15 @@ async function bundle(pathToSpec, options: any = {}) {
async function getPageHTML(
spec: any,
pathToSpec: string,
{ ssr, cdn, title, templateFileName, redocOptions = {} }: Options,
{
ssr,
cdn,
title,
disableGoogleFont,
templateFileName,
templateOptions,
redocOptions = {},
}: Options,
) {
let html;
let css;
@ -251,6 +288,8 @@ async function getPageHTML(
: `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>',
title,
disableGoogleFont,
templateOptions,
});
}
@ -312,3 +351,25 @@ function handleError(error: Error) {
console.error(error.stack);
process.exit(1);
}
function getObjectOrJSON(options) {
switch (typeof options) {
case 'object':
return options;
case 'string':
try {
if (existsSync(options) && lstatSync(options).isFile()) {
return JSON.parse(readFileSync(options, 'utf-8'));
} else {
return JSON.parse(options);
}
} catch (e) {
console.log(
`Encountered error:\n\n${options}\n\nis neither a file with a valid JSON object neither a stringified JSON object.`
);
handleError(e);
}
default:
return {};
}
}

View File

@ -1,38 +1,35 @@
{
"name": "redoc-cli",
"version": "0.7.0",
"version": "0.9.2",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
"repository": "https://github.com/Rebilly/ReDoc",
"repository": "https://github.com/Redocly/redoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT",
"engines": {
"node": ">= 8"
},
"dependencies": {
"chokidar": "^2.0.4",
"handlebars": "^4.0.11",
"isarray": "^2.0.4",
"chokidar": "^3.0.2",
"handlebars": "^4.1.2",
"isarray": "^2.0.5",
"mkdirp": "^0.5.1",
"mobx": "^4.2.0",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"node-libs-browser": "^2.2.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"redoc": "github:BusinessDuck/ReDoc#master",
"styled-components": "^4.1.1",
"tslib": "^1.9.3",
"yargs": "^12.0.5",
"node-fetch-h2": "^2.3.1-0",
"builtin-status-codes": "^3.0.0",
"to-arraybuffer": "^1.0.1"
},
"scripts": {
"ci-publish": "ci-publish"
"styled-components": "^4.3.2",
"tslib": "^1.10.0",
"yargs": "^13.3.0"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/chokidar": "^1.7.5",
"@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1"
"@types/mkdirp": "^0.5.2"
}
}

View File

@ -13,7 +13,7 @@
}
</style>
{{{redocHead}}}
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
{{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
</head>
<body>

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ Serve local file:
- `PAGE_FAVICON` (default `"favicon.png"`) - URL to page favicon
- `SPEC_URL` (default `"http://petstore.swagger.io/v2/swagger.json"`) - URL to spec
- `PORT` (default `80`) - nginx port
- `REDOC_OPTIONS` - [`<redoc>` tag attributes](https://github.com/Rebilly/ReDoc#redoc-tag-attributes)
- `REDOC_OPTIONS` - [`<redoc>` tag attributes](https://github.com/Redocly/redoc#redoc-tag-attributes)
## Build

View File

@ -82,7 +82,7 @@ class DemoApp extends React.Component<
<>
<Heading>
<a href=".">
<Logo src="https://github.com/Rebilly/ReDoc/raw/master/docs/images/redoc-logo.png" />
<Logo src="https://github.com/Redocly/redoc/raw/master/docs/images/redoc-logo.png" />
</a>
<ControlsContainer>
<ComboBox
@ -97,7 +97,7 @@ class DemoApp extends React.Component<
</CorsCheckbox>
</ControlsContainer>
<iframe
src="https://ghbtns.com/github-btn.html?user=Rebilly&amp;repo=ReDoc&amp;type=star&amp;count=true&amp;size=large"
src="https://ghbtns.com/github-btn.html?user=Redocly&amp;repo=redoc&amp;type=star&amp;count=true&amp;size=large"
frameBorder="0"
scrolling="0"
width="150px"

View File

@ -15,15 +15,15 @@ info:
This API is documented in **OpenAPI format** and is based on
[Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team.
It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md).
# OpenAPI Specification
This API is documented in **OpenAPI format** and is based on
[Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team.
It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md).
# Cross-Origin Resource Sharing
This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).
@ -38,7 +38,7 @@ info:
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<security-definitions />
<SecurityDefinitions />
version: 1.0.0
title: Swagger Petstore
@ -46,9 +46,9 @@ info:
contact:
name: API Support
email: apiteam@swagger.io
url: https://github.com/Rebilly/ReDoc
url: https://github.com/Redocly/redoc
x-logo:
url: 'https://rebilly.github.io/ReDoc/petstore-logo.png'
url: 'https://redocly.github.io/redoc/petstore-logo.png'
altText: Petstore logo
license:
name: Apache 2.0
@ -63,6 +63,14 @@ tags:
description: Access to Petstore orders
- name: user
description: Operations about user
- name: pet_model
x-displayName: The Pet Model
description: |
<SchemaDefinition schemaRef="#/components/schemas/Pet" />
- name: store_model
x-displayName: The Order Model
description: |
<SchemaDefinition schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} />
x-tagGroups:
- name: General
tags:
@ -71,9 +79,21 @@ x-tagGroups:
- name: User Management
tags:
- user
- name: Models
tags:
- pet_model
- store_model
paths:
/pet:
parameters:
- name: Accept-Language
in: header
description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US"
example: en-US
required: false
schema:
type: string
default: en-AU
- name: cookieParam
in: cookie
description: Some cookie
@ -666,6 +686,7 @@ components:
type: string
description: The measured skill for hunting
default: lazy
example: adventurous
enum:
- clueless
- lazy
@ -754,6 +775,11 @@ components:
description: Indicates whenever order was completed or not
type: boolean
default: false
readOnly: true
rqeuestId:
description: Unique Request Id
type: string
writeOnly: true
xml:
name: Order
Pet:
@ -866,14 +892,13 @@ components:
as well as digits
format: password
minLength: 8
pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])'
pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/'
example: drowssaP123
phone:
description: User phone number in international format
type: string
pattern: '^\+(?:[0-9]-?){6,14}[0-9]$'
pattern: '/^\+(?:[0-9]-?){6,14}[0-9]$/'
example: +1-202-555-0192
nullable: true
userStatus:
description: User status
type: integer
@ -926,3 +951,10 @@ components:
type: apiKey
name: api_key
in: header
examples:
Order:
value:
quantity: 1,
shipDate: 2018-10-19T16:46:45Z,
status: placed,
complete: false

View File

@ -14,14 +14,14 @@ info:
This API is documented in **OpenAPI format** and is based on
[Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team.
It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md).
# OpenAPI Specification
This API is documented in **OpenAPI format** and is based on
[Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team.
It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md).
# Cross-Origin Resource Sharing
This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).
And that allows cross-domain communication from the browser.
@ -39,9 +39,9 @@ info:
termsOfService: 'http://swagger.io/terms/'
contact:
email: apiteam@swagger.io
url: https://github.com/Rebilly/ReDoc
url: https://github.com/Redocly/redoc
x-logo:
url: 'https://rebilly.github.io/ReDoc/petstore-logo.png'
url: 'https://redocly.github.io/redoc/petstore-logo.png'
altText: Petstore logo
license:
name: Apache 2.0

View File

@ -38,7 +38,6 @@ const babelLoader = mode => ({
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-jsx',
mode !== 'production' ? 'react-hot-loader/babel' : undefined,
[
'babel-plugin-styled-components',
{
@ -50,6 +49,13 @@ const babelLoader = mode => ({
},
});
const babelHotLoader = {
loader: 'babel-loader',
options: {
plugins: ['react-hot-loader/babel'],
},
};
export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({
entry: [
root('../src/polyfills.ts'),
@ -94,6 +100,9 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
externals: {
esprima: 'esprima',
'node-fetch': 'null',
'node-fetch-h2': 'null',
yaml: 'null',
'safe-json-stringify': 'null',
},
module: {
@ -102,7 +111,11 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{
test: /\.tsx?$/,
use: [tsLoader(env), babelLoader(mode)],
use: compact([
mode !== 'production' ? babelHotLoader : undefined,
tsLoader(env),
babelLoader(mode),
]),
exclude: [/node_modules/],
},
{

View File

@ -107,7 +107,7 @@ json
"version": "1.0.0",
"title": "Swagger Petstore",
"x-logo": {
"url": "https://rebilly.github.io/ReDoc/petstore-logo.png",
"url": "https://redocly.github.io/redoc/petstore-logo.png",
"backgroundColor": "#FFFFFF",
"altText": "Petstore logo"
}
@ -120,7 +120,7 @@ info:
version: "1.0.0"
title: "Swagger Petstore"
x-logo:
url: "https://rebilly.github.io/ReDoc/petstore-logo.png"
url: "https://redocly.github.io/redoc/petstore-logo.png"
backgroundColor: "#FFFFFF"
altText: "Petstore logo"
```
@ -278,3 +278,31 @@ PayPalPayment:
In the example above the names of definitions (`PayPalPayment`) are named differently than
names in the payload (`paypal`) which is not supported by default `discriminator`.
#### x-additionalPropertiesName
**ATTENTION**: This is ReDoc-specific vendor extension. It won't be supported by other tools.
Extends the `additionalProperties` property of the schema object.
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-additionalPropertiesName | string | descriptive name of additional properties keys |
###### Usage in ReDoc
ReDoc uses this extension to display a more descriptive property name in objects with `additionalProperties` when viewing the property list with an `object`.
###### x-additionalPropertiesName example
```yaml
Player:
required:
- name
properties:
name:
type: string
additionalProperties:
x-additionalPropertiesName: attribute-name
type: string
```

24
docs/usage-with-ie11.md Normal file
View File

@ -0,0 +1,24 @@
# Usage With IE11
## Standalone package
IE11 is supported by default if you use ReDoc as a standalone package.
## Usage as a React component
If you use ReDoc as a React component you should include the following polyfills in your project:
```js
import 'core-js/es6/promise';
import 'core-js/fn/array/find';
import 'core-js/fn/object/assign';
import 'core-js/fn/string/ends-with';
import 'core-js/fn/string/starts-with';
import 'core-js/es6/map';
import 'core-js/es6/symbol';
import 'unfetch/polyfill/index'; // or any other fetch polyfill
import 'url-polyfill';
```

6
e2e/e2e.html Normal file
View File

@ -0,0 +1,6 @@
<html>
<body>
<script src="../bundles/redoc.standalone.js">{}</script>
<div id="redoc" />
</body>
</html>;

6
e2e/index.html Normal file
View File

@ -0,0 +1,6 @@
<html>
<body>
<script src="../bundles/redoc.standalone.js">{}</script>
<div id="redoc" />
</body>
</html>;

View File

@ -6,7 +6,7 @@ describe('Menu', () => {
it('should have valid items count', () => {
cy.get('.menu-content')
.find('li')
.should('have.length', 6 + (2 + 8 + 4) + (1 + 8));
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8));
});
it('should sync active menu items while scroll', () => {

View File

@ -0,0 +1,64 @@
// tslint:disable:no-implicit-dependencies
import * as yaml from 'yaml-js';
async function loadSpec(url: string): Promise<any> {
const spec = await (await fetch(url)).text();
return yaml.load(spec);
}
function initReDoc(win, spec, options = {}) {
(win as any).Redoc.init(spec, options, win.document.getElementById('redoc'));
}
describe('Servers', () => {
beforeEach(() => {
cy.visit('e2e/');
});
it('should have valid server', () => {
cy.window().then(async win => {
const spec = await loadSpec('/demo/openapi.yaml');
initReDoc(win, spec, {});
// TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should(
'contain',
'http://petstore.swagger.io/v2/pet',
);
cy.get('[data-section-id="operation/addPet"]').should(
'contain',
'http://petstore.swagger.io/sandbox/pet',
);
});
});
it('should have valid server for when servers not provided', () => {
cy.window().then(async win => {
const spec = await loadSpec('/demo/openapi.yaml');
delete spec.servers;
initReDoc(win, spec, {});
// TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should(
'contain',
'http://localhost:' + win.location.port + '/e2e/pet',
);
});
});
it('should have valid server for when servers not provided at .html pages', () => {
cy.visit('e2e/e2e.html');
cy.window().then(async win => {
const spec = await loadSpec('/demo/openapi.yaml');
delete spec.servers;
initReDoc(win, spec, {});
// TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should(
'contain',
'http://localhost:' + win.location.port + '/e2e/pet',
);
});
});
});

View File

@ -1,10 +1,10 @@
{
"name": "redoc",
"version": "2.0.0-rc.4",
"version": "2.0.0-rc.18",
"description": "ReDoc",
"repository": {
"type": "git",
"url": "git://github.com/Rebilly/ReDoc"
"url": "git://github.com/Redocly/redoc"
},
"engines": {
"node": ">=6.9",
@ -28,7 +28,7 @@
"start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts",
"start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts",
"start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts",
"test": "npm run lint && npm run unit && npm run bundlesize && npm run license-check",
"test": "npm run lint && npm run unit && npm run license-check",
"unit": "jest --coverage",
"e2e": "cypress run",
"e2e-ci": "cypress run --record",
@ -36,7 +36,7 @@
"cy:open": "cypress open",
"bundle:clean": "rimraf bundles",
"bundle:standalone": "webpack --env.standalone --mode=production",
"bundle:lib": "webpack --mode=production",
"bundle:lib": "webpack --mode=production && npm run declarations",
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone",
"declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
"stats": "webpack --env.standalone --json --profile --mode=production > stats.json",
@ -52,79 +52,81 @@
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
},
"devDependencies": {
"@babel/core": "7.3.4",
"@babel/core": "7.6.2",
"@babel/plugin-syntax-decorators": "7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-jsx": "7.2.0",
"@babel/plugin-syntax-typescript": "7.3.3",
"@cypress/webpack-preprocessor": "4.0.3",
"@hot-loader/react-dom": "^16.8.4",
"@types/chai": "4.1.7",
"@types/dompurify": "^0.0.32",
"@types/enzyme": "^3.9.0",
"@cypress/webpack-preprocessor": "4.1.0",
"@hot-loader/react-dom": "^16.9.0",
"@types/chai": "4.2.3",
"@types/dompurify": "^0.0.33",
"@types/enzyme": "^3.10.3",
"@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^24.0.11",
"@types/jest": "^24.0.18",
"@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.122",
"@types/lodash": "^4.14.141",
"@types/lunr": "^2.3.2",
"@types/mark.js": "^8.11.3",
"@types/marked": "^0.6.3",
"@types/prismjs": "^1.9.1",
"@types/prop-types": "^15.7.0",
"@types/react": "^16.8.7",
"@types/react-dom": "^16.8.2",
"@types/mark.js": "^8.11.4",
"@types/marked": "^0.6.5",
"@types/prismjs": "^1.16.0",
"@types/prop-types": "^15.7.3",
"@types/react": "^16.9.3",
"@types/react-dom": "^16.9.1",
"@types/react-hot-loader": "^4.1.0",
"@types/react-tabs": "^2.3.1",
"@types/styled-components": "^4.1.12",
"@types/styled-components": "^4.1.19",
"@types/tapable": "1.0.4",
"@types/webpack": "^4.4.25",
"@types/webpack-env": "^1.13.9",
"@types/yargs": "^12.0.9",
"babel-loader": "8.0.5",
"babel-plugin-styled-components": "^1.10.0",
"@types/webpack": "^4.39.2",
"@types/webpack-env": "^1.14.0",
"@types/yargs": "^13.0.3",
"babel-loader": "8.0.6",
"babel-plugin-styled-components": "^1.10.6",
"beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.1",
"conventional-changelog-cli": "^2.0.12",
"copy-webpack-plugin": "^5.0.0",
"core-js": "^2.6.5",
"coveralls": "^3.0.3",
"css-loader": "^2.1.1",
"cypress": "~3.1.5",
"deploy-to-gh-pages": "^1.3.6",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.10.0",
"enzyme-to-json": "^3.3.5",
"fork-ts-checker-webpack-plugin": "1.0.0",
"bundlesize": "^0.18.0",
"conventional-changelog-cli": "^2.0.23",
"copy-webpack-plugin": "^5.0.4",
"core-js": "^3.2.1",
"coveralls": "^3.0.6",
"css-loader": "^3.2.0",
"cypress": "~3.4.1",
"deploy-to-gh-pages": "^1.3.7",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.4.0",
"fork-ts-checker-webpack-plugin": "1.5.0",
"html-webpack-plugin": "^3.1.0",
"jest": "^24.3.1",
"jest": "^24.9.0",
"license-checker": "^25.0.1",
"lodash": "^4.17.11",
"lodash": "^4.17.15",
"mobx": "^4.3.1",
"prettier": "^1.16.4",
"prettier-eslint": "^8.8.2",
"puppeteer": "^1.13.0",
"prettier": "^1.18.2",
"prettier-eslint": "^9.0.0",
"raf": "^3.4.1",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"rimraf": "^2.6.3",
"react": "^16.10.1",
"react-dom": "^16.10.1",
"rimraf": "^3.0.0",
"shelljs": "^0.8.3",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"styled-components": "^4.1.3",
"ts-jest": "24.0.0",
"ts-loader": "5.3.3",
"ts-node": "^8.0.3",
"tslint": "^5.13.1",
"tslint-react": "^3.4.0",
"typescript": "^3.3.3333",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"style-loader": "^1.0.0",
"styled-components": "^4.4.0",
"ts-jest": "24.1.0",
"ts-loader": "6.2.0",
"ts-node": "^8.4.1",
"tslint": "^5.20.0",
"tslint-react": "^4.1.0",
"typescript": "^3.6.3",
"unfetch": "^4.1.0",
"url-polyfill": "^1.1.7",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1",
"webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.4",
"workerize-loader": "^1.1.0",
"yaml-js": "^0.2.3"
},
"peerDependencies": {
"core-js": "^3.1.4",
"mobx": "^4.2.0 || ^5.0.0",
"react": "^16.8.4",
"react-dom": "^16.8.4",
@ -133,27 +135,28 @@
"dependencies": {
"classnames": "^2.2.6",
"decko": "^1.2.0",
"dompurify": "^1.0.10",
"eventemitter3": "^3.0.0",
"dompurify": "^2.0.3",
"eventemitter3": "^4.0.0",
"json-pointer": "^0.6.0",
"json-schema-ref-parser": "^6.1.0",
"lunr": "2.3.6",
"mark.js": "^8.11.1",
"marked": "^0.6.1",
"memoize-one": "^5.0.0",
"mobx-react": "^5.4.3",
"openapi-sampler": "1.0.0-beta.14",
"marked": "^0.7.0",
"memoize-one": "~5.0.5",
"mobx-react": "^6.1.3",
"openapi-sampler": "1.0.0-beta.15",
"perfect-scrollbar": "^1.4.0",
"polished": "^3.0.3",
"prismjs": "^1.15.0",
"polished": "^3.4.1",
"prismjs": "^1.17.1",
"prop-types": "^15.7.2",
"react-dropdown": "^1.6.4",
"react-hot-loader": "^4.8.0",
"react-hot-loader": "^4.12.14",
"react-tabs": "^3.0.0",
"slugify": "^1.3.4",
"slugify": "^1.3.5",
"stickyfill": "^1.1.1",
"swagger2openapi": "^5.2.3",
"tslib": "^1.9.3"
"swagger2openapi": "^5.3.1",
"tslib": "^1.10.0",
"url-template": "^2.0.8"
},
"bundlesize": [
{

View File

@ -5,11 +5,7 @@ import { ClipboardService } from '../services/ClipboardService';
export interface CopyButtonWrapperProps {
data: any;
children: (
props: {
renderCopyButton: (() => React.ReactNode);
},
) => React.ReactNode;
children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
}
export class CopyButtonWrapper extends React.PureComponent<

View File

@ -55,7 +55,7 @@ export const StyledDropdown = styled(Dropdown)`
display: block;
height: 0;
position: absolute;
right: 0.35em;
right: 0.3em;
top: 50%;
margin-top: -0.125em;
width: 0;
@ -97,7 +97,7 @@ export const StyledDropdown = styled(Dropdown)`
export const SimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
font-size: 0.969em;
.Dropdown-control {
font-size: 1em;

View File

@ -75,7 +75,7 @@ export const PropertyNameCell = styled(PropertyCell)`
line-height: 20px;
white-space: nowrap;
font-size: 0.929em;
font-family: ${props => props.theme.typography.headings.fontFamily};
font-family: ${props => props.theme.typography.code.fontFamily};
&.deprecated {
${deprecatedCss};

View File

@ -32,6 +32,7 @@ export const TypeName = styled(FieldLabel)`
export const TypeTitle = styled(FieldLabel)`
color: ${props => props.theme.schema.typeTitleColor};
word-break: break-word;
`;
export const TypeFormat = TypeName;
@ -60,14 +61,6 @@ export const PatternLabel = styled(FieldLabel)`
&::after {
font-weight: bold;
}
&::before {
content: ' /';
}
&::after {
content: '/ ';
}
`;
export const ExampleValue = styled(FieldLabel)`
@ -76,11 +69,9 @@ export const ExampleValue = styled(FieldLabel)`
background-color: ${transparentize(0.95, theme.colors.text.primary)};
color: ${transparentize(0.1, theme.colors.text.primary)};
margin: ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.text.primary)};
font-family: ${theme.typography.code.fontFamily};
color: ${theme.typography.code.color};
}`};
& + & {
margin-left: 0;
@ -99,6 +90,7 @@ export const ConstraintItem = styled(FieldLabel)`
margin: 0 ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.primary.main)};
font-family: ${theme.typography.code.fontFamily};
}`};
& + & {
margin-left: 0;

View File

@ -1,15 +1,21 @@
import { SECTION_ATTR } from '../services/MenuStore';
import styled, { media } from '../styled-components';
export const MiddlePanel = styled.div`
export const MiddlePanel = styled.div<{ compact?: boolean }>`
width: calc(100% - ${props => props.theme.rightPanel.width});
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
${({ compact, theme }) =>
media.lessThan('medium', true)`
width: 100%;
padding: ${props =>
`${props.theme.spacing.sectionVertical}px ${props.theme.spacing.sectionHorizontal}px`};
padding: ${`${compact ? 0 : theme.spacing.sectionVertical}px ${
theme.spacing.sectionHorizontal
}px`};
`};
`;
export const Section = styled.div.attrs(props => ({
[SECTION_ATTR]: props.id,
}))<{ underlined?: boolean }>`
})) <{ underlined?: boolean }>`
padding: ${props => props.theme.spacing.sectionVertical}px 0;
&:last-child {

View File

@ -18,6 +18,7 @@ export const OneOfLabel = styled.span`
export const OneOfButton = styled.li<{ active: boolean }>`
display: inline-block;
margin-right: 10px;
margin-bottom: 5px;
font-size: 0.8em;
cursor: pointer;
border: 1px solid ${props => props.theme.colors.primary.main};

View File

@ -98,7 +98,7 @@ export const SmallTabs = styled(Tabs)`
> .react-tabs__tab-panel {
& > div,
& > pre {
padding: ${props => props.theme.spacing.unit * 2} 0;
padding: ${props => props.theme.spacing.unit * 2}px 0;
}
}
`;

View File

@ -44,8 +44,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null;
const website =
(info.contact &&
info.contact.url && (
(info.contact && info.contact.url && (
<InfoSpan>
URL: <a href={info.contact.url}>{info.contact.url}</a>
</InfoSpan>
@ -53,8 +52,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null;
const email =
(info.contact &&
info.contact.email && (
(info.contact && info.contact.email && (
<InfoSpan>
{info.contact.name || 'E-mail'}:{' '}
<a href={'mailto:' + info.contact.email}>{info.contact.email}</a>
@ -70,11 +68,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) ||
null;
const version =
(info.version && (
<span>({info.version})</span>
)) ||
null;
const version = (info.version && <span>({info.version})</span>) || null;
return (
<Section>

View File

@ -17,13 +17,11 @@ export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
// Use the english word logo if no alt text is provided
const altText = logoInfo.altText ? logoInfo.altText : 'logo';
const logo = (
<LogoImgEl
src={logoInfo.url}
style={{ backgroundColor: logoInfo.backgroundColor }}
alt={altText}
/>
const logo = <LogoImgEl src={logoInfo.url} alt={altText} />;
return (
<LogoWrap style={{ backgroundColor: logoInfo.backgroundColor }}>
{logoHref ? LinkWrap(logoHref)(logo) : logo}
</LogoWrap>
);
return <LogoWrap>{logoHref ? LinkWrap(logoHref)(logo) : logo}</LogoWrap>;
}
}

View File

@ -4,13 +4,13 @@ import styled from '../../styled-components';
export const LogoImgEl = styled.img`
max-height: ${props => props.theme.logo.maxHeight};
max-width: ${props => props.theme.logo.maxWidth};
padding: ${props => props.theme.logo.gutter};
width: 100%;
display: block;
`;
export const LogoWrap = styled.div`
text-align: center;
padding: ${props => props.theme.logo.gutter};
`;
const Link = styled.a`

View File

@ -60,7 +60,7 @@ export class ContentItem extends React.Component<ContentItemProps> {
}
}
const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
const middlePanelWrap = component => <MiddlePanel compact={true}>{component}</MiddlePanel>;
@observer
export class SectionItem extends React.Component<ContentItemProps> {
@ -71,7 +71,7 @@ export class SectionItem extends React.Component<ContentItemProps> {
return (
<>
<Row>
<MiddlePanel>
<MiddlePanel compact={level !== 1}>
<Header>
<ShareLink to={this.props.item.id} />
{name}

View File

@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils';
import { expandDefaultServerVariables, getBasePath } from '../../utils';
import {
EndpointInfo,
HttpVerb,
@ -60,21 +60,26 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
/>
</EndpointInfo>
<ServersOverlay expanded={expanded}>
{operation.servers.map(server => (
<ServerItem key={server.url}>
{operation.servers.map(server => {
const normalizedUrl = options.expandDefaultServerVariables
? expandDefaultServerVariables(server.url, server.variables)
: server.url;
return (
<ServerItem key={normalizedUrl}>
<Markdown source={server.description || ''} compact={true} />
<SelectOnClick>
<ServerUrl>
<span>
{hideHostname || options.hideHostname
? getBasePath(server.url)
: server.url}
? getBasePath(normalizedUrl)
: normalizedUrl}
</span>
{operation.path}
</ServerUrl>
</SelectOnClick>
</ServerItem>
))}
);
})}
</ServersOverlay>
</OperationEndpointWrap>
)}

View File

@ -1,14 +1,19 @@
import * as React from 'react';
import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export interface EnumValuesProps {
values: string[];
type: string;
}
export class EnumValues extends React.PureComponent<EnumValuesProps> {
static contextType = OptionsContext;
render() {
const { values, type } = this.props;
const { enumSkipQuotes } = this.context;
if (!values.length) {
return null;
}
@ -16,11 +21,17 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
return (
<div>
<FieldLabel>
{type === 'array' ? 'Items' : ''} {values.length === 1 ? 'Value' : 'Enum'}:
</FieldLabel>
{values.map((value, idx) => (
<ExampleValue key={idx}>{JSON.stringify(value)} </ExampleValue>
))}
{type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>{' '}
{values.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return (
<React.Fragment key={idx}>
<ExampleValue>{exampleValue}</ExampleValue>{' '}
</React.Fragment>
);
})}
</div>
);
}

View File

@ -4,6 +4,7 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
export interface FieldDetailProps {
value?: any;
label: string;
raw?: boolean;
}
export class FieldDetail extends React.PureComponent<FieldDetailProps> {
@ -11,10 +12,12 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
if (this.props.value === undefined) {
return null;
}
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);
return (
<div>
<FieldLabel> {this.props.label} </FieldLabel>{' '}
<ExampleValue> {JSON.stringify(this.props.value)} </ExampleValue>
<FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue>
</div>
);
}

View File

@ -9,6 +9,7 @@ import {
TypePrefix,
TypeTitle,
} from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown';
import { EnumValues } from './EnumValues';
@ -19,12 +20,31 @@ import { FieldDetail } from './FieldDetail';
import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export class FieldDetails extends React.PureComponent<FieldProps> {
static contextType = OptionsContext;
render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { enumSkipQuotes } = this.context;
const { schema, description, example, deprecated } = field;
const rawDefault = !!enumSkipQuotes || field.in === 'header'; // having quotes around header field default values is confusing and inappropriate
let exampleField: JSX.Element | null = null;
if (showExamples && example !== undefined) {
const label = l('example') + ':';
if (field.in && (field.style || field.serializationMime)) {
const serializedValue = serializeParameterValue(field, example);
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
} else {
exampleField = <FieldDetail label={label} value={example} />;
}
}
return (
<div>
<div>
@ -40,18 +60,18 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
)}
{schema.title && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> Nullable </NullableLabel>}
{schema.pattern && <PatternLabel>{schema.pattern}</PatternLabel>}
{schema.isCircular && <RecursiveLabel> Recursive </RecursiveLabel>}
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
{schema.pattern && <PatternLabel> {schema.pattern} </PatternLabel>}
{schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
</div>
{deprecated && (
<div>
<Badge type="warning"> Deprecated </Badge>
<Badge type="warning"> {l('deprecated')} </Badge>
</div>
)}
<FieldDetail label={'Default:'} value={schema.default} />
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{showExamples && <FieldDetail label={'Example:'} value={example} />}
{exampleField}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div>
<Markdown compact={true} source={description} />

View File

@ -5,6 +5,7 @@ import { SampleControls } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv';
import { jsonToHTML } from '../../utils/jsonToHtml';
import { OptionsContext } from '../OptionsProvider';
import { jsonStyles } from './style';
export interface JsonProps {
@ -32,12 +33,18 @@ class Json extends React.PureComponent<JsonProps> {
<span onClick={this.expandAll}> Expand all </span>
<span onClick={this.collapseAll}> Collapse all </span>
</SampleControls>
<OptionsContext.Consumer>
{options => (
<PrismDiv
className={this.props.className}
// tslint:disable-next-line
ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}
/>
)}
</OptionsContext.Consumer>
</JsonViewerWrap>
);

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { DropdownProps } from '../../common-elements/dropdown';
import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface MediaTypeChildProps {
schema: SchemaModel;
@ -11,6 +12,8 @@ export interface MediaTypeChildProps {
export interface MediaTypesSwitchProps {
content?: MediaContentModel;
withLabel?: boolean;
renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeMime: MediaTypeModel) => JSX.Element;
}
@ -37,13 +40,25 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
};
});
const Wrapper = ({ children }) =>
this.props.withLabel ? (
<DropdownWrapper>
<DropdownLabel>Content type</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return (
<>
<Wrapper>
{this.props.renderDropdown({
value: options[activeMimeIdx],
options,
onChange: this.switchMedia,
})}
</Wrapper>
{this.props.children(content.active)}
</>
);

View File

@ -2,9 +2,6 @@ import * as React from 'react';
import { StyledPre } from '../../common-elements/samples';
import { ExampleModel } from '../../services/models';
import { isJsonLike, langFromMime } from '../../utils';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { ExampleValue } from './ExampleValue';
import { useExternalExample } from './exernalExampleHook';
@ -22,14 +19,13 @@ export function Example({ example, mimeType }: ExampleProps) {
}
export function ExternalExample({ example, mimeType }: ExampleProps) {
let value = useExternalExample(example, mimeType);
const value = useExternalExample(example, mimeType);
if (value === undefined) {
return <span>Loading...</span>;
}
if (value instanceof Error) {
console.log(value);
return (
<StyledPre>
Error loading external example: <br />
@ -40,13 +36,5 @@ export function ExternalExample({ example, mimeType }: ExampleProps) {
);
}
if (isJsonLike(mimeType)) {
return <JsonViewer data={value} />;
} else {
if (typeof value === 'object') {
// just in case example was cached as json but used as non-json
value = JSON.stringify(value, null, 2);
}
return <SourceCodeWithCopy lang={langFromMime(mimeType)} source={value} />;
}
return <ExampleValue value={value} mimeType={mimeType} />;
}

View File

@ -13,6 +13,10 @@ export function ExampleValue({ value, mimeType }: ExampleValueProps) {
if (isJsonLike(mimeType)) {
return <JsonViewer data={value} />;
} else {
if (typeof value === 'object') {
// just in case example was cached as json but used as non-json
value = JSON.stringify(value, null, 2);
}
return <SourceCodeWithCopy lang={langFromMime(mimeType)} source={value} />;
}
}

View File

@ -1,17 +1,33 @@
import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import styled from '../../styled-components';
import { DropdownProps } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { Markdown } from '../Markdown/Markdown';
import { Example } from './Example';
import { NoSampleLabel } from './styled.elements';
import { DropdownLabel, DropdownWrapper, NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps {
mediaType: MediaTypeModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
interface MediaTypeSamplesState {
activeIdx: number;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps, MediaTypeSamplesState> {
state = {
activeIdx: 0,
};
switchMedia = ({ value }) => {
this.setState({
activeIdx: parseInt(value, 10),
});
};
render() {
const { activeIdx } = this.state;
const examples = this.props.mediaType.examples || {};
const mimeType = this.props.mediaType.name;
@ -21,28 +37,46 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
if (examplesNames.length === 0) {
return noSample;
}
if (examplesNames.length > 1) {
const options = examplesNames.map((name, idx) => {
return {
label: examples[name].summary || name,
value: idx.toString(),
};
});
const example = examples[examplesNames[activeIdx]];
const description = example.description;
return (
<SmallTabs defaultIndex={0}>
<TabList>
{examplesNames.map(name => (
<Tab key={name}> {examples[name].summary || name} </Tab>
))}
</TabList>
{examplesNames.map(name => (
<TabPanel key={name}>
<Example example={examples[name]} mimeType={mimeType} />
</TabPanel>
))}
</SmallTabs>
<SamplesWrapper>
<DropdownWrapper>
<DropdownLabel>Example</DropdownLabel>
{this.props.renderDropdown({
value: options[activeIdx],
options,
onChange: this.switchMedia,
})}
</DropdownWrapper>
<div>
{description && <Markdown source={description} />}
<Example example={example} mimeType={mimeType} />
</div>
</SamplesWrapper>
);
} else {
const name = examplesNames[0];
const example = examples[examplesNames[0]];
return (
<div>
<Example example={examples[name]} mimeType={mimeType} />
</div>
<SamplesWrapper>
{example.description && <Markdown source={example.description} />}
<Example example={example} mimeType={mimeType} />
</SamplesWrapper>
);
}
}
}
const SamplesWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -2,10 +2,9 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { MediaTypeSamples } from './MediaTypeSamples';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { MediaContentModel } from '../../services/models';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
export interface PayloadSamplesProps {
@ -21,8 +20,14 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
}
return (
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown}>
{mediaType => <MediaTypeSamples key="samples" mediaType={mediaType} />}
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
{mediaType => (
<MediaTypeSamples
key="samples"
mediaType={mediaType}
renderDropdown={this.renderDropdown}
/>
)}
</MediaTypesSwitch>
);
}

View File

@ -13,8 +13,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
prevRef.current = example;
useEffect(
() => {
useEffect(() => {
const load = async () => {
setIsLoading(true);
try {
@ -26,9 +25,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
};
load();
},
[example, mimeType],
);
}, [example, mimeType]);
return value.current;
}

View File

@ -1,42 +1,58 @@
// @ts-ignore
import Dropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div`
position: relative;
top: -35px;
left: 132px;
margin: 0;
font-size: 0.929em;
color: #000;
padding: 12px;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0;
display: block;
`;
export const DropdownLabel = styled.span`
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 12px;
position: absolute;
z-index: 1;
top: -11px;
left: 12px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
`;
export const DropdownWrapper = styled.div`
position: relative;
`;
export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control,
.Dropdown-control:hover {
font-size: 1em;
border: none;
padding: 0 1.2em 0 0;
padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none;
.Dropdown-arrow {
border-top-color: ${({ theme }) => theme.rightPanel.textColor};
}
}
.Dropdown-menu {
margin: 0;
margin-top: 2px;
}
`;

View File

@ -1,7 +1,8 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { Tab, TabList, TabPanel, Tabs, UnderlinedHeader } from '../../common-elements';
import { OperationModel } from '../../services/models';
import { OperationModel, RedocNormalizedOptions } from '../../services';
import { OptionsContext } from '../OptionsProvider';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
@ -11,6 +12,8 @@ export interface RequestSamplesProps {
@observer
export class RequestSamples extends React.Component<RequestSamplesProps> {
static contextType = OptionsContext;
context: RedocNormalizedOptions;
operation: OperationModel;
render() {
@ -20,6 +23,7 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
const samples = operation.codeSamples;
const hasSamples = hasBodySample || samples.length > 0;
return (
(hasSamples && (
<div>

View File

@ -22,7 +22,7 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
<ShelfIcon
size={'1.5em'}
color={type}
direction={opened ? 'up' : 'down'}
direction={opened ? 'down' : 'right'}
float={'left'}
/>
)}

View File

@ -34,9 +34,9 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
const filteredFields = needFilter
? fields.filter(item => {
return (
(this.props.skipReadOnly && !item.schema.readOnly) ||
(this.props.skipWriteOnly && !item.schema.writeOnly)
return !(
(this.props.skipReadOnly && item.schema.readOnly) ||
(this.props.skipWriteOnly && item.schema.writeOnly)
);
})
: fields;

View File

@ -10,6 +10,8 @@ import { ArraySchema } from './ArraySchema';
import { ObjectSchema } from './ObjectSchema';
import { OneOfSchema } from './OneOfSchema';
import { l } from '../../services/Labels';
export interface SchemaOptions {
showTitle?: boolean;
skipReadOnly?: boolean;
@ -34,7 +36,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
<div>
<TypeName>{schema.displayType}</TypeName>
{schema.title && <TypeTitle> {schema.title} </TypeTitle>}
<RecursiveLabel> Recursive </RecursiveLabel>
<RecursiveLabel> {l('recursive')} </RecursiveLabel>
</div>
);
}
@ -42,9 +44,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
if (discriminatorProp !== undefined) {
if (!oneOf || !oneOf.length) {
throw new Error(
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${
schema.title
}`,
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${schema.title}`,
);
}
return (
@ -64,9 +64,9 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
switch (type) {
case 'object':
return <ObjectSchema {...this.props as any} />;
return <ObjectSchema {...(this.props as any)} />;
case 'array':
return <ArraySchema {...this.props as any} />;
return <ArraySchema {...(this.props as any)} />;
}
// TODO: maybe adjust FieldDetails to accept schema

View File

@ -0,0 +1,93 @@
import * as React from 'react';
import { DarkRightPanel, MiddlePanel, MimeLabel, Row, Section } from '../../common-elements';
import { MediaTypeModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import styled from '../../styled-components';
import { OpenAPIMediaType } from '../../types';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples';
import { InvertedSimpleDropdown } from '../PayloadSamples/styled.elements';
import { Schema } from '../Schema';
export interface ObjectDescriptionProps {
schemaRef: string;
exampleRef?: string;
showReadOnly?: boolean;
showWriteOnly?: boolean;
parser: OpenAPIParser;
options: RedocNormalizedOptions;
}
export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps> {
private static getMediaType(schemaRef: string, exampleRef?: string): OpenAPIMediaType {
if (!schemaRef) {
return {};
}
const info: OpenAPIMediaType = {
schema: { $ref: schemaRef },
};
if (exampleRef) {
info.examples = { example: { $ref: exampleRef } };
}
return info;
}
private _mediaModel: MediaTypeModel;
private get mediaModel() {
const { parser, schemaRef, exampleRef, options } = this.props;
if (!this._mediaModel) {
this._mediaModel = new MediaTypeModel(
parser,
'json',
false,
SchemaDefinition.getMediaType(schemaRef, exampleRef),
options,
);
}
return this._mediaModel;
}
render() {
const { showReadOnly = true, showWriteOnly = false } = this.props;
return (
<Section>
<Row>
<MiddlePanel>
<Schema
skipWriteOnly={!showWriteOnly}
skipReadOnly={!showReadOnly}
schema={this.mediaModel.schema}
/>
</MiddlePanel>
<DarkRightPanel>
<MediaSamplesWrap>
<MediaTypeSamples renderDropdown={this.renderDropdown} mediaType={this.mediaModel} />
</MediaSamplesWrap>
</DarkRightPanel>
</Row>
</Section>
);
}
private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
};
}
const MediaSamplesWrap = styled.div`
background: ${({ theme }) => theme.codeSample.backgroundColor};
& > div,
& > pre {
padding: ${props => props.theme.spacing.unit * 4}px;
margin: 0;
}
& > div > pre {
padding: 0;
}
`;

View File

@ -4,6 +4,7 @@ import { SecuritySchemesModel } from '../../services/models';
import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { OpenAPISecurityScheme } from '../../types';
import { titleize } from '../../utils/helpers';
import { Markdown } from '../Markdown/Markdown';
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
@ -48,7 +49,7 @@ export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
<strong> Scopes: </strong>
</div>
<ul>
{Object.keys(flow!.scopes).map(scope => (
{Object.keys(flow!.scopes || {}).map(scope => (
<li key={scope}>
<code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} />
</li>
@ -84,7 +85,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
</tr>
{scheme.apiKey ? (
<tr>
<th> {scheme.apiKey.in} parameter name:</th>
<th> {titleize(scheme.apiKey.in || '')} parameter name:</th>
<td> {scheme.apiKey.name} </td>
</tr>
) : scheme.http ? (
@ -93,8 +94,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
<th> HTTP Authorization Scheme </th>
<td> {scheme.http.scheme} </td>
</tr>,
scheme.http.scheme === 'bearer' &&
scheme.http.bearerFormat && (
scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && (
<tr key="bearer">
<th> Bearer format </th>
<td> "{scheme.http.bearerFormat}" </td>

View File

@ -1,3 +1,4 @@
// import { observe } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@ -15,7 +16,7 @@ export interface MenuItemProps {
@observer
export class MenuItem extends React.Component<MenuItemProps> {
ref: Element | null;
ref = React.createRef<HTMLLabelElement>();
activate = (evt: React.MouseEvent<HTMLElement>) => {
this.props.onActivate!(this.props.item);
@ -31,24 +32,15 @@ export class MenuItem extends React.Component<MenuItemProps> {
}
scrollIntoViewIfActive() {
if (this.props.item.active && this.ref) {
this.ref.scrollIntoViewIfNeeded();
if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded();
}
}
saveRef = ref => {
this.ref = ref;
};
render() {
const { item, withoutChildren } = this.props;
return (
<MenuItemLi
onClick={this.activate}
depth={item.depth}
ref={this.saveRef}
data-item-id={item.id}
>
<MenuItemLi onClick={this.activate} depth={item.depth} data-item-id={item.id}>
{item.type === 'operation' ? (
<OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : (
@ -83,11 +75,24 @@ export interface OperationMenuItemContentProps {
}
@observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
export class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
ref = React.createRef<HTMLLabelElement>();
componentDidUpdate() {
if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded();
}
}
render() {
const { item } = this.props;
return (
<MenuItemLabel depth={item.depth} active={item.active} deprecated={item.deprecated}>
<MenuItemLabel
depth={item.depth}
active={item.active}
deprecated={item.deprecated}
ref={this.ref}
>
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
<MenuItemTitle width="calc(100% - 38px)">
{item.name}

View File

@ -2,12 +2,14 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { IMenuItem, MenuStore } from '../../services/MenuStore';
import { OptionsContext } from '../OptionsProvider';
import { MenuItems } from './MenuItems';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
@observer
export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> {
static contextType = OptionsContext;
private _updateScroll?: () => void;
render() {
@ -26,6 +28,10 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
}
activate = (item: IMenuItem) => {
if (item && item.active && this.context.menuToggle) {
return item.expanded ? item.collapse() : item.expand();
}
this.props.menu.activateAndScroll(item, true);
setTimeout(() => {
if (this._updateScroll) {

View File

@ -19,6 +19,10 @@ export interface StickySidebarProps {
menu: MenuStore;
}
export interface StickySidebarState {
offsetTop?: string;
}
const stickyfill = Stickyfill && Stickyfill();
const StyledStickySidebar = styled.div<{ open?: boolean }>`
@ -77,13 +81,26 @@ const FloatingButton = styled.div`
`;
@observer
export class StickyResponsiveSidebar extends React.Component<StickySidebarProps> {
export class StickyResponsiveSidebar extends React.Component<
StickySidebarProps,
StickySidebarState
> {
static contextType = OptionsContext;
context!: React.ContextType<typeof OptionsContext>;
state: StickySidebarState = { offsetTop: '0px' };
stickyElement: Element;
componentDidMount() {
if (stickyfill) {
stickyfill.add(this.stickyElement);
}
// rerender when hydrating from SSR
// see https://github.com/facebook/react/issues/8017#issuecomment-256351955
this.setState({
offsetTop: this.getScrollYOffset(this.context),
});
}
componentWillUnmount() {
@ -92,7 +109,7 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
}
}
getScrollYOffset(options) {
getScrollYOffset(options: RedocNormalizedOptions) {
let top;
if (this.props.scrollYOffset !== undefined) {
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
@ -105,22 +122,17 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
render() {
const open = this.props.menu.sideBarOpened;
const style = options => {
const top = this.getScrollYOffset(options);
return {
top,
height: `calc(100vh - ${top})`,
};
};
const top = this.state.offsetTop;
return (
<OptionsContext.Consumer>
{options => (
<>
<StyledStickySidebar
open={open}
className={this.props.className}
style={style(options)}
style={{
top,
height: `calc(100vh - ${top})`,
}}
// tslint:disable-next-line
ref={el => {
this.stickyElement = el as any;
@ -132,16 +144,10 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
<AnimatedChevronButton open={open} />
</FloatingButton>
</>
)}
</OptionsContext.Consumer>
);
}
private toggleNavMenu = () => {
this.props.menu.toggleSidebar();
};
// private closeNavMenu = () => {
// this.setState({ open: false });
// };
}

View File

@ -10,6 +10,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "",
"example": undefined,
"expanded": false,
"explode": false,
"in": undefined,
"kind": "field",
"name": "packSize",
@ -59,6 +60,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"description": "",
"example": undefined,
"expanded": false,
"explode": false,
"in": undefined,
"kind": "field",
"name": "type",

View File

@ -28,3 +28,5 @@ export * from './OptionsProvider';
export * from './SideMenu/';
export * from './StickySidebar/StickyResponsiveSidebar';
export * from './SearchBox/SearchBox';
export * from './SchemaDefinition/SchemaDefinition';
export * from './SourceCode/SourceCode';

View File

@ -1,8 +1,12 @@
import 'core-js/es6/promise';
import 'core-js/fn/array/find';
import 'core-js/fn/object/assign';
import 'core-js/fn/string/ends-with';
import 'core-js/fn/string/starts-with';
import 'core-js/es/promise';
import 'core-js/es6/map';
import 'core-js/es6/symbol';
import 'core-js/es/array/find';
import 'core-js/es/object/assign';
import 'core-js/es/string/ends-with';
import 'core-js/es/string/starts-with';
import 'core-js/es/map';
import 'core-js/es/symbol';
import 'unfetch/polyfill/index';
import 'url-polyfill';

View File

@ -10,8 +10,13 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore';
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
import {
SCHEMA_DEFINITION_JSX_NAME,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
export interface StoreState {
menu: {
@ -151,5 +156,18 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
securitySchemes: store.spec.securitySchemes,
}),
},
[SECURITY_DEFINITIONS_JSX_NAME]: {
component: SecurityDefs,
propsSelector: (store: AppStore) => ({
securitySchemes: store.spec.securitySchemes,
}),
},
[SCHEMA_DEFINITION_JSX_NAME]: {
component: SchemaDefinition,
propsSelector: (store: AppStore) => ({
parser: store.spec.parser,
options: store.options,
}),
},
},
};

View File

@ -28,7 +28,10 @@ export class ClipboardService {
if ((document as any).selection) {
(document as any).selection.empty();
} else if (window.getSelection) {
window.getSelection()!.removeAllRanges();
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
}
}

View File

@ -13,7 +13,7 @@ export class HistoryService {
}
get currentId(): string {
return IS_BROWSER ? window.location.hash.substring(1) : '';
return IS_BROWSER ? decodeURIComponent(window.location.hash.substring(1)) : '';
}
linkForId(id: string) {

37
src/services/Labels.ts Normal file
View File

@ -0,0 +1,37 @@
export interface LabelsConfig {
enum: string;
enumSingleValue: string;
enumArray: string;
default: string;
deprecated: string;
example: string;
nullable: string;
recursive: string;
arrayOf: string;
}
export type LabelsConfigRaw = Partial<LabelsConfig>;
const labels: LabelsConfig = {
enum: 'Enum',
enumSingleValue: 'Value',
enumArray: 'Items',
default: 'Default',
deprecated: 'Deprecated',
example: 'Example',
nullable: 'Nullable',
recursive: 'Recursive',
arrayOf: 'Array of ',
};
export function setRedocLabels(_labels?: LabelsConfigRaw) {
Object.assign(labels, _labels);
}
export function l(key: keyof LabelsConfig, idx?: number): string {
const label = labels[key];
if (idx !== undefined) {
return label[idx];
}
return label;
}

View File

@ -1,6 +1,6 @@
import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils';
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -65,6 +65,7 @@ export class MarkdownRenderer {
container: MarkdownHeading[] = this.headings,
parentId?: string,
): MarkdownHeading {
name = unescapeHTMLChars(name);
const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
name,
@ -88,7 +89,7 @@ export class MarkdownRenderer {
}
attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => {
const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
};

View File

@ -42,7 +42,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
@ -59,14 +59,16 @@ export class MenuBuilder {
*/
static addMarkdownItems(
description: string,
parent: GroupModel | undefined,
initialDepth: number,
options: RedocNormalizedOptions,
): ContentItemModel[] {
const renderer = new MarkdownRenderer(options);
const headings = renderer.extractHeadings(description || '');
const mapHeadingsDeep = (parent, items, depth = 1) =>
const mapHeadingsDeep = (_parent, items, depth = 1) =>
items.map(heading => {
const group = new GroupModel('section', heading, parent);
const group = new GroupModel('section', heading, _parent);
group.depth = depth;
if (heading.items) {
group.items = mapHeadingsDeep(group, heading.items, depth + 1);
@ -82,7 +84,7 @@ export class MenuBuilder {
return group;
});
return mapHeadingsDeep(undefined, headings);
return mapHeadingsDeep(parent, headings, initialDepth);
}
/**
@ -144,15 +146,22 @@ export class MenuBuilder {
}
const item = new GroupModel('tag', tag, parent);
item.depth = GROUP_DEPTH + 1;
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
// don't put empty tag into content, instead put its operations
if (tag.name === '') {
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options);
const items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, undefined, tag, item.depth + 1, options),
];
res.push(...items);
continue;
}
item.items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
];
res.push(item);
}
return res;

View File

@ -116,7 +116,7 @@ export class MenuStore {
}
if (isScrolledDown) {
const el = this.getElementAt(itemIdx + 1);
const el = this.getElementAtOrFirstChild(itemIdx + 1);
if (this.scroll.isElementBellow(el)) {
break;
}
@ -163,6 +163,18 @@ export class MenuStore {
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* get section/operation DOM Node related to the item or if it is group item, returns first item of the group
* @param idx item absolute index
*/
getElementAtOrFirstChild(idx: number): Element | null {
let item = this.flatItems[idx];
if (item && item.type === 'group') {
item = item.items[0];
}
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* current active item
*/
@ -189,6 +201,11 @@ export class MenuStore {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
return;
}
if (item && item.type === 'group') {
return;
}
this.deactivate(this.activeItem);
if (!item) {
this.history.replace('', rewriteHistory);

View File

@ -4,7 +4,11 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer';
import { isNamedDefinition, SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
import {
isNamedDefinition,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -40,6 +44,7 @@ class RefCounter {
export class OpenAPIParser {
specUrl?: string;
spec: OpenAPISpec;
mergeRefs: Set<string>;
private _refCounter: RefCounter = new RefCounter();
@ -53,6 +58,8 @@ export class OpenAPIParser {
this.spec = spec;
this.mergeRefs = new Set();
const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') {
this.specUrl = urlResolve(href, specUrl);
@ -74,7 +81,10 @@ export class OpenAPIParser {
) {
// Automatically inject Authentication section with SecurityDefinitions component
const description = spec.info.description || '';
if (!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME)) {
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);
}
@ -176,7 +186,12 @@ export class OpenAPIParser {
schema: OpenAPISchema,
$ref?: string,
forceCircular: boolean = false,
used$Refs = new Set<string>(),
): MergedOpenAPISchema {
if ($ref) {
used$Refs.add($ref);
}
schema = this.hoistOneOfs(schema);
if (schema.allOf === undefined) {
@ -187,6 +202,7 @@ export class OpenAPIParser {
...schema,
allOf: undefined,
parentRefs: [],
title: schema.title || (isNamedDefinition($ref) ? JsonPointer.baseName($ref) : undefined),
};
// avoid mutating inner objects
@ -197,16 +213,25 @@ export class OpenAPIParser {
receiver.items = { ...receiver.items };
}
const allOfSchemas = schema.allOf.map(subSchema => {
const allOfSchemas = schema.allOf
.map(subSchema => {
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
return undefined;
}
const resolved = this.deref(subSchema, forceCircular);
const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular);
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
return {
$ref: subRef,
schema: subMerged,
};
});
})
.filter(child => child !== undefined) as Array<{
$ref: string | undefined;
schema: MergedOpenAPISchema;
}>;
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
if (
@ -257,17 +282,12 @@ export class OpenAPIParser {
receiver.parentRefs!.push(subSchemaRef);
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
// this is not so correct behaviour. comented out for now
// ref: https://github.com/Rebilly/ReDoc/issues/601
// ref: https://github.com/Redocly/redoc/issues/601
// receiver.title = JsonPointer.baseName(subSchemaRef);
}
}
}
// name of definition or title on top level
if (schema.title === undefined && isNamedDefinition($ref)) {
receiver.title = JsonPointer.baseName($ref);
}
return receiver;
}

View File

@ -2,6 +2,7 @@ import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } fr
import { querySelector } from '../utils/dom';
import { isNumeric, mergeObjects } from '../utils/helpers';
import { LabelsConfigRaw, setRedocLabels } from './Labels';
import { MDXComponentMeta } from './MarkdownRenderer';
export interface RedocRawOptions {
@ -20,10 +21,19 @@ export interface RedocRawOptions {
disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[];
hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all';
unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>;
labels?: LabelsConfigRaw;
enumSkipQuotes?: boolean | string;
expandDefaultServerVariables?: boolean;
}
function argValueToBoolean(val?: string | boolean): boolean {
@ -106,6 +116,16 @@ export class RedocNormalizedOptions {
return value;
}
private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number {
if (level === 'all') {
return +Infinity;
}
if (!isNaN(Number(level))) {
return Math.ceil(Number(level));
}
return 2;
}
theme: ResolvedThemeInterface;
scrollYOffset: () => number;
hideHostname: boolean;
@ -120,11 +140,17 @@ export class RedocNormalizedOptions {
disableSearch: boolean;
onlyRequiredInSamples: boolean;
showExtensions: boolean | string[];
hideSingleRequestSampleTab: boolean;
menuToggle: boolean;
jsonSampleExpandLevel: number;
enumSkipQuotes: boolean;
/* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean;
allowedMdComponents: Dict<MDXComponentMeta>;
expandDefaultServerVariables: boolean;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
raw = { ...defaults, ...raw };
const hook = raw.theme && raw.theme.extensionsHook;
@ -134,6 +160,9 @@ export class RedocNormalizedOptions {
this.theme.extensionsHook = hook as any;
// do not support dynamic labels changes. Labels should be configured before
setRedocLabels(raw.labels);
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);
@ -147,9 +176,17 @@ export class RedocNormalizedOptions {
this.disableSearch = true; //argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
raw.jsonSampleExpandLevel,
);
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
this.allowedMdComponents = raw.allowedMdComponents || {};
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
}
}

View File

@ -2,7 +2,7 @@ import * as lunr from 'lunr';
try {
// tslint:disable-next-line
require('core-js/es6/promise'); // bundle into worker
require('core-js/es/promise'); // bundle into worker
} catch (_) {
// nope
}

View File

@ -21,6 +21,7 @@ Object {
"type": "string",
},
},
"title": undefined,
},
Object {
"allOf": undefined,
@ -38,6 +39,7 @@ Object {
"type": "string",
},
},
"title": undefined,
},
],
},
@ -59,6 +61,7 @@ Object {
"type": "string",
},
},
"title": undefined,
},
Object {
"allOf": undefined,
@ -76,6 +79,7 @@ Object {
"type": "string",
},
},
"title": undefined,
},
],
},

View File

@ -10,6 +10,13 @@
"in": "path",
"name": "test_name",
"schema": { "type": "string" }
},
"serializationParam": {
"in": "query",
"name": "serialization_test_name",
"schema": { "type": "array" },
"style": "form",
"explode": true
}
},
"headers": {

View File

@ -26,6 +26,23 @@ describe('Models', () => {
expect(field.schema.type).toEqual('string');
});
test('field details relevant for parameter serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/serializationParam',
},
'#/components/parameters/serializationParam',
opts,
);
expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('query');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('form');
expect(field.explode).toEqual(true);
});
test('field name should populated from name even if $ref (headers)', () => {
const field = new FieldModel(
parser,

View File

@ -37,7 +37,7 @@ describe('Models', () => {
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.WithArray, '', opts);
expect(schema.oneOf).toHaveLength(2);
expect(schema.displayType).toBe('(Array of string or number) or string');
expect(schema.displayType).toBe('(Array of strings or numbers) or string');
});
});
});

View File

@ -15,7 +15,7 @@ export class ExampleModel {
constructor(
parser: OpenAPIParser,
infoOrRef: Referenced<OpenAPIExample>,
mime: string,
public mime: string,
encoding?: { [field: string]: OpenAPIEncoding },
) {
const example = parser.deref(infoOrRef);

View File

@ -1,12 +1,30 @@
import { action, observable } from 'mobx';
import { OpenAPIParameter, Referenced } from '../../types';
import {
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
Referenced,
} from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema';
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
switch (parameterLocation) {
case 'header':
return 'simple';
case 'query':
return 'form';
case 'path':
return 'simple';
default:
return 'form';
}
}
/**
* Field or Parameter model ready to be used by components
*/
@ -20,9 +38,13 @@ export class FieldModel {
description: string;
example?: string;
deprecated: boolean;
in?: string;
in?: OpenAPIParameterLocation;
kind: string;
extensions?: Dict<any>;
explode: boolean;
style?: OpenAPIParameterStyle;
serializationMime?: string;
constructor(
parser: OpenAPIParser,
@ -35,11 +57,29 @@ export class FieldModel {
this.name = infoOrRef.name || info.name;
this.in = info.in;
this.required = !!info.required;
this.schema = new SchemaModel(parser, info.schema || {}, pointer, options);
let fieldSchema = info.schema;
let serializationMime = '';
if (!fieldSchema && info.in && info.content) {
serializationMime = Object.keys(info.content)[0];
fieldSchema = info.content[serializationMime] && info.content[serializationMime].schema;
}
this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options);
this.description =
info.description === undefined ? this.schema.description || '' : info.description;
this.example = info.example || this.schema.example;
if (serializationMime) {
this.serializationMime = serializationMime;
} else if (info.style) {
this.style = info.style;
} else if (this.in) {
this.style = getDefaultStyleValue(this.in);
}
this.explode = !!info.explode;
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef);

View File

@ -40,7 +40,14 @@ export class GroupModel implements IMenuItem {
this.type = type;
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
this.level = (tagOrGroup as MarkdownHeading).level || 1;
// remove sections from markdown, same as in ApiInfo
this.description = tagOrGroup.description || '';
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos);
}
this.parent = parent;
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;

View File

@ -14,10 +14,13 @@ import {
isNamedDefinition,
isPrimitiveType,
JsonPointer,
pluralizeType,
sortByField,
sortByRequired,
} from '../../utils/';
import { l } from '../Labels';
// TODO: refactor this model, maybe use getters instead of copying all the values
export class SchemaModel {
pointer: string;
@ -145,9 +148,9 @@ export class SchemaModel {
this.fields = buildFields(parser, schema, this.pointer, this.options);
} else if (this.type === 'array' && schema.items) {
this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options);
this.displayType = this.items.displayType;
this.displayType = pluralizeType(this.items.displayType);
this.displayFormat = this.items.format;
this.typePrefix = this.items.typePrefix + 'Array of ';
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) {
@ -220,7 +223,7 @@ export class SchemaModel {
if (variant.$ref === undefined) {
continue;
}
const name = JsonPointer.dirName(variant.$ref);
const name = JsonPointer.baseName(variant.$ref);
derived[variant.$ref] = name;
}
}
@ -289,7 +292,10 @@ function buildFields(
new FieldModel(
parser,
{
name: 'property name *',
name: (typeof additionalProps === 'object'
? additionalProps['x-additionalPropertiesName'] || 'property name'
: 'property name'
).concat('*'),
required: false,
schema: additionalProps === true ? {} : additionalProps,
kind: 'additionalProperties',

View File

@ -1,5 +1,5 @@
import slugify from 'slugify';
import { mapWithLast, appendToMdHeading, mergeObjects, safeSlugify } from '../helpers';
import { appendToMdHeading, mapWithLast, mergeObjects, safeSlugify, titleize } from '../helpers';
describe('Utils', () => {
describe('helpers', () => {
@ -60,7 +60,7 @@ describe('Utils', () => {
test('should behave like Object.assign on the top level', () => {
const obj1 = { a: { a1: 'A1' }, c: 'C' };
const obj2 = { a: undefined, b: { b1: 'B1' } };
expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2));
expect(mergeObjects({}, obj1, obj2)).toEqual({ ...obj1, ...obj2 });
});
test('should not merge array values, just override', () => {
const obj1 = { a: ['A', 'B'] };
@ -68,5 +68,11 @@ describe('Utils', () => {
expect(mergeObjects({}, obj1, obj2)).toEqual({ a: ['C'], b: ['D'] });
});
});
describe('titleize', () => {
test('should return the string with the first letter capitalized', () => {
expect(titleize('my title')).toEqual('My title');
});
});
});
});

View File

@ -7,10 +7,13 @@ import {
isPrimitiveType,
mergeParams,
normalizeServers,
pluralizeType,
serializeParameterValue,
} from '../';
import { OpenAPIParser } from '../../services';
import { OpenAPIParameter } from '../../types';
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => {
describe('openapi getStatusCode', () => {
@ -295,11 +298,8 @@ describe('Utils', () => {
it('should expand variables', () => {
const servers = normalizeServers('', [
{
url: '{protocol}{host}{basePath}',
url: 'http://{host}{basePath}',
variables: {
protocol: {
default: 'http://',
},
host: {
default: '127.0.0.1',
},
@ -317,9 +317,15 @@ describe('Utils', () => {
},
]);
expect(servers[0].url).toEqual('http://127.0.0.1/path/to/endpoint');
expect(servers[1].url).toEqual('http://127.0.0.2:{port}');
expect(servers[2].url).toEqual('http://127.0.0.3');
expect(expandDefaultServerVariables(servers[0].url, servers[0].variables)).toEqual(
'http://127.0.0.1/path/to/endpoint',
);
expect(expandDefaultServerVariables(servers[1].url, servers[1].variables)).toEqual(
'http://127.0.0.2:{port}',
);
expect(expandDefaultServerVariables(servers[2].url, servers[2].variables)).toEqual(
'http://127.0.0.3',
);
});
});
@ -353,4 +359,266 @@ describe('Utils', () => {
expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty');
});
});
describe('OpenAPI pluralizeType', () => {
it('should pluralize all simple types', () => {
expect(pluralizeType('string')).toEqual('strings');
expect(pluralizeType('number')).toEqual('numbers');
expect(pluralizeType('object')).toEqual('objects');
expect(pluralizeType('integer')).toEqual('integers');
expect(pluralizeType('boolean')).toEqual('booleans');
expect(pluralizeType('array')).toEqual('arrays');
});
it('should pluralize complex dislay types', () => {
expect(pluralizeType('object (Pet)')).toEqual('objects (Pet)');
expect(pluralizeType('string <email>')).toEqual('strings <email>');
});
it('should pluralize oneOf-ed dislay types', () => {
expect(pluralizeType('object or string')).toEqual('objects or strings');
expect(pluralizeType('object (Pet) or number <int64>')).toEqual(
'objects (Pet) or numbers <int64>',
);
});
it('should not pluralize display types that are already pluralized', () => {
expect(pluralizeType('strings')).toEqual('strings');
expect(pluralizeType('objects (Pet)')).toEqual('objects (Pet)');
expect(pluralizeType('strings <email>')).toEqual('strings <email>');
expect(pluralizeType('objects or strings')).toEqual('objects or strings');
expect(pluralizeType('objects (Pet) or numbers <int64>')).toEqual('objects (Pet) or numbers <int64>');
});
});
describe('openapi serializeParameter', () => {
interface TestCase {
style: OpenAPIParameterStyle;
explode: boolean;
expected: string;
}
interface TestValueTypeGroup {
value: any;
description: string;
cases: TestCase[];
}
interface TestLocationGroup {
location: OpenAPIParameterLocation;
name: string;
description: string;
cases: TestValueTypeGroup[];
}
const testCases: TestLocationGroup[] = [
{
location: 'path',
name: 'id',
description: 'path parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
{ style: 'label', explode: false, expected: '.5' },
{ style: 'label', explode: true, expected: '.5' },
{ style: 'matrix', explode: false, expected: ';id=5' },
{ style: 'matrix', explode: true, expected: ';id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
{ style: 'label', explode: false, expected: '.3,4,5' },
{ style: 'label', explode: true, expected: '.3.4.5' },
{ style: 'matrix', explode: false, expected: ';id=3,4,5' },
{ style: 'matrix', explode: true, expected: ';id=3;id=4;id=5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
{ style: 'label', explode: true, expected: '.role=admin.firstName=Alex' },
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
],
},
],
},
{
location: 'query',
name: 'id',
description: 'query parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
{ style: 'spaceDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'spaceDelimited', explode: false, expected: 'id=3%204%205' },
{ style: 'pipeDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'pipeDelimited', explode: false, expected: 'id=3|4|5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
{ style: 'deepObject', explode: true, expected: 'id[role]=admin&id[firstName]=Alex' },
],
},
],
},
{
location: 'cookie',
name: 'id',
description: 'cookie parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
],
},
],
},
{
location: 'header',
name: 'x-id',
description: 'header parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
],
},
],
},
];
testCases.forEach(locationTestGroup => {
describe(locationTestGroup.description, () => {
locationTestGroup.cases.forEach(valueTypeTestGroup => {
describe(valueTypeTestGroup.description, () => {
valueTypeTestGroup.cases.forEach(testCase => {
it(`should serialize correctly when style is ${testCase.style} and explode is ${testCase.explode}`, () => {
const parameter: OpenAPIParameter = {
name: locationTestGroup.name,
in: locationTestGroup.location,
style: testCase.style,
explode: testCase.explode,
};
const serialized = serializeParameterValue(parameter, valueTypeTestGroup.value);
expect(serialized).toEqual(testCase.expected);
});
});
});
});
});
});
describe('advanced serialization', () => {
it('should serialize correctly query parameter with content with application/json', () => {
const parameter: OpenAPIParameter = {
name: 'id',
in: 'query',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
};
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
const opts = new RedocNormalizedOptions({});
const field = new FieldModel(parser, parameter, '', opts);
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
'id={"name":"test","age":23}',
);
});
it('should serialize correctly header parameter with content with application/json', () => {
const parameter: OpenAPIParameter = {
name: 'x-header',
in: 'header',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
};
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
const opts = new RedocNormalizedOptions({});
const field = new FieldModel(parser, parameter, '', opts);
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
'{"name":"test","age":23}',
);
});
});
});
});

View File

@ -83,7 +83,7 @@ export function appendToMdHeading(md: string, heading: string, content: string)
}
// credits https://stackoverflow.com/a/46973278/1749888
export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T => {
export const mergeObjects = (target: any, ...sources: any[]): any => {
if (!sources.length) {
return target;
}
@ -147,7 +147,7 @@ export function resolveUrl(url: string, to: string) {
let res;
if (to.startsWith('//')) {
const { protocol: specProtocol } = parse(url);
res = `${specProtocol}${to}`;
res = `${specProtocol || 'https:'}${to}`;
} else if (isAbsoluteUrl(to)) {
res = to;
} else if (!to.startsWith('/')) {
@ -163,5 +163,38 @@ export function resolveUrl(url: string, to: string) {
}
export function getBasePath(serverUrl: string): string {
return new URL(serverUrl).pathname;
try {
return parseURL(serverUrl).pathname;
} catch (e) {
// when using with redoc-cli serverUrl can be empty resulting in crash
return serverUrl;
}
}
export function titleize(text: string) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
export function removeQueryString(serverUrl: string): string {
try {
const url = parseURL(serverUrl);
url.search = '';
return url.toString();
} catch (e) {
// when using with redoc-cli serverUrl can be empty resulting in crash
return serverUrl;
}
}
function parseURL(url: string) {
if (typeof URL === 'undefined') {
// node
return new (require('url')).URL(url);
} else {
return new URL(url);
}
}
export function unescapeHTMLChars(str: string): string {
return str.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)));
}

View File

@ -1,11 +1,12 @@
let level = 1;
const COLLAPSE_LEVEL = 2;
export function jsonToHTML(json) {
export function jsonToHTML(json, maxExpandLevel) {
level = 1;
let output = '';
output += '<div class="redoc-json">';
output += valueToHTML(json);
output += '<code>';
output += valueToHTML(json, maxExpandLevel);
output += '</code>';
output += '</div>';
return output;
}
@ -21,8 +22,8 @@ function htmlEncode(t) {
: '';
}
function escapeForStringLiteral(str: string) {
return str.replace(/([\\"])/g, '\\$1');
function stringifyStringLiteral(str: string) {
return JSON.stringify(str).slice(1, -1);
}
function decorateWithSpan(value, className) {
@ -33,20 +34,20 @@ function punctuation(val) {
return '<span class="token punctuation">' + val + '</span>';
}
function valueToHTML(value) {
function valueToHTML(value, maxExpandLevel: number) {
const valueType = typeof value;
let output = '';
if (value === undefined || value === null) {
output += decorateWithSpan('null', 'token keyword');
} else if (value && value.constructor === Array) {
level++;
output += arrayToHTML(value);
output += arrayToHTML(value, maxExpandLevel);
level--;
} else if (value && value.constructor === Date) {
output += decorateWithSpan('"' + value.toISOString() + '"', 'token string');
} else if (valueType === 'object') {
level++;
output += objectToHTML(value);
output += objectToHTML(value, maxExpandLevel);
level--;
} else if (valueType === 'number') {
output += decorateWithSpan(value, 'token number');
@ -57,11 +58,11 @@ function valueToHTML(value) {
'<a href="' +
value +
'">' +
htmlEncode(escapeForStringLiteral(value)) +
htmlEncode(stringifyStringLiteral(value)) +
'</a>' +
decorateWithSpan('"', 'token string');
} else {
output += decorateWithSpan('"' + escapeForStringLiteral(value) + '"', 'token string');
output += decorateWithSpan('"' + stringifyStringLiteral(value) + '"', 'token string');
}
} else if (valueType === 'boolean') {
output += decorateWithSpan(value, 'token boolean');
@ -70,8 +71,8 @@ function valueToHTML(value) {
return output;
}
function arrayToHTML(json) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : '';
function arrayToHTML(json, maxExpandLevel: number) {
const collapsed = level > maxExpandLevel ? 'collapsed' : '';
let output = `<div class="collapser"></div>${punctuation(
'[',
)}<span class="ellipsis"></span><ul class="array collapsible">`;
@ -80,7 +81,7 @@ function arrayToHTML(json) {
for (let i = 0; i < length; i++) {
hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">';
output += valueToHTML(json[i]);
output += valueToHTML(json[i], maxExpandLevel);
if (i < length - 1) {
output += ',';
}
@ -93,8 +94,8 @@ function arrayToHTML(json) {
return output;
}
function objectToHTML(json) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : '';
function objectToHTML(json, maxExpandLevel: number) {
const collapsed = level > maxExpandLevel ? 'collapsed' : '';
const keys = Object.keys(json);
const length = keys.length;
let output = `<div class="collapser"></div>${punctuation(
@ -106,7 +107,7 @@ function objectToHTML(json) {
hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">';
output += '<span class="property token string">"' + htmlEncode(key) + '"</span>: ';
output += valueToHTML(json[key]);
output += valueToHTML(json[key], maxExpandLevel);
if (i < length - 1) {
output += punctuation(',');
}

View File

@ -5,9 +5,9 @@ import { OpenAPISpec } from '../types';
export async function loadAndBundleSpec(specUrlOrObject: object | string): Promise<OpenAPISpec> {
const parser = new JsonSchemaRefParser();
const spec = await parser.bundle(specUrlOrObject, {
const spec = (await parser.bundle(specUrlOrObject, {
resolve: { http: { withCredentials: false } },
} as object);
} as object)) as any;
if (spec.swagger !== undefined) {
return convertSwagger2OpenAPI(spec);
@ -19,7 +19,7 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi
export function convertSwagger2OpenAPI(spec: any): Promise<OpenAPISpec> {
console.warn('[ReDoc Compatibility mode]: Converting OpenAPI 2.0 to OpenAPI 3.0');
return new Promise<OpenAPISpec>((resolve, reject) =>
convertObj(spec, { patch: true, warnOnly: true }, (err, res) => {
convertObj(spec, { patch: true, warnOnly: true, text: '{}' }, (err, res) => {
// TODO: log any warnings
if (err) {
return reject(err);

View File

@ -1,4 +1,5 @@
import { dirname } from 'path';
const URLtemplate = require('url-template');
import { OpenAPIParser } from '../services/OpenAPIParser';
import {
@ -6,12 +7,13 @@ import {
OpenAPIMediaType,
OpenAPIOperation,
OpenAPIParameter,
OpenAPIParameterStyle,
OpenAPISchema,
OpenAPIServer,
Referenced,
} from '../types';
import { IS_BROWSER } from './dom';
import { isNumeric, resolveUrl } from './helpers';
import { isNumeric, removeQueryString, resolveUrl, stripTrailingSlash } from './helpers';
function isWildcardStatusCode(statusCode: string | number): statusCode is string {
return typeof statusCode === 'string' && /\dxx/i.test(statusCode);
@ -135,36 +137,6 @@ export function isFormUrlEncoded(contentType: string): boolean {
return contentType === 'application/x-www-form-urlencoded';
}
function formEncodeField(fieldVal: any, fieldName: string, explode: boolean): string {
if (!fieldVal || !fieldVal.length) {
return fieldName + '=';
}
if (Array.isArray(fieldVal)) {
if (explode) {
return fieldVal.map(val => `${fieldName}=${val}`).join('&');
} else {
return fieldName + '=' + fieldVal.map(val => val.toString()).join(',');
}
} else if (typeof fieldVal === 'object') {
if (explode) {
return Object.keys(fieldVal)
.map(k => `${k}=${fieldVal[k]}`)
.join('&');
} else {
return (
fieldName +
'=' +
Object.keys(fieldVal)
.map(k => `${k},${fieldVal[k]}`)
.join(',')
);
}
} else {
return fieldName + '=' + fieldVal.toString();
}
}
function delimitedEncodeField(fieldVal: any, fieldName: string, delimeter: string): string {
if (Array.isArray(fieldVal)) {
return fieldVal.map(v => v.toString()).join(delimeter);
@ -191,6 +163,18 @@ function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
}
}
function serializeFormValue(name: string, explode: boolean, value: any) {
// Use RFC6570 safe name ([a-zA-Z0-9_]) and replace with our name later
// e.g. URI.template doesn't parse names with hypen (-) which are valid query param names
const safeName = '__redoc_param_name__';
const suffix = explode ? '*' : '';
const template = URLtemplate.parse(`{?${safeName}${suffix}}`);
return template
.expand({ [safeName]: value })
.substring(1)
.replace(/__redoc_param_name__/g, name);
}
/*
* Should be used only for url-form-encoded body payloads
* To be used for parmaters should be extended with other style values
@ -208,8 +192,7 @@ export function urlFormEncodePayload(
const { style = 'form', explode = true } = encoding[fieldName] || {};
switch (style) {
case 'form':
return formEncodeField(fieldVal, fieldName, explode);
break;
return serializeFormValue(fieldName, explode, fieldVal);
case 'spaceDelimited':
return delimitedEncodeField(fieldVal, fieldName, '%20');
case 'pipeDelimited':
@ -226,6 +209,154 @@ export function urlFormEncodePayload(
}
}
function serializePathParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
const suffix = explode ? '*' : '';
let prefix = '';
if (style === 'label') {
prefix = '.';
} else if (style === 'matrix') {
prefix = ';';
}
// Use RFC6570 safe name ([a-zA-Z0-9_]) and replace with our name later
// e.g. URI.template doesn't parse names with hypen (-) which are valid query param names
const safeName = '__redoc_param_name__';
const template = URLtemplate.parse(`{${prefix}${safeName}${suffix}}`);
return template.expand({ [safeName]: value }).replace(/__redoc_param_name__/g, name);
}
function serializeQueryParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'form':
return serializeFormValue(name, explode, value);
case 'spaceDelimited':
if (!Array.isArray(value)) {
console.warn('The style spaceDelimited is only applicable to arrays');
return '';
}
if (explode) {
return serializeFormValue(name, explode, value);
}
return `${name}=${value.join('%20')}`;
case 'pipeDelimited':
if (!Array.isArray(value)) {
console.warn('The style pipeDelimited is only applicable to arrays');
return '';
}
if (explode) {
return serializeFormValue(name, explode, value);
}
return `${name}=${value.join('|')}`;
case 'deepObject':
if (!explode || Array.isArray(value) || typeof value !== 'object') {
console.warn('The style deepObject is only applicable for objects with explode=true');
return '';
}
return deepObjectEncodeField(value, name);
default:
console.warn('Unexpected style for query: ' + style);
return '';
}
}
function serializeHeaderParameter(
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'simple':
const suffix = explode ? '*' : '';
// name is not important here, so use RFC6570 safe name ([a-zA-Z0-9_])
const name = '__redoc_param_name__';
const template = URLtemplate.parse(`{${name}${suffix}}`);
return decodeURIComponent(template.expand({ [name]: value }));
default:
console.warn('Unexpected style for header: ' + style);
return '';
}
}
function serializeCookieParameter(
name: string,
style: OpenAPIParameterStyle,
explode: boolean,
value: any,
): string {
switch (style) {
case 'form':
return serializeFormValue(name, explode, value);
default:
console.warn('Unexpected style for cookie: ' + style);
return '';
}
}
export function serializeParameterValueWithMime(value: any, mime: string): string {
if (isJsonLike(mime)) {
return JSON.stringify(value);
} else {
console.warn(`Parameter serialization as ${mime} is not supported`);
return '';
}
}
export function serializeParameterValue(
parameter: OpenAPIParameter & { serializationMime?: string },
value: any,
): string {
const { name, style, explode = false, serializationMime } = parameter;
if (serializationMime) {
switch (parameter.in) {
case 'path':
case 'header':
return serializeParameterValueWithMime(value, serializationMime);
case 'cookie':
case 'query':
return `${name}=${serializeParameterValueWithMime(value, serializationMime)}`;
default:
console.warn('Unexpected parameter location: ' + parameter.in);
return '';
}
}
if (!style) {
console.warn(`Missing style attribute or content for parameter ${name}`);
return '';
}
switch (parameter.in) {
case 'path':
return serializePathParameter(name, style, explode, value);
case 'query':
return serializeQueryParameter(name, style, explode, value);
case 'header':
return serializeHeaderParameter(style, explode, value);
case 'cookie':
return serializeCookieParameter(name, style, explode, value);
default:
console.warn('Unexpected parameter location: ' + parameter.in);
return '';
}
}
export function langFromMime(contentType: string): string {
if (contentType.search(/xml/i) !== -1) {
return 'xml';
@ -356,7 +487,7 @@ export function mergeSimilarMediaTypes(types: Dict<OpenAPIMediaType>): Dict<Open
return mergedTypes;
}
function expandVariables(url: string, variables: object = {}) {
export function expandDefaultServerVariables(url: string, variables: object = {}) {
return url.replace(
/(?:{)(\w+)(?:})/g,
(match, name) => (variables[name] && variables[name].default) || match,
@ -367,32 +498,41 @@ export function normalizeServers(
specUrl: string | undefined,
servers: OpenAPIServer[],
): OpenAPIServer[] {
const baseUrl =
specUrl === undefined ? (IS_BROWSER ? window.location.href : '') : dirname(specUrl);
const getHref = () => {
if (!IS_BROWSER) {
return '';
}
const href = window.location.href;
return href.endsWith('.html') ? dirname(href) : href;
};
const baseUrl = specUrl === undefined ? removeQueryString(getHref()) : dirname(specUrl);
if (servers.length === 0) {
return [
{
url: baseUrl,
url: stripTrailingSlash(baseUrl),
},
];
}
function normalizeUrl(url: string, variables: object | undefined): string {
url = expandVariables(url, variables);
function normalizeUrl(url: string): string {
return resolveUrl(baseUrl, url);
}
return servers.map(server => {
return {
...server,
url: normalizeUrl(server.url, server.variables),
url: normalizeUrl(server.url),
description: server.description || '',
};
});
}
export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions';
export const SECURITY_DEFINITIONS_JSX_NAME = 'SecurityDefinitions';
export const SCHEMA_DEFINITION_JSX_NAME = 'SchemaDefinition';
export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/';
export function setSecuritySchemePrefix(prefix: string) {
SECURITY_SCHEMES_SECTION_PREFIX = prefix;
@ -416,6 +556,7 @@ export function isRedocExtension(key: string): boolean {
'x-servers': true,
'x-tagGroups': true,
'x-traitTag': true,
'x-additionalPropertiesName': true,
};
return key in redocExtensions;
@ -434,3 +575,10 @@ export function extractExtensions(obj: object, showExtensions: string[] | true):
return acc;
}, {});
}
export function pluralizeType(displayType: string): string {
return displayType
.split(' or ')
.map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2'))
.join(' or ');
}

View File

@ -107,7 +107,7 @@ if (process.env.JOB === 'e2e-guru') {
delete apisGuruList['googleapis.com:mirror']; // bad urls in images
delete apisGuruList['googleapis.com:discovery']; // non-string references
delete apisGuruList['clarify.io']; // non-string references
//delete apisGuruList['pushpay.com']; // https://github.com/Rebilly/ReDoc/issues/30
//delete apisGuruList['pushpay.com']; // https://github.com/Redocly/redoc/issues/30
delete apisGuruList['bbci.co.uk']; // too big
delete apisGuruList['bbc.com']; // too big
delete apisGuruList['osisoft.com']; // too big

View File

@ -33,7 +33,7 @@ try {
const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
-------------------------------------------------------------
Version: ${VERSION}
Repo: https://github.com/Rebilly/ReDoc`;
Repo: https://github.com/Redocly/redoc`;
export default (env: { standalone?: boolean } = {}, { mode }) => ({
entry: env.standalone ? ['./src/polyfills.ts', './src/standalone.tsx'] : './src/index.ts',
@ -65,13 +65,13 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
? {
esprima: 'esprima',
'node-fetch': 'null',
'node-fetch-h2': 'null',
yaml: 'null',
'safe-json-stringify': 'null',
}
: (context, request, callback) => {
// ignore node-fetch dep of swagger2openapi as it is not used
if (/node-fetch$/i.test(request)) {
return callback(null, 'var undefined');
}
if (/esprima$/i.test(request)) {
if (/esprima|node-fetch|node-fetch-h2|yaml|safe-json-stringify$/i.test(request)) {
return callback(null, 'var undefined');
}
return nodeExternals(context, request, callback);

5793
yarn.lock

File diff suppressed because it is too large Load Diff