From 5fc4ceed8751a0508047013d14ac8353dffa4ead Mon Sep 17 00:00:00 2001 From: Kanad Gupta <8854718+kanadgupta@users.noreply.github.com> Date: Wed, 30 Nov 2022 13:31:07 -0600 Subject: [PATCH] feat(`openapi:reduce`): pass opts (#684) * chore: fix debug * feat(openapi/reduce): ability to pass in opts * test: fix test * fix: fallback value when parsing through bad paths * fix: another edge case * docs: add language on this * Update README.md Co-authored-by: Jon Ursenbach * test: typo Co-authored-by: Jon Ursenbach --- README.md | 14 +++- __tests__/cmds/openapi/reduce.test.ts | 105 +++++++++++++++++++++++++- src/cmds/openapi/reduce.ts | 51 ++++++++++++- 3 files changed, 166 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c041e1ddc..d35e64524 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,19 @@ We also offer a tool that allows you to reduce a large API definition down to a rdme openapi:reduce [path-to-file.json] ``` -The command will ask you a couple questions about how you wish to reduce the file and then do so. And as with the `openapi` command, you can also [omit the file path](#omitting-the-file-path). +The command will ask you a couple questions about how you wish to reduce the file and then do so. If you wish to automate this command, you can pass in CLI arguments to bypass the prompts. Here's an example use case: + +- The input API definition is called `petstore.json` +- The file is reduced to only the `/pet/{id}` path and the `GET` and `PUT` methods +- The output file is called `petstore-reduced.json` + +Here's what the resulting command looks like: + +``` +rdme openapi:reduce petstore.json --path /pet/{id} --method get --method put --out petstore-reduced.json +``` + +As with the `openapi` command, you can also [omit the file path](#omitting-the-file-path). ### Docs (a.k.a. Guides) 📖 diff --git a/__tests__/cmds/openapi/reduce.test.ts b/__tests__/cmds/openapi/reduce.test.ts index e003e4e72..df7107c5b 100644 --- a/__tests__/cmds/openapi/reduce.test.ts +++ b/__tests__/cmds/openapi/reduce.test.ts @@ -85,6 +85,30 @@ describe('rdme openapi:reduce', () => { expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/user']); }); + + it('should reduce with no prompts via opts', async () => { + const spec = 'petstore.json'; + + let reducedSpec; + fs.writeFileSync = jest.fn((fileName, data) => { + reducedSpec = JSON.parse(data as string); + }); + + await expect( + reducer.run({ + workingDirectory: './__tests__/__fixtures__/relative-ref-oas', + tag: ['user'], + out: 'output.json', + }) + ).resolves.toBe(successfulReduction()); + + expect(console.info).toHaveBeenCalledTimes(1); + + const output = getCommandOutput(); + expect(output).toBe(chalk.yellow(`ℹ️ We found ${spec} and are attempting to reduce it.`)); + + expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/user']); + }); }); describe('by path', () => { @@ -115,6 +139,34 @@ describe('rdme openapi:reduce', () => { expect(Object.keys(reducedSpec.paths['/pet'])).toStrictEqual(['post']); expect(Object.keys(reducedSpec.paths['/pet/findByStatus'])).toStrictEqual(['get']); }); + + it('should reduce with no prompts via opts', async () => { + const spec = 'petstore.json'; + + let reducedSpec; + fs.writeFileSync = jest.fn((fileName, data) => { + reducedSpec = JSON.parse(data as string); + }); + + await expect( + reducer.run({ + workingDirectory: './__tests__/__fixtures__/relative-ref-oas', + path: ['/pet', '/pet/{petId}'], + method: ['get', 'post'], + out: 'output.json', + }) + ).resolves.toBe(successfulReduction()); + + expect(console.info).toHaveBeenCalledTimes(1); + + const output = getCommandOutput(); + expect(output).toBe(chalk.yellow(`ℹ️ We found ${spec} and are attempting to reduce it.`)); + + expect(fs.writeFileSync).toHaveBeenCalledWith('output.json', expect.any(String)); + expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/pet', '/pet/{petId}']); + expect(Object.keys(reducedSpec.paths['/pet'])).toStrictEqual(['post']); + expect(Object.keys(reducedSpec.paths['/pet/{petId}'])).toStrictEqual(['get']); + }); }); }); @@ -129,7 +181,7 @@ describe('rdme openapi:reduce', () => { ).rejects.toStrictEqual(new Error('Sorry, this reducer feature in rdme only supports OpenAPI 3.0+ definitions.')); }); - it('should fail if you attempt to reduce a spec to nothing', async () => { + it('should fail if you attempt to reduce a spec to nothing via tags', async () => { const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); prompts.inject(['tags', ['unknown-tag'], 'output.json']); @@ -142,5 +194,56 @@ describe('rdme openapi:reduce', () => { new Error('All paths in the API definition were removed. Did you supply the right path name to reduce by?') ); }); + + it('should fail if you attempt to reduce a spec to nothing via paths', async () => { + const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); + + prompts.inject(['paths', ['unknown-path'], 'output.json']); + + await expect( + reducer.run({ + spec, + }) + ).rejects.toStrictEqual( + new Error('All paths in the API definition were removed. Did you supply the right path name to reduce by?') + ); + }); + + it('should fail if you attempt to pass both tags and paths as opts', async () => { + const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); + + await expect( + reducer.run({ + spec, + tag: ['tag1', 'tag2'], + path: ['/path'], + }) + ).rejects.toStrictEqual(new Error('You can pass in either tags or paths/methods, but not both.')); + }); + + it('should fail if you attempt to pass both tags and methods as opts', async () => { + const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); + + await expect( + reducer.run({ + spec, + tag: ['tag1', 'tag2'], + method: ['get'], + }) + ).rejects.toStrictEqual(new Error('You can pass in either tags or paths/methods, but not both.')); + }); + + it('should fail if you attempt to pass non-existent path and no method', async () => { + const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); + + await expect( + reducer.run({ + spec, + path: ['unknown-path'], + }) + ).rejects.toStrictEqual( + new Error('All paths in the API definition were removed. Did you supply the right path name to reduce by?') + ); + }); }); }); diff --git a/src/cmds/openapi/reduce.ts b/src/cmds/openapi/reduce.ts index 601f50788..a4a192d99 100644 --- a/src/cmds/openapi/reduce.ts +++ b/src/cmds/openapi/reduce.ts @@ -7,6 +7,7 @@ import chalk from 'chalk'; import jsonpath from 'jsonpath'; import oasReducer from 'oas/dist/lib/reducer'; import ora from 'ora'; +import prompts from 'prompts'; import Command, { CommandCategories } from '../../lib/baseCommand'; import { checkFilePath } from '../../lib/checkFile'; @@ -16,6 +17,10 @@ import promptTerminal from '../../lib/promptWrapper'; export type Options = { spec?: string; + tag?: string[]; + path?: string[]; + method?: string[]; + out?: string; workingDirectory?: string; }; @@ -36,6 +41,29 @@ export default class OpenAPIReduceCommand extends Command { type: String, defaultOption: true, }, + { + name: 'tag', + type: String, + multiple: true, + description: 'Tags to reduce by', + }, + { + name: 'path', + type: String, + multiple: true, + description: 'Paths to reduce by', + }, + { + name: 'method', + type: String, + multiple: true, + description: 'Methods to reduce by (can only be used alongside the `path` option)', + }, + { + name: 'out', + type: String, + description: 'Output file path to write reduced file to', + }, { name: 'workingDirectory', type: String, @@ -60,6 +88,18 @@ export default class OpenAPIReduceCommand extends Command { throw new Error('Sorry, this reducer feature in rdme only supports OpenAPI 3.0+ definitions.'); } + if ((opts.path?.length || opts.method?.length) && opts.tag?.length) { + throw new Error('You can pass in either tags or paths/methods, but not both.'); + } + + prompts.override({ + reduceBy: opts.tag?.length ? 'tags' : opts.path?.length ? 'paths' : undefined, + tags: opts.tag, + paths: opts.path, + methods: opts.method, + outputPath: opts.out, + }); + const promptResults = await promptTerminal([ { type: 'select', @@ -111,10 +151,17 @@ export default class OpenAPIReduceCommand extends Command { choices: (prev, values) => { const paths: string[] = values.paths; let methods = paths - .map((p: string) => Object.keys(parsedBundledSpec.paths[p])) + .map((p: string) => Object.keys(parsedBundledSpec.paths[p] || {})) .flat() .filter((method: string) => method.toLowerCase() !== 'parameters'); + // We have to catch this case so prompt doesn't crash + if (!methods.length && !opts.method?.length) { + throw new Error( + 'All paths in the API definition were removed. Did you supply the right path name to reduce by?' + ); + } + methods = [...new Set(methods)]; methods.sort(); @@ -140,7 +187,7 @@ export default class OpenAPIReduceCommand extends Command { Command.debug( `options being supplied to the reducer: ${JSON.stringify({ tags: promptResults.tags, - paths: promptResults.tags, + paths: promptResults.paths, methods: promptResults.methods, })}` );