diff --git a/.npmignore b/.npmignore index 7ffbfdf0..56abd88f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,6 @@ * !bundles/* -!typings/* +!typings/**/* !package.json !README.md !LICENSE \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a5ee3867..566b9d41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: addons: chrome: stable before_script: npm run bundle -script: npm test && npm run e2e-ci +script: npm test && [ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm run e2e after_script: cat ./coverage/lcov.info | coveralls before_deploy: npm run compile:cli && npm run declarations deploy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2572f8b8..0ca2df72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ + +# [2.0.0-alpha.24](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.23...v2.0.0-alpha.24) (2018-06-01) + + +### Bug Fixes + +* temporary downgrade marked as it introduced breaking changes and a few bugs ([902f97a](https://github.com/Rebilly/ReDoc/commit/902f97a)) + + + + +# [2.0.0-alpha.23](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.22...v2.0.0-alpha.23) (2018-05-31) + + +### Bug Fixes + +* **cli:** make positional arguments required and handle errors in serve and bundle manually ([#518](https://github.com/Rebilly/ReDoc/issues/518)) ([370d08a](https://github.com/Rebilly/ReDoc/commit/370d08a)) +* fix typings on npm ([d957ad7](https://github.com/Rebilly/ReDoc/commit/d957ad7)) +* fix vertical line misaligned in firefox ([bde08f1](https://github.com/Rebilly/ReDoc/commit/bde08f1)), closes [#503](https://github.com/Rebilly/ReDoc/issues/503) +* mergeAllOf takes items into account ([#511](https://github.com/Rebilly/ReDoc/issues/511)) ([47b2177](https://github.com/Rebilly/ReDoc/commit/47b2177)) + + + # [2.0.0-alpha.22](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.21...v2.0.0-alpha.22) (2018-05-29) diff --git a/README.md b/README.md index c5546e01..d5ff8e85 100644 --- a/README.md +++ b/README.md @@ -212,12 +212,13 @@ You can use all of the following options with standalone version on tag ## Advanced usage of standalone version Instead of adding `spec-url` attribute to the `` element you can initialize ReDoc via globally exposed `Redoc` object: ```js -Redoc.init(specOrSpecUrl, options, element) +Redoc.init(specOrSpecUrl, options, element, callback?) ``` - `specOrSpecUrl` is either JSON object with specification or an URL to the spec in `JSON` or `YAML` format - `options` [options object](#redoc-options-object) - `element` DOM element to put ReDoc into +- `callback` (optional) - callback to be called after Redoc has been fully rendered ```js Redoc.init('http://petstore.swagger.io/v2/swagger.json', { diff --git a/cli/index.ts b/cli/index.ts index 6281e5ca..bff4604e 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -32,7 +32,7 @@ const BUNDLES_DIR = dirname(require.resolve('redoc')); /* tslint:disable-next-line */ YargsParser.command( - 'serve [spec]', + 'serve ', 'start the server', yargs => { yargs.positional('spec', { @@ -60,16 +60,22 @@ YargsParser.command( return yargs; }, async argv => { - await serve(argv.port, argv.spec, { + const config = { ssr: argv.ssr, watch: argv.watch, templateFileName: argv.template, redocOptions: argv.options || {}, - }); + }; + + try { + await serve(argv.port, argv.spec, config); + } catch (e) { + handleError(e); + } }, ) .command( - 'bundle [spec]', + 'bundle ', 'bundle spec into zero-dependency HTML-file', yargs => { yargs.positional('spec', { @@ -99,16 +105,22 @@ YargsParser.command( return yargs; }, async argv => { - await bundle(argv.spec, { + const config = { ssr: true, output: argv.o, cdn: argv.cdn, title: argv.title, templateFileName: argv.template, redocOptions: argv.options || {}, - }); + }; + + try { + await bundle(argv.spec, config); + } catch (e) { + handleError(e); + } }, - ) +) .demandCommand() .options('t', { alias: 'template', @@ -117,10 +129,6 @@ YargsParser.command( }) .options('options', { describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars', - }) - .fail((message, error) => { - console.log(error.stack); - process.exit(1); }).argv; async function serve(port: number, pathToSpec: string, options: Options = {}) { @@ -229,13 +237,13 @@ async function getPageHTML( ssr ? 'hydrate(__redoc_state, container);' : `init("spec.json", ${JSON.stringify(redocOptions)}, container)` - }; + }; `, redocHead: ssr ? (cdn - ? '' - : ``) + css + ? '' + : ``) + css : '', title, }); @@ -296,3 +304,8 @@ function isURL(str: string): boolean { function escapeUnicode(str) { return str.replace(/\u2028|\u2029/g, m => '\\u202' + (m === '\u2028' ? '8' : '9')); } + +function handleError(error: Error) { + console.error(error.stack); + process.exit(1); +} diff --git a/custom.d.ts b/custom.d.ts index bbf43fb2..56b27d74 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -18,10 +18,6 @@ declare module '*.css' { declare var __REDOC_VERSION__: string; declare var __REDOC_REVISION__: string; -declare type Dict = { - [key: string]: T; -}; - interface Element { scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void; } diff --git a/demo/index.tsx b/demo/index.tsx index b4b182dd..4a1b69cb 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -11,7 +11,7 @@ const demos = [ value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml', label: 'Google Calendar', }, - { value: 'https://api.apis.guru/v2/specs/slack.com/1.0.3/swagger.yaml', label: 'Slack' }, + { value: 'https://api.apis.guru/v2/specs/slack.com/1.0.6/swagger.yaml', label: 'Slack' }, { value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' }, { value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml', diff --git a/docs/redoc-vendor-extensions.md b/docs/redoc-vendor-extensions.md index 4f012fd2..16909bba 100644 --- a/docs/redoc-vendor-extensions.md +++ b/docs/redoc-vendor-extensions.md @@ -200,6 +200,16 @@ Extends OpenAPI [Parameter Object](http://swagger.io/specification/#parameterObj ###### Usage in ReDoc `x-examples` are rendered in the JSON tab on the right panel of ReDoc. +### Response Object vendor extensions +Extneds OpeanAPI [Response Object](https://swagger.io/specification/#responseObject) + +#### x-summary +| Field Name | Type | Description | +| :------------- | :------: | :---------- | +| x-summary | string | a short summary of the response | + +###### Usage in ReDoc +If specified, `x-summary` is used as the response button text. Description is rendered under the button. ### Schema Object vendor extensions Extends OpenAPI [Schema Object](http://swagger.io/specification/#schemaObject) diff --git a/package.json b/package.json index 4eb83c6d..7bf96ebe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redoc", - "version": "2.0.0-alpha.22", + "version": "2.0.0-alpha.24", "description": "ReDoc", "repository": { "type": "git", @@ -51,10 +51,10 @@ "license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD-2-Clause;BSD-3-Clause' --summary" }, "devDependencies": { - "@babel/core": "^7.0.0-beta.40", - "@babel/plugin-syntax-decorators": "^7.0.0-beta.42", - "@babel/plugin-syntax-jsx": "^7.0.0-beta.42", - "@babel/plugin-syntax-typescript": "^7.0.0-beta.42", + "@babel/core": "7.0.0-beta.47", + "@babel/plugin-syntax-decorators": "7.0.0-beta.47", + "@babel/plugin-syntax-jsx": "7.0.0-beta.47", + "@babel/plugin-syntax-typescript": "7.0.0-beta.47", "@cypress/webpack-preprocessor": "2.0.1", "@types/dompurify": "^0.0.31", "@types/enzyme": "^3.1.8", @@ -138,7 +138,7 @@ "json-schema-ref-parser": "^5.0.0", "lunr": "^2.2.1", "mark.js": "^8.11.1", - "marked": "^0.4.0", + "marked": "0.3.18", "mobx": "^4.3.0", "mobx-react": "^5.0.0", "openapi-sampler": "1.0.0-beta.12", diff --git a/src/common-elements/fields-layout.ts b/src/common-elements/fields-layout.ts index f08efaba..14b5f1d7 100644 --- a/src/common-elements/fields-layout.ts +++ b/src/common-elements/fields-layout.ts @@ -117,7 +117,7 @@ export const InnerPropertiesWrap = styled.div` `; export const PropertiesTable = styled.table` - border-collapse: collapse; + border-collapse: separate; border-radius: 3px; font-size: ${props => props.theme.baseFont.size}; diff --git a/src/components/Fields/FieldDetails.tsx b/src/components/Fields/FieldDetails.tsx index e860772b..91201bef 100644 --- a/src/components/Fields/FieldDetails.tsx +++ b/src/components/Fields/FieldDetails.tsx @@ -28,12 +28,7 @@ export class FieldDetails extends React.PureComponent {
{schema.typePrefix} {schema.displayType} - {schema.format && ( - - {' <'} - {schema.format}> - - )} + {schema.format && <{schema.format}> } {schema.title && ({schema.title}) } {schema.nullable && Nullable } diff --git a/src/components/PayloadSamples/MediaTypeSamples.tsx b/src/components/PayloadSamples/MediaTypeSamples.tsx index 2b37e651..9101f2fa 100644 --- a/src/components/PayloadSamples/MediaTypeSamples.tsx +++ b/src/components/PayloadSamples/MediaTypeSamples.tsx @@ -21,9 +21,10 @@ export class MediaTypeSamples extends React.Component { const sampleView = isJsonLike(mimeType) ? sample => : sample => - (sample && ) || { - noSample, - }; + (sample !== undefined && ( + + )) || + noSample; const examplesNames = Object.keys(examples); if (examplesNames.length === 0) { diff --git a/src/components/Responses/Response.tsx b/src/components/Responses/Response.tsx index 6f521f4b..2404dce5 100644 --- a/src/components/Responses/Response.tsx +++ b/src/components/Responses/Response.tsx @@ -8,6 +8,7 @@ import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel'; import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch'; import { Schema } from '../Schema'; +import { Markdown } from '../Markdown/Markdown'; import { ResponseHeaders } from './ResponseHeaders'; import { ResponseDetailsWrap, StyledResponseTitle } from './styled.elements'; @@ -18,11 +19,11 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> { }; render() { - const { headers, type, description, code, expanded, content } = this.props.response; + const { headers, type, summary, description, code, expanded, content } = this.props.response; const mimes = content === undefined ? [] : content.mediaTypes.filter(mime => mime.schema !== undefined); - const empty = headers.length === 0 && mimes.length === 0; + const empty = headers.length === 0 && mimes.length === 0 && !description; return (
@@ -30,13 +31,14 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> { onClick={this.toggle} type={type} empty={empty} - title={description || ''} + title={summary || ''} code={code} opened={expanded} /> {expanded && !empty && ( + {description && } {({ schema }) => { diff --git a/src/components/SideMenu/SideMenu.tsx b/src/components/SideMenu/SideMenu.tsx index 169c54f3..9aab7310 100644 --- a/src/components/SideMenu/SideMenu.tsx +++ b/src/components/SideMenu/SideMenu.tsx @@ -6,6 +6,7 @@ import { IMenuItem, MenuStore } from '../../services/MenuStore'; import { MenuItems } from './MenuItems'; import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar'; +import { RedocAttribution } from './styled.elements'; @observer export class SideMenu extends React.Component<{ menu: MenuStore }> { @@ -29,6 +30,11 @@ export class SideMenu extends React.Component<{ menu: MenuStore }> { ) : ( + + + Documentation Powered by ReDoc + + ) } diff --git a/src/components/SideMenu/styled.elements.ts b/src/components/SideMenu/styled.elements.ts index ca8d7145..302dca3e 100644 --- a/src/components/SideMenu/styled.elements.ts +++ b/src/components/SideMenu/styled.elements.ts @@ -160,3 +160,21 @@ export const MenuItemTitle = withProps<{ width?: string }>(styled.span)` overflow: hidden; text-overflow: ellipsis; `; + +export const RedocAttribution = styled.div` + font-size: 0.8em; + margin-top: ${({ theme }) => `${theme.spacingUnit / 2}px`}; + padding: ${({ theme }) => `0 ${theme.spacingUnit}px`}; + text-align: left; + + opacity: 0.7; + + a, + a:visited, + a:hover { + color: ${({ theme }) => theme.colors.text} !important; + border-top: 1px solid #e1e1e1; + padding-top: 10px; + display: block; + } +`; diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index cabf56f5..8982bf3d 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -233,6 +233,15 @@ export class OpenAPIParser { } } + if (subSchema.items !== undefined) { + receiver.items = receiver.items || {}; + // merge inner properties + receiver.items = this.mergeAllOf( + { allOf: [receiver.items, subSchema.items] }, + $ref + '/items', + ); + } + if (subSchema.required !== undefined) { receiver.required = (receiver.required || []).concat(subSchema.required); } diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index f6fa04ee..9059d346 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -10,7 +10,9 @@ import { OpenAPIExternalDocumentation, OpenAPIServer } from '../../types'; import { getOperationSummary, + getStatusCodeType, isAbsolutePath, + isStatusCode, JsonPointer, mergeParams, sortByRequired, @@ -99,10 +101,15 @@ export class OperationModel implements IMenuItem { let hasSuccessResponses = false; this.responses = Object.keys(operationSpec.responses || []) .filter(code => { - if (parseInt(code, 10) >= 100 && parseInt(code, 10) <= 399) { + if (code === 'default') { + return true; + } + + if (getStatusCodeType(code) === 'success') { hasSuccessResponses = true; } - return isNumeric(code) || code === 'default'; + + return isStatusCode(code); }) // filter out other props (e.g. x-props) .map(code => { return new ResponseModel( diff --git a/src/services/models/Response.ts b/src/services/models/Response.ts index 3f8937cd..c18f4097 100644 --- a/src/services/models/Response.ts +++ b/src/services/models/Response.ts @@ -13,6 +13,7 @@ export class ResponseModel { content?: MediaContentModel; code: string; + summary: string; description: string; type: string; headers: FieldModel[] = []; @@ -32,7 +33,15 @@ export class ResponseModel { if (info.content !== undefined) { this.content = new MediaContentModel(parser, info.content, false, options); } - this.description = info.description || ''; + + if (info['x-summary'] !== undefined) { + this.summary = info['x-summary']; + this.description = info.description || ''; + } else { + this.summary = info.description || ''; + this.description = ''; + } + this.type = getStatusCodeType(code, defaultAsError); const headers = info.headers; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index c02a721c..8103c2ef 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,3 +1,8 @@ export * from './open-api'; export type Omit = Pick>; +declare global { + type Dict = { + [key: string]: T; + }; +} diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index e5ef8872..a5aeec9a 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -6,21 +6,35 @@ import { OpenAPISchema, Referenced, } from '../types'; +import { isNumeric } from './helpers'; -export function getStatusCodeType(statusCode: string | number, defaultAsError = false): string { +function isWildcardStatusCode(statusCode: string | number): statusCode is string { + return typeof statusCode === 'string' && /\dxx/i.test(statusCode); +} + +export function isStatusCode(statusCode: string) { + return statusCode === 'default' || isNumeric(statusCode) || isWildcardStatusCode(statusCode); +} + +export function getStatusCodeType(statusCode: string, defaultAsError = false): string { if (statusCode === 'default') { return defaultAsError ? 'error' : 'success'; } - if (statusCode < 100 || statusCode > 599) { + let code = parseInt(statusCode, 10); + if (isWildcardStatusCode(statusCode)) { + code *= 100; // parseInt('2xx') parses to 2 + } + + if (code < 100 || code > 599) { throw new Error('invalid HTTP code'); } let res = 'success'; - if (statusCode >= 300 && statusCode < 400) { + if (code >= 300 && code < 400) { res = 'redirect'; - } else if (statusCode >= 400) { + } else if (code >= 400) { res = 'error'; - } else if (statusCode < 200) { + } else if (code < 200) { res = 'info'; } return res; diff --git a/yarn.lock b/yarn.lock index b6666acf..5288d77e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,7 +14,7 @@ dependencies: "@babel/highlight" "7.0.0-beta.46" -"@babel/core@^7.0.0-beta.40": +"@babel/core@7.0.0-beta.47": version "7.0.0-beta.47" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.0.0-beta.47.tgz#b9c164fb9a1e1083f067c236a9da1d7a7d759271" dependencies: @@ -98,19 +98,19 @@ esutils "^2.0.2" js-tokens "^3.0.0" -"@babel/plugin-syntax-decorators@^7.0.0-beta.42": +"@babel/plugin-syntax-decorators@7.0.0-beta.47": version "7.0.0-beta.47" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.0.0-beta.47.tgz#a42f10fcd651940bc475d93b3ac23432b4a8a293" dependencies: "@babel/helper-plugin-utils" "7.0.0-beta.47" -"@babel/plugin-syntax-jsx@^7.0.0-beta.42": +"@babel/plugin-syntax-jsx@7.0.0-beta.47": version "7.0.0-beta.47" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.0.0-beta.47.tgz#f3849d94288695d724bd205b4f6c3c99e4ec24a4" dependencies: "@babel/helper-plugin-utils" "7.0.0-beta.47" -"@babel/plugin-syntax-typescript@^7.0.0-beta.42": +"@babel/plugin-syntax-typescript@7.0.0-beta.47": version "7.0.0-beta.47" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.0.0-beta.47.tgz#108d4c83ff48ddcb8f0532252a9892e805ddc64c" dependencies: @@ -6542,9 +6542,9 @@ mark.js@^8.11.1: version "8.11.1" resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" -marked@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66" +marked@0.3.18: + version "0.3.18" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.18.tgz#3ef058cd926101849b92a7a7c15db18c7fc76b2f" math-expression-evaluator@^1.2.14: version "1.2.17"