Merge branch 'master' of github.com:Rebilly/ReDoc

This commit is contained in:
Andrei Sorescu 2019-05-21 11:12:11 +03:00
commit dd2da68e4e
59 changed files with 4942 additions and 3021 deletions

View File

@ -1,3 +1,104 @@
# [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)
### Bug Fixes
* move swagger2openapi to deps because of missing transitive deps ([ed9b878](https://github.com/Rebilly/ReDoc/commit/ed9b878))
### Features
* display requestBody description [#833](https://github.com/Rebilly/ReDoc/issues/833) ([#838](https://github.com/Rebilly/ReDoc/issues/838)) ([56ca371](https://github.com/Rebilly/ReDoc/commit/56ca371))
# [2.0.0-rc.3](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.2...v2.0.0-rc.3) (2019-03-15)
### Bug Fixes
* add extra deref step for anyOf/oneOf variants ([d81b631](https://github.com/Rebilly/ReDoc/commit/d81b631)), closes [#810](https://github.com/Rebilly/ReDoc/issues/810)
* duplicate keys in request samples ([3ce5bff](https://github.com/Rebilly/ReDoc/commit/3ce5bff)), closes [#815](https://github.com/Rebilly/ReDoc/issues/815)
* escape backslashes in string literals ([#823](https://github.com/Rebilly/ReDoc/issues/823)) ([70faca1](https://github.com/Rebilly/ReDoc/commit/70faca1)), closes [#822](https://github.com/Rebilly/ReDoc/issues/822)
* escape quotes in string values ([0473165](https://github.com/Rebilly/ReDoc/commit/0473165)), closes [#882](https://github.com/Rebilly/ReDoc/issues/882)
* pin lunr version in ReDoc ([178ff4c](https://github.com/Rebilly/ReDoc/commit/178ff4c)), closes [#844](https://github.com/Rebilly/ReDoc/issues/844)
* set last section min-height ([4dd79cd](https://github.com/Rebilly/ReDoc/commit/4dd79cd)), closes [#820](https://github.com/Rebilly/ReDoc/issues/820)
### Features
* support externalValue for examples ([2cdfcd2](https://github.com/Rebilly/ReDoc/commit/2cdfcd2)), closes [#551](https://github.com/Rebilly/ReDoc/issues/551) [#840](https://github.com/Rebilly/ReDoc/issues/840)
* **cli:** Add templateOptions param to pass additional data to custom template ([#792](https://github.com/Rebilly/ReDoc/issues/792)) ([4e8ee03](https://github.com/Rebilly/ReDoc/commit/4e8ee03))
# [2.0.0-rc.2](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.1...v2.0.0-rc.2) (2019-01-27) # [2.0.0-rc.2](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.1...v2.0.0-rc.2) (2019-01-27)

View File

@ -165,7 +165,7 @@ Also you can pass options:
specUrl="http://rebilly.github.io/RebillyAPI/openapi.json" specUrl="http://rebilly.github.io/RebillyAPI/openapi.json"
options={{ options={{
nativeScrollbars: true, nativeScrollbars: true,
theme: { colors: { 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 ## 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: 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:

View File

@ -13,7 +13,7 @@ import * as zlib from 'zlib';
// @ts-ignore // @ts-ignore
import { createStore, loadAndBundleSpec, Redoc } from 'redoc'; import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
import {watch} from 'chokidar'; import { watch } from 'chokidar';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs'; import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs';
import * as mkdirp from 'mkdirp'; import * as mkdirp from 'mkdirp';
@ -25,6 +25,7 @@ interface Options {
cdn?: boolean; cdn?: boolean;
output?: string; output?: string;
title?: string; title?: string;
port?: number;
templateFileName?: string; templateFileName?: string;
templateOptions?: any; templateOptions?: any;
redocOptions?: any; redocOptions?: any;
@ -62,16 +63,16 @@ YargsParser.command(
return yargs; return yargs;
}, },
async argv => { async argv => {
const config = { const config: Options = {
ssr: argv.ssr, ssr: argv.ssr as boolean,
watch: argv.watch, watch: argv.watch as boolean,
templateFileName: argv.template, templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {}, templateOptions: argv.templateOptions || {},
redocOptions: argv.options || {}, redocOptions: argv.options || {},
}; };
try { try {
await serve(argv.port, argv.spec, config); await serve(argv.port as number, argv.spec as string, config);
} catch (e) { } catch (e) {
handleError(e); handleError(e);
} }
@ -108,12 +109,12 @@ YargsParser.command(
return yargs; return yargs;
}, },
async argv => { async argv => {
const config = { const config: Options = {
ssr: true, ssr: true,
output: argv.o, output: argv.o as string,
cdn: argv.cdn, cdn: argv.cdn as boolean,
title: argv.title, title: argv.title as string,
templateFileName: argv.template, templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {}, templateOptions: argv.templateOptions || {},
redocOptions: argv.options || {}, redocOptions: argv.options || {},
}; };
@ -132,7 +133,8 @@ YargsParser.command(
type: 'string', type: 'string',
}) })
.options('templateOptions', { .options('templateOptions', {
describe: 'Additional options that you want pass to template. Use dot notation, e.g. templateOptions.metaDescription', describe:
'Additional options that you want pass to template. Use dot notation, e.g. templateOptions.metaDescription',
}) })
.options('options', { .options('options', {
describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars', describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars',
@ -190,7 +192,8 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
log('Updated successfully'); log('Updated successfully');
} catch (e) { } catch (e) {
console.error('Error while updating: ', e.message); console.error('Error while updating: ', e.message);
}}) }
})
.on('error', error => console.error(`Watcher error: ${error}`)) .on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`)); .on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
} }
@ -247,13 +250,13 @@ async function getPageHTML(
ssr ssr
? 'hydrate(__redoc_state, container);' ? 'hydrate(__redoc_state, container);'
: `init("spec.json", ${JSON.stringify(redocOptions)}, container)` : `init("spec.json", ${JSON.stringify(redocOptions)}, container)`
}; };
</script>`, </script>`,
redocHead: ssr redocHead: ssr
? (cdn ? (cdn
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>' ? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`) + css : `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>', : '<script src="redoc.standalone.js"></script>',
title, title,
templateOptions, templateOptions,

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.7.1", "version": "0.8.4",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -13,10 +13,11 @@
"isarray": "^2.0.4", "isarray": "^2.0.4",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mobx": "^4.2.0", "mobx": "^4.2.0",
"react": "^16.6.3", "node-libs-browser": "^2.2.0",
"react-dom": "^16.6.3", "react": "^16.8.4",
"redoc": "^2.0.0-rc.1", "react-dom": "^16.8.4",
"styled-components": "^4.1.1", "redoc": "^2.0.0-rc.8-1",
"styled-components": "^4.1.3",
"tslib": "^1.9.3", "tslib": "^1.9.3",
"yargs": "^12.0.5" "yargs": "^12.0.5"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,13 @@
Serve remote spec by URL: Serve remote spec by URL:
docker run -it --rm -p 80:80 \ docker run -it --rm -p 80:80 \
-e SPEC_URL='http://localhost:8000/swagger.yaml' redoc -e SPEC_URL='http://localhost:8000/swagger.yaml' redocly/redoc
Serve local file: Serve local file:
docker run -it --rm -p 80:80 \ docker run -it --rm -p 80:80 \
-v $(PWD)/demo/swagger.yaml:/usr/share/nginx/html/swagger.yaml \ -v $(pwd)/demo/swagger.yaml:/usr/share/nginx/html/swagger.yaml \
-e SPEC_URL=swagger.yaml redoc -e SPEC_URL=swagger.yaml redocly/redoc
## Runtime configuration options ## Runtime configuration options
@ -23,4 +23,4 @@ Serve local file:
## Build ## Build
docker build -t redoc . docker build -t redocly/redoc .

2
custom.d.ts vendored
View File

@ -18,6 +18,8 @@ declare module '*.css' {
declare var __REDOC_VERSION__: string; declare var __REDOC_VERSION__: string;
declare var __REDOC_REVISION__: string; declare var __REDOC_REVISION__: string;
declare var reactHotLoaderGlobal: any;
interface Element { interface Element {
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void; scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
} }

View File

@ -17,9 +17,12 @@ const renderRoot = (props: RedocProps) =>
); );
const big = window.location.search.indexOf('big') > -1; const big = window.location.search.indexOf('big') > -1;
const swagger = window.location.search.indexOf('swagger') > -1; // compatibility mode ? const swagger = window.location.search.indexOf('swagger') > -1;
const specUrl = swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml'; const userUrl = window.location.search.match(/url=(.*)$/);
const specUrl =
(userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml');
let store; let store;
const options: RedocRawOptions = { nativeScrollbars: false, parentElementSelector: '#redoc-container' }; const options: RedocRawOptions = { nativeScrollbars: false, parentElementSelector: '#redoc-container' };

View File

@ -57,8 +57,8 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
env.playground env.playground
? 'playground/hmr-playground.tsx' ? 'playground/hmr-playground.tsx'
: env.bench : env.bench
? '../benchmark/index.tsx' ? '../benchmark/index.tsx'
: 'index.tsx', : 'index.tsx',
), ),
], ],
output: { output: {
@ -77,6 +77,12 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'], extensions: ['.ts', '.tsx', '.js', '.json'],
alias:
mode !== 'production'
? {
'react-dom': '@hot-loader/react-dom',
}
: {},
}, },
node: { node: {
@ -88,6 +94,9 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
externals: { externals: {
esprima: 'esprima', esprima: 'esprima',
'node-fetch': 'null', 'node-fetch': 'null',
'node-fetch-h2': 'null',
yaml: 'null',
'safe-json-stringify': 'null',
}, },
module: { module: {
@ -105,7 +114,6 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
loader: 'css-loader', loader: 'css-loader',
options: { options: {
sourceMap: true, sourceMap: true,
minimize: true,
}, },
}, },
}, },
@ -136,8 +144,8 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
template: env.playground template: env.playground
? 'demo/playground/index.html' ? 'demo/playground/index.html'
: env.bench : env.bench
? 'benchmark/index.html' ? 'benchmark/index.html'
: 'demo/index.html', : 'demo/index.html',
}), }),
new ForkTsCheckerWebpackPlugin(), new ForkTsCheckerWebpackPlugin(),
ignore(/js-yaml\/dumper\.js$/), ignore(/js-yaml\/dumper\.js$/),

View File

@ -134,13 +134,6 @@ Extends OpenAPI [Tag Object](http://swagger.io/specification/#tagObject)
| :------------- | :------: | :---------- | | :------------- | :------: | :---------- |
| x-traitTag | boolean | In Swagger two operations can have multiple tags. This property distinguishes between tags that are used to group operations (default) from tags that are used to mark operation with certain trait (`true` value) | | x-traitTag | boolean | In Swagger two operations can have multiple tags. This property distinguishes between tags that are used to group operations (default) from tags that are used to mark operation with certain trait (`true` value) |
#### x-displayName
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-displayName | string | Defines the text that is used for this tag in the menu and in section headings |
###### Usage in Redoc ###### Usage in Redoc
Tags that have `x-traitTag` set to `true` are listed in side-menu but don't have any subitems (operations). Tag `description` is rendered as well. Tags that have `x-traitTag` set to `true` are listed in side-menu but don't have any subitems (operations). Tag `description` is rendered as well.
This is useful for handling out common things like Pagination, Rate-Limits, etc. This is useful for handling out common things like Pagination, Rate-Limits, etc.
@ -161,6 +154,12 @@ description: Pagination description (can use markdown syntax)
x-traitTag: true x-traitTag: true
``` ```
#### x-displayName
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-displayName | string | Defines the text that is used for this tag in the menu and in section headings |
### Operation Object vendor extensions ### Operation Object vendor extensions
Extends OpenAPI [Operation Object](http://swagger.io/specification/#operationObject) Extends OpenAPI [Operation Object](http://swagger.io/specification/#operationObject)
#### x-code-samples #### x-code-samples

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

@ -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,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.2", "version": "2.0.0-rc.8-1",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -36,7 +36,7 @@
"cy:open": "cypress open", "cy:open": "cypress open",
"bundle:clean": "rimraf bundles", "bundle:clean": "rimraf bundles",
"bundle:standalone": "webpack --env.standalone --mode=production", "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", "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/", "declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
"stats": "webpack --env.standalone --json --profile --mode=production > stats.json", "stats": "webpack --env.standalone --json --profile --mode=production > stats.json",
@ -52,106 +52,109 @@
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ." "docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.1.6", "@babel/core": "7.3.4",
"@babel/plugin-syntax-decorators": "7.1.0", "@babel/plugin-syntax-decorators": "7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-jsx": "7.0.0", "@babel/plugin-syntax-jsx": "7.2.0",
"@babel/plugin-syntax-typescript": "7.1.5", "@babel/plugin-syntax-typescript": "7.3.3",
"@cypress/webpack-preprocessor": "4.0.2", "@cypress/webpack-preprocessor": "4.0.3",
"@hot-loader/react-dom": "^16.8.4",
"@types/chai": "4.1.7", "@types/chai": "4.1.7",
"@types/dompurify": "^0.0.32", "@types/dompurify": "^0.0.32",
"@types/enzyme": "^3.1.15", "@types/enzyme": "^3.9.0",
"@types/enzyme-to-json": "^1.5.2", "@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^23.3.9", "@types/jest": "^24.0.11",
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.118", "@types/lodash": "^4.14.122",
"@types/lunr": "^2.1.6", "@types/lunr": "^2.3.2",
"@types/mark.js": "^8.11.1", "@types/mark.js": "^8.11.3",
"@types/marked": "^0.6.0", "@types/marked": "^0.6.3",
"@types/prismjs": "^1.6.4", "@types/prismjs": "^1.9.1",
"@types/prop-types": "^15.5.6", "@types/prop-types": "^15.7.0",
"@types/react": "^16.7.7", "@types/react": "^16.8.7",
"@types/react-dom": "^16.0.10", "@types/react-dom": "^16.8.2",
"@types/react-hot-loader": "^4.1.0", "@types/react-hot-loader": "^4.1.0",
"@types/react-tabs": "^2.3.0", "@types/react-tabs": "^2.3.1",
"@types/styled-components": "^4.1.1", "@types/styled-components": "^4.1.12",
"@types/tapable": "1.0.4", "@types/tapable": "1.0.4",
"@types/webpack": "^4.4.19", "@types/webpack": "^4.4.25",
"@types/webpack-env": "^1.13.0", "@types/webpack-env": "^1.13.9",
"@types/yargs": "^12.0.1", "@types/yargs": "^12.0.9",
"babel-loader": "8.0.4", "babel-loader": "8.0.5",
"babel-plugin-styled-components": "^1.9.0", "babel-plugin-styled-components": "^1.10.0",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.0", "bundlesize": "^0.17.1",
"conventional-changelog-cli": "^2.0.11", "conventional-changelog-cli": "^2.0.12",
"copy-webpack-plugin": "^4.6.0", "copy-webpack-plugin": "^5.0.0",
"core-js": "^2.5.7", "core-js": "^2.6.5",
"coveralls": "^3.0.2", "coveralls": "^3.0.3",
"css-loader": "^1.0.1", "css-loader": "^2.1.1",
"cypress": "~3.1.2", "cypress": "~3.1.5",
"deploy-to-gh-pages": "^1.3.6", "deploy-to-gh-pages": "^1.3.6",
"enzyme": "^3.7.0", "enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.7.0", "enzyme-adapter-react-16": "^1.10.0",
"enzyme-to-json": "^3.3.4", "enzyme-to-json": "^3.3.5",
"fork-ts-checker-webpack-plugin": "0.5.0", "fork-ts-checker-webpack-plugin": "1.0.0",
"html-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^3.1.0",
"jest": "^23.6.0", "jest": "^24.3.1",
"license-checker": "^24.0.1", "license-checker": "^25.0.1",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"mobx": "^4.3.1", "mobx": "^4.3.1",
"prettier": "^1.15.2", "prettier": "^1.16.4",
"prettier-eslint": "^8.8.2", "prettier-eslint": "^8.8.2",
"puppeteer": "^1.10.0", "puppeteer": "^1.13.0",
"raf": "^3.4.1", "raf": "^3.4.1",
"react": "^16.6.3", "react": "^16.8.4",
"react-dom": "^16.6.3", "react-dom": "^16.8.4",
"rimraf": "^2.6.2", "rimraf": "^2.6.3",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"styled-components": "^4.1.1", "styled-components": "^4.1.3",
"swagger2openapi": "^3.2.14", "ts-jest": "24.0.0",
"ts-jest": "23.10.5", "ts-loader": "5.3.3",
"ts-loader": "5.3.1", "ts-node": "^8.0.3",
"ts-node": "^7.0.1", "tslint": "^5.13.1",
"tslint": "^5.11.0",
"tslint-react": "^3.4.0", "tslint-react": "^3.4.0",
"typescript": "^3.1.6", "typescript": "^3.3.3333",
"webpack": "^4.26.1", "unfetch": "^4.1.0",
"webpack-cli": "^3.1.2", "url-polyfill": "^1.1.5",
"webpack-dev-server": "^3.1.10", "webpack": "^4.29.6",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"webpack-node-externals": "^1.6.0", "webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.4", "workerize-loader": "^1.0.4",
"yaml-js": "^0.2.3" "yaml-js": "^0.2.3"
}, },
"peerDependencies": { "peerDependencies": {
"mobx": "^4.2.0 || ^5.0.0", "mobx": "^4.2.0 || ^5.0.0",
"react": "^16.2.0", "react": "^16.8.4",
"react-dom": "^16.2.0", "react-dom": "^16.8.4",
"styled-components": "^4.1.1" "styled-components": "^4.1.1"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^1.0.7", "dompurify": "^1.0.10",
"eventemitter3": "^3.0.0", "eventemitter3": "^3.0.0",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
"json-schema-ref-parser": "^6.0.1", "json-schema-ref-parser": "^6.1.0",
"lunr": "^2.3.2", "lunr": "2.3.6",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.6.0", "marked": "^0.6.1",
"memoize-one": "^4.0.0", "memoize-one": "^5.0.0",
"mobx-react": "^5.2.5", "mobx-react": "^5.4.3",
"openapi-sampler": "1.0.0-beta.14", "openapi-sampler": "1.0.0-beta.14",
"perfect-scrollbar": "^1.4.0", "perfect-scrollbar": "^1.4.0",
"polished": "^2.0.2", "polished": "^3.0.3",
"prismjs": "^1.15.0", "prismjs": "^1.15.0",
"prop-types": "^15.6.2", "prop-types": "^15.7.2",
"react-dropdown": "^1.6.2", "react-dropdown": "^1.6.4",
"react-hot-loader": "^4.3.5", "react-hot-loader": "^4.8.0",
"react-tabs": "^2.0.0", "react-tabs": "^3.0.0",
"slugify": "^1.3.1", "slugify": "^1.3.4",
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"swagger2openapi": "^5.2.3",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"bundlesize": [ "bundlesize": [
@ -161,7 +164,9 @@
} }
], ],
"jest": { "jest": {
"setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts", "setupFilesAfterEnv": [
"<rootDir>/src/setupTests.ts"
],
"preset": "ts-jest", "preset": "ts-jest",
"collectCoverageFrom": [ "collectCoverageFrom": [
"src/**/*.{ts,tsx}" "src/**/*.{ts,tsx}"

View File

@ -88,6 +88,8 @@ export const ExampleValue = styled(FieldLabel)`
${extensionsHook('ExampleValue')}; ${extensionsHook('ExampleValue')};
`; `;
export const ExtensionValue = styled(ExampleValue)``;
export const ConstraintItem = styled(FieldLabel)` export const ConstraintItem = styled(FieldLabel)`
border-radius: 2px; border-radius: 2px;
${({ theme }) => ` ${({ theme }) => `

View File

@ -7,14 +7,15 @@ const headerFontSize = {
}; };
export const headerCommonMixin = level => css` export const headerCommonMixin = level => css`
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-weight: ${({ theme }) => theme.typography.headings.fontWeight}; font-weight: ${({ theme }) => theme.typography.headings.fontWeight};
font-size: ${headerFontSize[level]}; font-size: ${headerFontSize[level]};
line-height: ${({ theme }) => theme.typography.headings.lineHeight};
`; `;
export const H1 = styled.h1` export const H1 = styled.h1`
${headerCommonMixin(1)}; ${headerCommonMixin(1)};
color: ${props => props.theme.colors.primary.main}; color: ${({ theme }) => theme.colors.primary.main};
${extensionsHook('H1')}; ${extensionsHook('H1')};
`; `;

View File

@ -17,6 +17,14 @@ export const Section = styled.div.attrs(props => ({
}))<{ underlined?: boolean }>` }))<{ underlined?: boolean }>`
padding: ${props => props.theme.spacing.sectionVertical}px 0; padding: ${props => props.theme.spacing.sectionVertical}px 0;
&:last-child {
min-height: calc(100vh + 1px);
}
& > &:last-child {
min-height: initial;
}
${media.lessThan('medium', true)` ${media.lessThan('medium', true)`
padding: 0; padding: 0;
`} `}

View File

@ -1,4 +1,5 @@
import styled from '../styled-components'; import styled from '../styled-components';
import { PrismDiv } from './PrismDiv';
export const SampleControls = styled.div` export const SampleControls = styled.div`
opacity: 0.4; opacity: 0.4;
@ -21,3 +22,12 @@ export const SampleControlsWrap = styled.div`
opacity: 1; opacity: 1;
} }
`; `;
export const StyledPre = styled(PrismDiv.withComponent('pre'))`
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
overflow-x: auto;
margin: 0;
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
`;

View File

@ -16,14 +16,15 @@ export const Tabs = styled(ReactTabs)`
padding: 5px 10px; padding: 5px 10px;
display: inline-block; display: inline-block;
background-color: ${({ theme }) => darken(0.05, theme.rightPanel.backgroundColor)}; background-color: ${({ theme }) => theme.codeSample.backgroundColor};
border-bottom: 1px solid rgba(0, 0, 0, 0.5); border-bottom: 1px solid rgba(0, 0, 0, 0.5);
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
outline: none; outline: none;
color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)}; color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)};
margin: 5px; margin: 0
border: 1px solid ${({ theme }) => darken(0.1, theme.rightPanel.backgroundColor)}; ${({ theme }) => `${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px`};
border: 1px solid ${({ theme }) => darken(0.05, theme.codeSample.backgroundColor)};
border-radius: 5px; border-radius: 5px;
min-width: 60px; min-width: 60px;
font-size: 0.9em; font-size: 0.9em;

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 // Use the english word logo if no alt text is provided
const altText = logoInfo.altText ? logoInfo.altText : 'logo'; const altText = logoInfo.altText ? logoInfo.altText : 'logo';
const logo = ( const logo = <LogoImgEl src={logoInfo.url} alt={altText} />;
<LogoImgEl return (
src={logoInfo.url} <LogoWrap style={{ backgroundColor: logoInfo.backgroundColor }}>
style={{ backgroundColor: logoInfo.backgroundColor }} {logoHref ? LinkWrap(logoHref)(logo) : logo}
alt={altText} </LogoWrap>
/>
); );
return <LogoWrap>{logoHref ? LinkWrap(logoHref)(logo) : logo}</LogoWrap>;
} }
} }

View File

@ -4,6 +4,7 @@ import styled from '../../styled-components';
export const LogoImgEl = styled.img` export const LogoImgEl = styled.img`
max-height: ${props => props.theme.logo.maxHeight}; max-height: ${props => props.theme.logo.maxHeight};
max-width: ${props => props.theme.logo.maxWidth}; max-width: ${props => props.theme.logo.maxWidth};
padding: ${props => props.theme.logo.gutter};
width: 100%; width: 100%;
display: block; display: block;
`; `;

View File

@ -7,7 +7,7 @@ export const OperationEndpointWrap = styled.div`
`; `;
export const ServerRelativeURL = styled.span` export const ServerRelativeURL = styled.span`
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${props => props.theme.typography.code.fontFamily};
margin-left: 10px; margin-left: 10px;
flex: 1; flex: 1;
overflow-x: hidden; overflow-x: hidden;

View File

@ -1,6 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { ExampleValue, FieldLabel } from '../../common-elements/fields'; import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels';
export interface EnumValuesProps { export interface EnumValuesProps {
values: string[]; values: string[];
type: string; type: string;
@ -16,10 +18,13 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
return ( return (
<div> <div>
<FieldLabel> <FieldLabel>
{type === 'array' ? 'Items' : ''} {values.length === 1 ? 'Value' : 'Enum'}: {type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel> </FieldLabel>
{values.map((value, idx) => ( {values.map((value, idx) => (
<ExampleValue key={idx}>{JSON.stringify(value)} </ExampleValue> <React.Fragment key={idx}>
<ExampleValue>{JSON.stringify(value)}</ExampleValue>{' '}
</React.Fragment>
))} ))}
</div> </div>
); );

View File

@ -1,4 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { ExtensionValue, FieldLabel } from '../../common-elements/fields';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
@ -6,14 +9,9 @@ import { OptionsContext } from '../OptionsProvider';
import { StyledMarkdownBlock } from '../Markdown/styled.elements'; import { StyledMarkdownBlock } from '../Markdown/styled.elements';
const Extension = styled(StyledMarkdownBlock)` const Extension = styled(StyledMarkdownBlock)`
opacity: 0.9;
margin: 2px 0; margin: 2px 0;
`; `;
const ExtensionLable = styled.span`
font-style: italic;
`;
export interface ExtensionsProps { export interface ExtensionsProps {
extensions: { extensions: {
[k: string]: any; [k: string]: any;
@ -22,15 +20,18 @@ export interface ExtensionsProps {
export class Extensions extends React.PureComponent<ExtensionsProps> { export class Extensions extends React.PureComponent<ExtensionsProps> {
render() { render() {
const exts = this.props.extensions;
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {options => (
<> <>
{options.showExtensions && {options.showExtensions &&
Object.keys(this.props.extensions).map(key => ( Object.keys(exts).map(key => (
<Extension key={key}> <Extension key={key}>
<ExtensionLable>{key}</ExtensionLable>:{' '} <FieldLabel> {key.substring(2)}: </FieldLabel>{' '}
<code>{JSON.stringify(this.props.extensions[key])}</code> <ExtensionValue>
{typeof exts[key] === 'string' ? exts[key] : JSON.stringify(exts[key])}
</ExtensionValue>
</Extension> </Extension>
))} ))}
</> </>

View File

@ -14,7 +14,9 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
return ( return (
<div> <div>
<FieldLabel> {this.props.label} </FieldLabel>{' '} <FieldLabel> {this.props.label} </FieldLabel>{' '}
<ExampleValue> {JSON.stringify(this.props.value)} </ExampleValue> <ExampleValue>
{JSON.stringify(this.props.value)}
</ExampleValue>
</div> </div>
); );
} }

View File

@ -19,6 +19,8 @@ import { FieldDetail } from './FieldDetail';
import { Badge } from '../../common-elements/'; import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
export class FieldDetails extends React.PureComponent<FieldProps> { export class FieldDetails extends React.PureComponent<FieldProps> {
render() { render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props; const { showExamples, field, renderDiscriminatorSwitch } = this.props;
@ -40,18 +42,18 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
)} )}
{schema.title && <TypeTitle> ({schema.title}) </TypeTitle>} {schema.title && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} /> <ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> Nullable </NullableLabel>} {schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
{schema.pattern && <PatternLabel>{schema.pattern}</PatternLabel>} {schema.pattern && <PatternLabel>{schema.pattern}</PatternLabel>}
{schema.isCircular && <RecursiveLabel> Recursive </RecursiveLabel>} {schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
</div> </div>
{deprecated && ( {deprecated && (
<div> <div>
<Badge type="warning"> Deprecated </Badge> <Badge type="warning"> {l('deprecated')} </Badge>
</div> </div>
)} )}
<FieldDetail label={'Default:'} value={schema.default} /> <FieldDetail label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '} {!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{showExamples && <FieldDetail label={'Example:'} value={example} />} {showExamples && <FieldDetail label={l('example') + ':'} value={example} />}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />} {<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div> <div>
<Markdown compact={true} source={description} /> <Markdown compact={true} source={description} />

View File

@ -4,11 +4,12 @@ import { ParametersGroup } from './ParametersGroup';
import { UnderlinedHeader } from '../../common-elements'; import { UnderlinedHeader } from '../../common-elements';
import { MediaContentModel } from '../../services';
import { FieldModel, RequestBodyModel } from '../../services/models'; import { FieldModel, RequestBodyModel } from '../../services/models';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch'; import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { Schema } from '../Schema'; import { Schema } from '../Schema';
import { MediaContentModel } from '../../services'; import { Markdown } from '../Markdown/Markdown';
function safePush(obj, prop, item) { function safePush(obj, prop, item) {
if (!obj[prop]) { if (!obj[prop]) {
@ -45,13 +46,15 @@ export class Parameters extends React.PureComponent<ParametersProps> {
const bodyContent = body && body.content; const bodyContent = body && body.content;
const bodyDescription = body && body.description;
return ( return (
<div> <>
{paramsPlaces.map(place => ( {paramsPlaces.map(place => (
<ParametersGroup key={place} place={place} parameters={paramsMap[place]} /> <ParametersGroup key={place} place={place} parameters={paramsMap[place]} />
))} ))}
{bodyContent && <BodyContent content={bodyContent} />} {bodyContent && <BodyContent content={bodyContent} description={bodyDescription} />}
</div> </>
); );
} }
} }
@ -64,12 +67,17 @@ function DropdownWithinHeader(props) {
); );
} }
function BodyContent(props: { content: MediaContentModel }): JSX.Element { function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element {
const { content } = props; const { content, description } = props;
return ( return (
<MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}> <MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}>
{({ schema }) => { {({ schema }) => {
return <Schema skipReadOnly={true} key="schema" schema={schema} />; return (
<>
{description !== undefined && <Markdown source={description} />}
<Schema skipReadOnly={true} key="schema" schema={schema} />
</>
);
}} }}
</MediaTypesSwitch> </MediaTypesSwitch>
); );

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import { StyledPre } from '../../common-elements/samples';
import { ExampleModel } from '../../services/models';
import { ExampleValue } from './ExampleValue';
import { useExternalExample } from './exernalExampleHook';
export interface ExampleProps {
example: ExampleModel;
mimeType: string;
}
export function Example({ example, mimeType }: ExampleProps) {
if (example.value === undefined && example.externalValueUrl) {
return <ExternalExample example={example} mimeType={mimeType} />;
} else {
return <ExampleValue value={example.value} mimeType={mimeType} />;
}
}
export function ExternalExample({ example, mimeType }: ExampleProps) {
const value = useExternalExample(example, mimeType);
if (value === undefined) {
return <span>Loading...</span>;
}
if (value instanceof Error) {
return (
<StyledPre>
Error loading external example: <br />
<a className={'token string'} href={example.externalValueUrl} target="_blank">
{example.externalValueUrl}
</a>
</StyledPre>
);
}
return <ExampleValue value={value} mimeType={mimeType} />;
}

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import { isJsonLike, langFromMime } from '../../utils/openapi';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
export interface ExampleValueProps {
value: any;
mimeType: string;
}
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

@ -2,11 +2,9 @@ import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements'; import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
import { MediaTypeModel } from '../../services/models'; import { MediaTypeModel } from '../../services/models';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { NoSampleLabel } from './styled.elements';
import { isJsonLike, langFromMime } from '../../utils'; import { Example } from './Example';
import { NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps { export interface PayloadSamplesProps {
mediaType: MediaTypeModel; mediaType: MediaTypeModel;
@ -18,13 +16,6 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
const mimeType = this.props.mediaType.name; const mimeType = this.props.mediaType.name;
const noSample = <NoSampleLabel>No sample</NoSampleLabel>; const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
const sampleView = isJsonLike(mimeType)
? sample => <JsonViewer data={sample} />
: sample =>
(sample !== undefined && (
<SourceCodeWithCopy lang={langFromMime(mimeType)} source={sample} />
)) ||
noSample;
const examplesNames = Object.keys(examples); const examplesNames = Object.keys(examples);
if (examplesNames.length === 0) { if (examplesNames.length === 0) {
@ -39,13 +30,19 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
))} ))}
</TabList> </TabList>
{examplesNames.map(name => ( {examplesNames.map(name => (
<TabPanel key={name}>{sampleView(examples[name].value)}</TabPanel> <TabPanel key={name}>
<Example example={examples[name]} mimeType={mimeType} />
</TabPanel>
))} ))}
</SmallTabs> </SmallTabs>
); );
} else { } else {
const name = examplesNames[0]; const name = examplesNames[0];
return <div>{sampleView(examples[name].value)}</div>; return (
<div>
<Example example={examples[name]} mimeType={mimeType} />
</div>
);
} }
} }
} }

View File

@ -0,0 +1,34 @@
import { useEffect, useRef, useState } from 'react';
import { ExampleModel } from '../../services/models/Example';
export function useExternalExample(example: ExampleModel, mimeType: string) {
const [, setIsLoading] = useState(true); // to trigger component reload
const value = useRef<any>(undefined);
const prevRef = useRef<ExampleModel | undefined>(undefined);
if (prevRef.current !== example) {
value.current = undefined;
}
prevRef.current = example;
useEffect(
() => {
const load = async () => {
setIsLoading(true);
try {
value.current = await example.getExternalValue(mimeType);
} catch (e) {
value.current = e;
}
setIsLoading(false);
};
load();
},
[example, mimeType],
);
return value.current;
}

View File

@ -1,10 +1,11 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { OperationModel } from '../../services/models'; import { OperationModel, RedocNormalizedOptions } from '../../services';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples'; import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode'; import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements'; import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements';
import { OptionsContext } from '../OptionsProvider';
export interface RequestSamplesProps { export interface RequestSamplesProps {
operation: OperationModel; operation: OperationModel;
@ -12,6 +13,8 @@ export interface RequestSamplesProps {
@observer @observer
export class RequestSamples extends React.Component<RequestSamplesProps> { export class RequestSamples extends React.Component<RequestSamplesProps> {
static contextType = OptionsContext;
context: RedocNormalizedOptions;
operation: OperationModel; operation: OperationModel;
render() { render() {
@ -21,16 +24,20 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
const samples = operation.codeSamples; const samples = operation.codeSamples;
const hasSamples = hasBodySample || samples.length > 0; const hasSamples = hasBodySample || samples.length > 0;
const hideTabList =
samples.length + (hasBodySample ? 1 : 0) === 1
? this.context.hideSingleRequestSampleTab
: false;
return ( return (
(hasSamples && ( (hasSamples && (
<div> <div>
<RightPanelHeader> Request samples </RightPanelHeader> <RightPanelHeader> Request samples </RightPanelHeader>
<Tabs defaultIndex={0}> <Tabs defaultIndex={0}>
<TabList> <TabList hidden={hideTabList}>
{hasBodySample && <Tab key="payload"> Payload </Tab>} {hasBodySample && <Tab key="payload"> Payload </Tab>}
{samples.map(sample => ( {samples.map(sample => (
<Tab key={sample.lang}> <Tab key={sample.lang + '_' + (sample.label || '')}>
{sample.label !== undefined ? sample.label : sample.lang} {sample.label !== undefined ? sample.label : sample.lang}
</Tab> </Tab>
))} ))}

View File

@ -10,6 +10,8 @@ import { ArraySchema } from './ArraySchema';
import { ObjectSchema } from './ObjectSchema'; import { ObjectSchema } from './ObjectSchema';
import { OneOfSchema } from './OneOfSchema'; import { OneOfSchema } from './OneOfSchema';
import { l } from '../../services/Labels';
export interface SchemaOptions { export interface SchemaOptions {
showTitle?: boolean; showTitle?: boolean;
skipReadOnly?: boolean; skipReadOnly?: boolean;
@ -34,7 +36,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
<div> <div>
<TypeName>{schema.displayType}</TypeName> <TypeName>{schema.displayType}</TypeName>
{schema.title && <TypeTitle> {schema.title} </TypeTitle>} {schema.title && <TypeTitle> {schema.title} </TypeTitle>}
<RecursiveLabel> Recursive </RecursiveLabel> <RecursiveLabel> {l('recursive')} </RecursiveLabel>
</div> </div>
); );
} }

View File

@ -4,6 +4,7 @@ import { SecuritySchemesModel } from '../../services/models';
import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { OpenAPISecurityScheme } from '../../types'; import { OpenAPISecurityScheme } from '../../types';
import { titleize } from '../../utils/helpers';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { StyledMarkdownBlock } from '../Markdown/styled.elements'; import { StyledMarkdownBlock } from '../Markdown/styled.elements';
@ -84,7 +85,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
</tr> </tr>
{scheme.apiKey ? ( {scheme.apiKey ? (
<tr> <tr>
<th> {scheme.apiKey.in} parameter name:</th> <th> {titleize(scheme.apiKey.in || '')} parameter name:</th>
<td> {scheme.apiKey.name} </td> <td> {scheme.apiKey.name} </td>
</tr> </tr>
) : scheme.http ? ( ) : scheme.http ? (
@ -93,13 +94,12 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
<th> HTTP Authorization Scheme </th> <th> HTTP Authorization Scheme </th>
<td> {scheme.http.scheme} </td> <td> {scheme.http.scheme} </td>
</tr>, </tr>,
scheme.http.scheme === 'bearer' && scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && (
scheme.http.bearerFormat && ( <tr key="bearer">
<tr key="bearer"> <th> Bearer format </th>
<th> Bearer format </th> <td> "{scheme.http.bearerFormat}" </td>
<td> "{scheme.http.bearerFormat}" </td> </tr>
</tr> ),
),
] ]
) : scheme.openId ? ( ) : scheme.openId ? (
<tr> <tr>

View File

@ -1,19 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { highlight } from '../../utils'; import { highlight } from '../../utils';
import { SampleControls, SampleControlsWrap } from '../../common-elements'; import { SampleControls, SampleControlsWrap, StyledPre } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper'; import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv';
import styled from '../../styled-components';
const StyledPre = styled(PrismDiv.withComponent('pre'))`
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
overflow-x: auto;
margin: 0;
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
`;
export interface SourceCodeProps { export interface SourceCodeProps {
source: string; source: string;

View File

@ -8,7 +8,6 @@ export * from './Schema/';
export * from './SearchBox/SearchBox'; export * from './SearchBox/SearchBox';
export * from './Operation/Operation'; export * from './Operation/Operation';
export * from './Loading/Loading'; export * from './Loading/Loading';
export * from './RedocStandalone';
export * from './JsonViewer'; export * from './JsonViewer';
export * from './Markdown/Markdown'; export * from './Markdown/Markdown';
export { StyledMarkdownBlock } from './Markdown/styled.elements'; export { StyledMarkdownBlock } from './Markdown/styled.elements';

View File

@ -5,7 +5,9 @@ import defaultTheme from '../theme';
export default class TestThemeProvider extends React.Component { export default class TestThemeProvider extends React.Component {
render() { render() {
return ( return (
<ThemeProvider theme={defaultTheme}>{React.Children.only(this.props.children)}</ThemeProvider> <ThemeProvider theme={defaultTheme}>
{React.Children.only(this.props.children as any)}
</ThemeProvider>
); );
} }
} }

View File

@ -6,3 +6,6 @@ import 'core-js/fn/string/starts-with';
import 'core-js/es6/map'; import 'core-js/es6/map';
import 'core-js/es6/symbol'; import 'core-js/es6/symbol';
import 'unfetch/polyfill/index';
import 'url-polyfill';

View File

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

@ -43,7 +43,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = []; const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec); const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options)); items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
if (spec['x-tagGroups']) { if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push( items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options), ...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
); );

View File

@ -187,6 +187,7 @@ export class OpenAPIParser {
...schema, ...schema,
allOf: undefined, allOf: undefined,
parentRefs: [], parentRefs: [],
title: schema.title || (isNamedDefinition($ref) ? JsonPointer.baseName($ref) : undefined),
}; };
// avoid mutating inner objects // avoid mutating inner objects
@ -263,11 +264,6 @@ export class OpenAPIParser {
} }
} }
// name of definition or title on top level
if (schema.title === undefined && isNamedDefinition($ref)) {
receiver.title = JsonPointer.baseName($ref);
}
return receiver; return receiver;
} }

View File

@ -2,6 +2,7 @@ import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } fr
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { isNumeric, mergeObjects } from '../utils/helpers'; import { isNumeric, mergeObjects } from '../utils/helpers';
import { LabelsConfigRaw, setRedocLabels } from './Labels';
import { MDXComponentMeta } from './MarkdownRenderer'; import { MDXComponentMeta } from './MarkdownRenderer';
export interface RedocRawOptions { export interface RedocRawOptions {
@ -21,10 +22,13 @@ export interface RedocRawOptions {
onlyRequiredInSamples?: boolean | string; onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
parentElementSelector?: string; parentElementSelector?: string;
hideSingleRequestSampleTab?: boolean | string;
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>; allowedMdComponents?: Dict<MDXComponentMeta>;
labels?: LabelsConfigRaw;
} }
function argValueToBoolean(val?: string | boolean): boolean { function argValueToBoolean(val?: string | boolean): boolean {
@ -137,7 +141,11 @@ export class RedocNormalizedOptions {
disableSearch: boolean; disableSearch: boolean;
onlyRequiredInSamples: boolean; onlyRequiredInSamples: boolean;
showExtensions: boolean | string[]; showExtensions: boolean | string[];
<<<<<<< HEAD
parentElement: Element | null; parentElement: Element | null;
=======
hideSingleRequestSampleTab: boolean;
>>>>>>> f29a4fe2eee39f7ef018c125d460ee8898856ce4
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
@ -152,6 +160,9 @@ export class RedocNormalizedOptions {
this.theme.extensionsHook = hook as any; 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.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);
@ -165,7 +176,11 @@ export class RedocNormalizedOptions {
this.disableSearch = argValueToBoolean(raw.disableSearch); this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
<<<<<<< HEAD
this.parentElement = RedocNormalizedOptions.normalizeParentElementSelector(raw.parentElementSelector); this.parentElement = RedocNormalizedOptions.normalizeParentElementSelector(raw.parentElementSelector);
=======
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
>>>>>>> f29a4fe2eee39f7ef018c125d460ee8898856ce4
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);

View File

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

View File

@ -37,7 +37,7 @@ describe('Models', () => {
parser = new OpenAPIParser(spec, undefined, opts); parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.WithArray, '', opts); const schema = new SchemaModel(parser, spec.components.schemas.WithArray, '', opts);
expect(schema.oneOf).toHaveLength(2); 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

@ -1,14 +1,64 @@
import { OpenAPIExample, Referenced } from '../../types'; import { resolve as urlResolve } from 'url';
import { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types';
import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
const externalExamplesCache: { [url: string]: Promise<any> } = {};
export class ExampleModel { export class ExampleModel {
value: any; value: any;
summary?: string; summary?: string;
description?: string; description?: string;
externalValue?: string; externalValueUrl?: string;
constructor(parser: OpenAPIParser, infoOrRef: Referenced<OpenAPIExample>) { constructor(
Object.assign(this, parser.deref(infoOrRef)); parser: OpenAPIParser,
infoOrRef: Referenced<OpenAPIExample>,
public mime: string,
encoding?: { [field: string]: OpenAPIEncoding },
) {
const example = parser.deref(infoOrRef);
this.value = example.value;
this.summary = example.summary;
this.description = example.description;
if (example.externalValue) {
this.externalValueUrl = urlResolve(parser.specUrl || '', example.externalValue);
}
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);
if (isFormUrlEncoded(mime) && this.value && typeof this.value === 'object') {
this.value = urlFormEncodePayload(this.value, encoding);
}
}
getExternalValue(mimeType: string): Promise<any> {
if (!this.externalValueUrl) {
return Promise.resolve(undefined);
}
if (externalExamplesCache[this.externalValueUrl]) {
return externalExamplesCache[this.externalValueUrl];
}
externalExamplesCache[this.externalValueUrl] = fetch(this.externalValueUrl).then(res => {
return res.text().then(txt => {
if (!res.ok) {
return Promise.reject(new Error(txt));
}
if (isJsonLike(mimeType)) {
try {
return JSON.parse(txt);
} catch (e) {
return txt;
}
} else {
return txt;
}
});
});
return externalExamplesCache[this.externalValueUrl];
} }
} }

View File

@ -1,6 +1,6 @@
import * as Sampler from 'openapi-sampler'; import * as Sampler from 'openapi-sampler';
import { OpenAPIExample, OpenAPIMediaType } from '../../types'; import { OpenAPIMediaType } from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { SchemaModel } from './Schema'; import { SchemaModel } from './Schema';
@ -9,7 +9,7 @@ import { OpenAPIParser } from '../OpenAPIParser';
import { ExampleModel } from './Example'; import { ExampleModel } from './Example';
export class MediaTypeModel { export class MediaTypeModel {
examples?: { [name: string]: OpenAPIExample }; examples?: { [name: string]: ExampleModel };
schema?: SchemaModel; schema?: SchemaModel;
name: string; name: string;
isRequestType: boolean; isRequestType: boolean;
@ -30,10 +30,18 @@ export class MediaTypeModel {
this.schema = info.schema && new SchemaModel(parser, info.schema, '', options); this.schema = info.schema && new SchemaModel(parser, info.schema, '', options);
this.onlyRequiredInSamples = options.onlyRequiredInSamples; this.onlyRequiredInSamples = options.onlyRequiredInSamples;
if (info.examples !== undefined) { if (info.examples !== undefined) {
this.examples = mapValues(info.examples, example => new ExampleModel(parser, example)); this.examples = mapValues(
info.examples,
example => new ExampleModel(parser, example, name, info.encoding),
);
} else if (info.example !== undefined) { } else if (info.example !== undefined) {
this.examples = { this.examples = {
default: new ExampleModel(parser, { value: info.example }), default: new ExampleModel(
parser,
{ value: parser.shalowDeref(info.example) },
name,
info.encoding,
),
}; };
} else if (isJsonLike(name)) { } else if (isJsonLike(name)) {
this.generateExample(parser, info); this.generateExample(parser, info);
@ -49,29 +57,31 @@ export class MediaTypeModel {
if (this.schema && this.schema.oneOf) { if (this.schema && this.schema.oneOf) {
this.examples = {}; this.examples = {};
for (const subSchema of this.schema.oneOf) { for (const subSchema of this.schema.oneOf) {
const sample = Sampler.sample( const sample = Sampler.sample(subSchema.rawSchema, samplerOptions, parser.spec);
subSchema.rawSchema,
samplerOptions,
parser.spec,
);
if (this.schema.discriminatorProp && typeof sample === 'object' && sample) { if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
sample[this.schema.discriminatorProp] = subSchema.title; sample[this.schema.discriminatorProp] = subSchema.title;
} }
this.examples[subSchema.title] = { this.examples[subSchema.title] = new ExampleModel(
value: sample, parser,
}; {
value: sample,
},
this.name,
info.encoding,
);
} }
} else if (this.schema) { } else if (this.schema) {
this.examples = { this.examples = {
default: new ExampleModel(parser, { default: new ExampleModel(
value: Sampler.sample( parser,
info.schema, {
samplerOptions, value: Sampler.sample(info.schema, samplerOptions, parser.spec),
parser.spec, },
), this.name,
}), info.encoding,
),
}; };
} }
} }

View File

@ -14,10 +14,13 @@ import {
isNamedDefinition, isNamedDefinition,
isPrimitiveType, isPrimitiveType,
JsonPointer, JsonPointer,
pluralizeType,
sortByField, sortByField,
sortByRequired, sortByRequired,
} from '../../utils/'; } from '../../utils/';
import { l } from '../Labels';
// TODO: refactor this model, maybe use getters instead of copying all the values // TODO: refactor this model, maybe use getters instead of copying all the values
export class SchemaModel { export class SchemaModel {
pointer: string; pointer: string;
@ -145,9 +148,9 @@ export class SchemaModel {
this.fields = buildFields(parser, schema, this.pointer, this.options); this.fields = buildFields(parser, schema, this.pointer, this.options);
} else if (this.type === 'array' && schema.items) { } else if (this.type === 'array' && schema.items) {
this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options); 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.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.title = this.title || this.items.title;
this.isPrimitive = this.items.isPrimitive; this.isPrimitive = this.items.isPrimitive;
if (this.example === undefined && this.items.example !== undefined) { if (this.example === undefined && this.items.example !== undefined) {
@ -161,7 +164,15 @@ export class SchemaModel {
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) { private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map((variant, idx) => { this.oneOf = oneOf!.map((variant, idx) => {
const merged = parser.mergeAllOf(variant, this.pointer + '/oneOf/' + idx); const derefVariant = parser.deref(variant);
const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx);
// try to infer title
const title =
isNamedDefinition(variant.$ref) && !merged.title
? JsonPointer.baseName(variant.$ref)
: merged.title;
const schema = new SchemaModel( const schema = new SchemaModel(
parser, parser,
@ -169,12 +180,14 @@ export class SchemaModel {
{ {
// variant may already have allOf so merge it to not get overwritten // variant may already have allOf so merge it to not get overwritten
...merged, ...merged,
title,
allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }], allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema, } as OpenAPISchema,
this.pointer + '/oneOf/' + idx, this.pointer + '/oneOf/' + idx,
this.options, this.options,
); );
parser.exitRef(variant);
// each oneOf should be independent so exiting all the parent refs // each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection // otherwise it will cause false-positive recursive detection
parser.exitParents(merged); parser.exitParents(merged);
@ -210,7 +223,7 @@ export class SchemaModel {
if (variant.$ref === undefined) { if (variant.$ref === undefined) {
continue; continue;
} }
const name = JsonPointer.dirName(variant.$ref); const name = JsonPointer.baseName(variant.$ref);
derived[variant.$ref] = name; derived[variant.$ref] = name;
} }
} }

View File

@ -105,6 +105,7 @@ const defaultTheme: ThemeInterface = {
headings: { headings: {
fontFamily: 'Montserrat, sans-serif', fontFamily: 'Montserrat, sans-serif',
fontWeight: '400', fontWeight: '400',
lineHeight: '1.6em',
}, },
code: { code: {
fontSize: '13px', fontSize: '13px',
@ -139,6 +140,7 @@ const defaultTheme: ThemeInterface = {
logo: { logo: {
maxHeight: ({ menu }) => menu.width, maxHeight: ({ menu }) => menu.width,
maxWidth: ({ menu }) => menu.width, maxWidth: ({ menu }) => menu.width,
gutter: '2px',
}, },
rightPanel: { rightPanel: {
backgroundColor: '#263238', backgroundColor: '#263238',
@ -166,7 +168,7 @@ export function resolveTheme(theme: ThemeInterface): ResolvedThemeInterface {
counter++; counter++;
if (counter > 1000) { if (counter > 1000) {
throw new Error( throw new Error(
`Theme probably contains cirucal dependency at ${currentPath}: ${val.toString()}`, `Theme probably contains circular dependency at ${currentPath}: ${val.toString()}`,
); );
} }
@ -281,6 +283,7 @@ export interface ResolvedThemeInterface {
headings: { headings: {
fontFamily: string; fontFamily: string;
fontWeight: string; fontWeight: string;
lineHeight: string;
}; };
links: { links: {
@ -307,6 +310,7 @@ export interface ResolvedThemeInterface {
logo: { logo: {
maxHeight: string; maxHeight: string;
maxWidth: string; maxWidth: string;
gutter: string;
}; };
rightPanel: { rightPanel: {
backgroundColor: string; backgroundColor: string;

View File

@ -1,5 +1,5 @@
import slugify from 'slugify'; import slugify from 'slugify';
import { mapWithLast, appendToMdHeading, mergeObjects, safeSlugify } from '../helpers'; import { appendToMdHeading, mapWithLast, mergeObjects, safeSlugify, titleize } from '../helpers';
describe('Utils', () => { describe('Utils', () => {
describe('helpers', () => { describe('helpers', () => {
@ -68,5 +68,11 @@ describe('Utils', () => {
expect(mergeObjects({}, obj1, obj2)).toEqual({ a: ['C'], b: ['D'] }); 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,6 +7,7 @@ import {
isPrimitiveType, isPrimitiveType,
mergeParams, mergeParams,
normalizeServers, normalizeServers,
pluralizeType,
} from '../'; } from '../';
import { OpenAPIParser } from '../../services'; import { OpenAPIParser } from '../../services';
@ -353,4 +354,27 @@ describe('Utils', () => {
expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty'); 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>',
);
});
});
}); });

View File

@ -147,7 +147,7 @@ export function resolveUrl(url: string, to: string) {
let res; let res;
if (to.startsWith('//')) { if (to.startsWith('//')) {
const { protocol: specProtocol } = parse(url); const { protocol: specProtocol } = parse(url);
res = `${specProtocol}${to}`; res = `${specProtocol || 'https:'}${to}`;
} else if (isAbsoluteUrl(to)) { } else if (isAbsoluteUrl(to)) {
res = to; res = to;
} else if (!to.startsWith('/')) { } else if (!to.startsWith('/')) {
@ -163,5 +163,34 @@ export function resolveUrl(url: string, to: string) {
} }
export function getBasePath(serverUrl: string): 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);
}
} }

View File

@ -21,6 +21,10 @@ function htmlEncode(t) {
: ''; : '';
} }
function escapeForStringLiteral(str: string) {
return str.replace(/([\\"])/g, '\\$1');
}
function decorateWithSpan(value, className) { function decorateWithSpan(value, className) {
return '<span class="' + className + '">' + htmlEncode(value) + '</span>'; return '<span class="' + className + '">' + htmlEncode(value) + '</span>';
} }
@ -53,11 +57,11 @@ function valueToHTML(value) {
'<a href="' + '<a href="' +
value + value +
'">' + '">' +
htmlEncode(value) + htmlEncode(escapeForStringLiteral(value)) +
'</a>' + '</a>' +
decorateWithSpan('"', 'token string'); decorateWithSpan('"', 'token string');
} else { } else {
output += decorateWithSpan('"' + value + '"', 'token string'); output += decorateWithSpan('"' + escapeForStringLiteral(value) + '"', 'token string');
} }
} else if (valueType === 'boolean') { } else if (valueType === 'boolean') {
output += decorateWithSpan(value, 'token boolean'); output += decorateWithSpan(value, 'token boolean');

View File

@ -19,7 +19,7 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi
export function convertSwagger2OpenAPI(spec: any): Promise<OpenAPISpec> { export function convertSwagger2OpenAPI(spec: any): Promise<OpenAPISpec> {
console.warn('[ReDoc Compatibility mode]: Converting OpenAPI 2.0 to OpenAPI 3.0'); console.warn('[ReDoc Compatibility mode]: Converting OpenAPI 2.0 to OpenAPI 3.0');
return new Promise<OpenAPISpec>((resolve, reject) => 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 // TODO: log any warnings
if (err) { if (err) {
return reject(err); return reject(err);

View File

@ -2,6 +2,7 @@ import { dirname } from 'path';
import { OpenAPIParser } from '../services/OpenAPIParser'; import { OpenAPIParser } from '../services/OpenAPIParser';
import { import {
OpenAPIEncoding,
OpenAPIMediaType, OpenAPIMediaType,
OpenAPIOperation, OpenAPIOperation,
OpenAPIParameter, OpenAPIParameter,
@ -10,7 +11,7 @@ import {
Referenced, Referenced,
} from '../types'; } from '../types';
import { IS_BROWSER } from './dom'; 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 { function isWildcardStatusCode(statusCode: string | number): statusCode is string {
return typeof statusCode === 'string' && /\dxx/i.test(statusCode); return typeof statusCode === 'string' && /\dxx/i.test(statusCode);
@ -130,6 +131,101 @@ export function isJsonLike(contentType: string): boolean {
return contentType.search(/json/i) !== -1; return contentType.search(/json/i) !== -1;
} }
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);
} else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal)
.map(k => `${k}${delimeter}${fieldVal[k]}`)
.join(delimeter);
} else {
return fieldName + '=' + fieldVal.toString();
}
}
function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
if (Array.isArray(fieldVal)) {
console.warn('deepObject style cannot be used with array value:' + fieldVal.toString());
return '';
} else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal)
.map(k => `${fieldName}[${k}]=${fieldVal[k]}`)
.join('&');
} else {
console.warn('deepObject style cannot be used with non-object value:' + fieldVal.toString());
return '';
}
}
/*
* Should be used only for url-form-encoded body payloads
* To be used for parmaters should be extended with other style values
*/
export function urlFormEncodePayload(
payload: object,
encoding: { [field: string]: OpenAPIEncoding } = {},
) {
if (Array.isArray(payload)) {
throw new Error('Payload must have fields: ' + payload.toString());
} else {
return Object.keys(payload)
.map(fieldName => {
const fieldVal = payload[fieldName];
const { style = 'form', explode = true } = encoding[fieldName] || {};
switch (style) {
case 'form':
return formEncodeField(fieldVal, fieldName, explode);
break;
case 'spaceDelimited':
return delimitedEncodeField(fieldVal, fieldName, '%20');
case 'pipeDelimited':
return delimitedEncodeField(fieldVal, fieldName, '|');
case 'deepObject':
return deepObjectEncodeField(fieldVal, fieldName);
default:
// TODO implement rest of styles for path parameters
console.warn('Incorrect or unsupported encoding style: ' + style);
return '';
}
})
.join('&');
}
}
export function langFromMime(contentType: string): string { export function langFromMime(contentType: string): string {
if (contentType.search(/xml/i) !== -1) { if (contentType.search(/xml/i) !== -1) {
return 'xml'; return 'xml';
@ -271,13 +367,20 @@ export function normalizeServers(
specUrl: string | undefined, specUrl: string | undefined,
servers: OpenAPIServer[], servers: OpenAPIServer[],
): OpenAPIServer[] { ): OpenAPIServer[] {
const baseUrl = const getHref = () => {
specUrl === undefined ? (IS_BROWSER ? window.location.href : '') : dirname(specUrl); 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) { if (servers.length === 0) {
return [ return [
{ {
url: baseUrl, url: stripTrailingSlash(baseUrl),
}, },
]; ];
} }
@ -338,3 +441,10 @@ export function extractExtensions(obj: object, showExtensions: string[] | true):
return acc; return acc;
}, {}); }, {});
} }
export function pluralizeType(displayType: string): string {
return displayType
.split(' or ')
.map(type => type.replace(/^(string|object|number|integer|array|boolean)( ?.*)/, '$1s$2'))
.join(' or ');
}

View File

@ -18,4 +18,15 @@ declare module 'styled-components' {
...interpolations: SimpleInterpolation[] ...interpolations: SimpleInterpolation[]
): Keyframes; ): Keyframes;
} }
export interface BaseThemedCssFunction<T extends object> {
<P extends object>(
first:
| TemplateStringsArray
| CSSObject
| InterpolationFunction<ThemedStyledProps<P, T>>
| string[],
...interpolations: Array<Interpolation<ThemedStyledProps<P, T>>>
): FlattenInterpolation<ThemedStyledProps<P, T>>;
}
} }

View File

@ -65,13 +65,13 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
? { ? {
esprima: 'esprima', esprima: 'esprima',
'node-fetch': 'null', 'node-fetch': 'null',
'node-fetch-h2': 'null',
yaml: 'null',
'safe-json-stringify': 'null',
} }
: (context, request, callback) => { : (context, request, callback) => {
// ignore node-fetch dep of swagger2openapi as it is not used // ignore node-fetch dep of swagger2openapi as it is not used
if (/node-fetch$/i.test(request)) { if (/esprima|node-fetch|node-fetch-h2|yaml|safe-json-stringify$/i.test(request)) {
return callback(null, 'var undefined');
}
if (/esprima$/i.test(request)) {
return callback(null, 'var undefined'); return callback(null, 'var undefined');
} }
return nodeExternals(context, request, callback); return nodeExternals(context, request, callback);
@ -134,7 +134,6 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
loader: 'css-loader', loader: 'css-loader',
options: { options: {
sourceMap: false, sourceMap: false,
minimize: true,
}, },
}, },
}, },

4802
yarn.lock

File diff suppressed because it is too large Load Diff