feat: Add download file option

Allows the user to set a download file name and format for the API spec.
This commit is contained in:
Zachary Whaley 2021-07-20 12:10:54 -05:00
parent f7211ceb08
commit 196a2a0912
7 changed files with 77 additions and 12 deletions

View File

@ -232,6 +232,7 @@ You can use all of the following options with standalone version on <redoc> tag
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time. * `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `maxDisplayedEnumValues` - display only specified number of enum values. hide rest values under spoiler. * `maxDisplayedEnumValues` - display only specified number of enum values. hide rest values under spoiler.
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button. * `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `downloadFileName` - set file name and format of downloaded spec file, e.g. `openapi.yaml`, default `swagger.json`.
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition. * `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `hideLoading` - do not show loading animation. Useful for small docs. * `hideLoading` - do not show loading animation. Useful for small docs.
* `hideSchemaPattern` - if set, the pattern is not shown in the schema. * `hideSchemaPattern` - if set, the pattern is not shown in the schema.

10
package-lock.json generated
View File

@ -16,6 +16,7 @@
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^2.2.8", "dompurify": "^2.2.8",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"js-yaml": "^4.1.0",
"json-pointer": "^0.6.1", "json-pointer": "^0.6.1",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
@ -90,7 +91,6 @@
"fork-ts-checker-webpack-plugin": "^6.2.10", "fork-ts-checker-webpack-plugin": "^6.2.10",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"jest": "^27.0.3", "jest": "^27.0.3",
"js-yaml": "^4.1.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mobx": "^6.3.2", "mobx": "^6.3.2",
@ -3852,8 +3852,7 @@
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"dev": true
}, },
"node_modules/arr-diff": { "node_modules/arr-diff": {
"version": "4.0.0", "version": "4.0.0",
@ -13118,7 +13117,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
@ -23957,8 +23955,7 @@
"argparse": { "argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"dev": true
}, },
"arr-diff": { "arr-diff": {
"version": "4.0.0", "version": "4.0.0",
@ -31064,7 +31061,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"requires": { "requires": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
} }

View File

@ -112,7 +112,6 @@
"fork-ts-checker-webpack-plugin": "^6.2.10", "fork-ts-checker-webpack-plugin": "^6.2.10",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"jest": "^27.0.3", "jest": "^27.0.3",
"js-yaml": "^4.1.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mobx": "^6.3.2", "mobx": "^6.3.2",
@ -154,6 +153,7 @@
"dompurify": "^2.2.8", "dompurify": "^2.2.8",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"json-pointer": "^0.6.1", "json-pointer": "^0.6.1",
"js-yaml": "^4.1.0",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.7.0", "marked": "^0.7.0",

View File

@ -1,3 +1,4 @@
import * as path from 'path';
import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } from '../theme'; import defaultTheme, { ResolvedThemeInterface, resolveTheme, ThemeInterface } from '../theme';
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { isNumeric, mergeObjects } from '../utils/helpers'; import { isNumeric, mergeObjects } from '../utils/helpers';
@ -19,6 +20,7 @@ export interface RedocRawOptions {
untrustedSpec?: boolean | string; untrustedSpec?: boolean | string;
hideLoading?: boolean | string; hideLoading?: boolean | string;
hideDownloadButton?: boolean | string; hideDownloadButton?: boolean | string;
downloadFileName?: string;
disableSearch?: boolean | string; disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string; onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
@ -153,6 +155,20 @@ export class RedocNormalizedOptions {
return 0; return 0;
} }
static normalizeDownloadFileName(value: RedocRawOptions['downloadFileName']): string {
if (value) {
const extname = path.extname(value);
if (extname === '.json' || extname === '.yaml') {
return value;
} else {
console.warn(`downloadFileName must be a JSON or YAML file.`);
}
}
// Default value
return 'swagger.json';
}
private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number { private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number {
if (level === 'all') { if (level === 'all') {
return +Infinity; return +Infinity;
@ -175,6 +191,7 @@ export class RedocNormalizedOptions {
pathInMiddlePanel: boolean; pathInMiddlePanel: boolean;
untrustedSpec: boolean; untrustedSpec: boolean;
hideDownloadButton: boolean; hideDownloadButton: boolean;
downloadFileName: string;
disableSearch: boolean; disableSearch: boolean;
onlyRequiredInSamples: boolean; onlyRequiredInSamples: boolean;
showExtensions: boolean | string[]; showExtensions: boolean | string[];
@ -232,6 +249,7 @@ export class RedocNormalizedOptions {
this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel);
this.untrustedSpec = argValueToBoolean(raw.untrustedSpec); this.untrustedSpec = argValueToBoolean(raw.untrustedSpec);
this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton); this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton);
this.downloadFileName = RedocNormalizedOptions.normalizeDownloadFileName(raw.downloadFileName);
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);

View File

@ -24,7 +24,7 @@ export class SpecStore {
private options: RedocNormalizedOptions, private options: RedocNormalizedOptions,
) { ) {
this.parser = new OpenAPIParser(spec, specUrl, options); this.parser = new OpenAPIParser(spec, specUrl, options);
this.info = new ApiInfoModel(this.parser); this.info = new ApiInfoModel(this.parser, this.options);
this.externalDocs = this.parser.spec.externalDocs; this.externalDocs = this.parser.spec.externalDocs;
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options); this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
this.securitySchemes = new SecuritySchemesModel(this.parser); this.securitySchemes = new SecuritySchemesModel(this.parser);

View File

@ -62,5 +62,43 @@ describe('Models', () => {
const { license = { identifier: null } } = new ApiInfoModel(parser); const { license = { identifier: null } } = new ApiInfoModel(parser);
expect(license.identifier).toEqual('MIT'); expect(license.identifier).toEqual('MIT');
}); });
test('should correctly populate default download file name', () => {
parser.spec = {
openapi: '3.0.0',
info: {
description: 'Test description',
},
} as any;
const info = new ApiInfoModel(parser);
expect(info.downloadFileName).toEqual('swagger.json');
});
test('should correctly populate download file name', () => {
parser.spec = {
openapi: '3.0.0',
info: {
description: 'Test description',
},
} as any;
const opts = new RedocNormalizedOptions({downloadFileName: 'openapi.yaml'});
const info = new ApiInfoModel(parser, opts);
expect(info.downloadFileName).toEqual('openapi.yaml');
});
test('should correctly populate default download file name if invalid extension is used', () => {
parser.spec = {
openapi: '3.0.0',
info: {
description: 'Test description',
},
} as any;
const opts = new RedocNormalizedOptions({downloadFileName: 'nope.txt'});
const info = new ApiInfoModel(parser, opts);
expect(info.downloadFileName).toEqual('swagger.json');
});
}); });
}); });

View File

@ -1,6 +1,9 @@
import * as path from 'path';
import * as yaml from 'js-yaml'
import { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types'; import { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types';
import { IS_BROWSER } from '../../utils/'; import { IS_BROWSER } from '../../utils/';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class ApiInfoModel implements OpenAPIInfo { export class ApiInfoModel implements OpenAPIInfo {
title: string; title: string;
@ -15,7 +18,10 @@ export class ApiInfoModel implements OpenAPIInfo {
downloadLink?: string; downloadLink?: string;
downloadFileName?: string; downloadFileName?: string;
constructor(private parser: OpenAPIParser) { constructor(
private parser: OpenAPIParser,
private options: RedocNormalizedOptions = new RedocNormalizedOptions({}),
) {
Object.assign(this, parser.spec.info); Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || ''; this.description = parser.spec.info.description || '';
this.summary = parser.spec.info.summary || ''; this.summary = parser.spec.info.summary || '';
@ -35,7 +41,13 @@ export class ApiInfoModel implements OpenAPIInfo {
} }
if (IS_BROWSER && window.Blob && window.URL && window.URL.createObjectURL) { if (IS_BROWSER && window.Blob && window.URL && window.URL.createObjectURL) {
const blob = new Blob([JSON.stringify(this.parser.spec, null, 2)], { let specString: string;
if (path.extname(this.options.downloadFileName) === '.yaml') {
specString = yaml.safeDump(this.parser.spec);
} else {
specString = JSON.stringify(this.parser.spec, null, 2);
}
const blob = new Blob([specString], {
type: 'application/json', type: 'application/json',
}); });
return window.URL.createObjectURL(blob); return window.URL.createObjectURL(blob);
@ -44,7 +56,7 @@ export class ApiInfoModel implements OpenAPIInfo {
private getDownloadFileName(): string | undefined { private getDownloadFileName(): string | undefined {
if (!this.parser.specUrl) { if (!this.parser.specUrl) {
return 'swagger.json'; return this.options.downloadFileName;
} }
return undefined; return undefined;
} }