Allow inso generate config to specify tags (#3381)

Co-authored-by: Dimitri Mitropoulos <dimitrimitropoulos@gmail.com>
This commit is contained in:
Opender Singh 2021-05-14 11:30:44 +12:00 committed by GitHub
parent 5a1f793547
commit 5525e6c87f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 121 additions and 88 deletions

View File

@ -71,6 +71,7 @@ Similar to the Kong [Kubernetes](https://insomnia.rest/plugins/insomnia-plugin-k
|- |- |- |
| `--type <type>` | `-t` |type of configuration to generate, options are `kubernetes` and `declarative` (default: `declarative` ) |
| `--output <path>` | `-o` |save the generated config to a file in the working directory|
| `--tags <tags>` | |comma separated list of tags to apply to each entity|
### Examples
@ -97,10 +98,16 @@ inso generate config spec.yaml --workingDir another/dir
```
Add tags
```sh
inso generate config spec.yaml --tags first
inso generate config spec.yaml --tags "first,second"
```
Output to file
``` sh
inso generate config spc_46c5a4 --output output.yaml
inso generate config spc_46c5a4 --output output.yaml
inso generate config spc_46c5a4 > output.yaml
```
@ -208,7 +215,7 @@ inso export spec "Sample Specification"
Output to file
``` sh
inso export spec spc_46c5a4 --output output.yaml
inso export spec spc_46c5a4 --output output.yaml
inso export spec spc_46c5a4 > output.yaml
```
@ -270,7 +277,7 @@ Alternatively, you can use the `--config <file>` global option to specify an exa
Options from the config file are combined with option defaults and any explicit overrides specified in script or command invocations. This combination is in priority order: command options > config file options > default options.
Any options specified in this file will apply to all scripts and manual commands. You can override these options by specifying them explicitly, when invoking a script or command.
Any options specified in this file will apply to all scripts and manual commands. You can override these options by specifying them explicitly, when invoking a script or command.
Only [global options](#global-options) can be set in the config file.
@ -290,7 +297,7 @@ scripts:
test-spec:200s: inso testSpec --testNamePattern 200
test-spec:404s: inso testSpec --testNamePattern 404
test-math-suites: inso run test uts_8783c30a24b24e9a851d96cce48bd1f2 --env DemoEnv
test-math-suites: inso run test uts_8783c30a24b24e9a851d96cce48bd1f2 --env DemoEnv
test-request-suite: inso run test uts_bce4af --env DemoEnv --bail
lint: inso lint spec Demo # must be invoked as `inso script lint`
@ -303,7 +310,7 @@ scripts:
Git Bash on Windows is not interactive and therefore prompts from `inso` will not work as expected. You may choose to specify the identifiers for each command explicitly, or run `inso` using `winpty` :
```
```
winpty inso.cmd generate config
```

View File

@ -25,8 +25,8 @@
"clean": "tsc --build tsconfig.build.json --clean",
"postclean": "rimraf dist",
"test": "jest --runInBand",
"test:watch": "jest --watch",
"test:snapshots": "npm run build && jest -u",
"test:watch": "npm run test -- --watch",
"test:snapshots": "npm run build && npm run test -- -u",
"prebuild": "npm run clean",
"build": "webpack --config webpack/webpack.config.development.js",
"build:production": "webpack --config webpack/webpack.config.production.js --display errors-only",

View File

@ -92,6 +92,7 @@ Generate configuration from an api spec.
Options:
-t, --type <value> type of configuration to generate, options are
[kubernetes, declarative] (default: declarative)
--tags <tags> comma separated list of tags to apply to each entity
-o, --output <path> save the generated config to a file
-h, --help display help for command"
`;

View File

@ -1,13 +1,13 @@
import * as cli from './cli';
import { generateConfig } from './commands/generate-config';
import { lintSpecification } from './commands/lint-specification';
import { runInsomniaTests } from './commands/run-tests';
import { exportSpecification } from './commands/export-specification';
import { generateConfig as _generateConfig } from './commands/generate-config';
import { lintSpecification as _lintSpecification } from './commands/lint-specification';
import { runInsomniaTests as _runInsomniaTests } from './commands/run-tests';
import { exportSpecification as _exportSpecification } from './commands/export-specification';
import { parseArgsStringToArgv } from 'string-argv';
import * as packageJson from '../package.json';
import { globalBeforeAll, globalBeforeEach } from './jest/before';
import { logger } from './logger';
import { exit } from './util';
import { exit as _exit } from './util';
jest.mock('./commands/generate-config');
jest.mock('./commands/lint-specification');
@ -23,6 +23,12 @@ const initInso = () => {
};
};
const generateConfig = _generateConfig as jest.MockedFunction<typeof _generateConfig>;
const lintSpecification = _lintSpecification as jest.MockedFunction<typeof _lintSpecification>;
const runInsomniaTests = _runInsomniaTests as jest.MockedFunction<typeof _runInsomniaTests>;
const exportSpecification = _exportSpecification as jest.MockedFunction<typeof _exportSpecification>;
const exit = _exit as jest.MockedFunction<typeof _exit>;
describe('cli', () => {
beforeAll(() => {
globalBeforeAll();
@ -52,11 +58,13 @@ describe('cli', () => {
it('should throw error if working dir argument is missing', () => {
expect(() => inso('-w')).toThrowError();
});
it.each(['-v', '--version'])('inso %s should print version from package.json', args => {
logger.wrapAll();
expect(() => inso(args)).toThrowError(packageJson.version);
logger.restoreAll();
});
it.each(['-v', '--version'])('inso %s should print "dev" if running in development', args => {
const oldNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
@ -95,6 +103,10 @@ describe('cli', () => {
expect(() => inso('generate config -o')).toThrowError();
});
it('should throw error if tags argument is missing', () => {
expect(() => inso('generate config --tags')).toThrowError();
});
it('should call generateConfig with undefined output argument', () => {
inso('generate config -t declarative file.yaml');
expect(generateConfig).toHaveBeenCalledWith('file.yaml', {
@ -103,12 +115,13 @@ describe('cli', () => {
});
it('should call generateConfig with all expected arguments', () => {
inso('generate config -t kubernetes -o output.yaml file.yaml');
inso('generate config -t kubernetes -o output.yaml --tags "a,b,c" file.yaml');
expect(generateConfig).toHaveBeenCalledWith(
'file.yaml',
expect.objectContaining({
type: 'kubernetes',
output: 'output.yaml',
tags: 'a,b,c',
}),
);
});
@ -219,7 +232,7 @@ describe('cli', () => {
const expectExitWith = async (result: boolean): Promise<void> =>
expect(
(exit as jest.Mock).mock.calls[0][0],
exit.mock.calls[0][0],
).resolves.toBe(result);
it('should call script command by default', () => {

View File

@ -24,8 +24,9 @@ type CreateCommand = (command: string) => commander.Command
const makeGenerateCommand = (commandCreator: CreateCommand) => {
// inso generate
const command = commandCreator('generate').description('Code generation utilities');
const defaultType = 'declarative'; // inso generate config -t kubernetes config.yaml
const defaultType: keyof typeof conversionTypeMap = 'declarative';
// inso generate config -t kubernetes config.yaml
command
.command('config [identifier]')
.description('Generate configuration from an api spec.')
@ -33,6 +34,7 @@ const makeGenerateCommand = (commandCreator: CreateCommand) => {
'-t, --type <value>',
`type of configuration to generate, options are [${Object.keys(conversionTypeMap).join(', ')}] (default: ${defaultType})`,
)
.option('--tags <tags>', 'comma separated list of tags to apply to each entity')
.option('-o, --output <path>', 'save the generated config to a file')
.action((identifier, cmd) => {
let options = getOptions<GenerateConfigOptions>(cmd, {
@ -47,8 +49,9 @@ const makeGenerateCommand = (commandCreator: CreateCommand) => {
const makeTestCommand = (commandCreator: CreateCommand) => {
// inso run
const run = commandCreator('run').description('Execution utilities');
const defaultReporter: TestReporter = 'spec'; // inso run tests
const defaultReporter: TestReporter = 'spec';
// inso run tests
run
.command('test [identifier]')
.description('Run Insomnia unit test suites')
@ -72,8 +75,9 @@ const makeTestCommand = (commandCreator: CreateCommand) => {
const makeLintCommand = (commandCreator: CreateCommand) => {
// inso lint
const lint = commandCreator('lint').description('Linting utilities'); // inso lint spec
const lint = commandCreator('lint').description('Linting utilities');
// inso lint spec
lint
.command('spec [identifier]')
.description('Lint an API Specification')
@ -87,8 +91,9 @@ const makeLintCommand = (commandCreator: CreateCommand) => {
const makeExportCommand = (commandCreator: CreateCommand) => {
// inso export
const exportCmd = commandCreator('export').description('Export data from insomnia models'); // inso export spec
const exportCmd = commandCreator('export').description('Export data from insomnia models');
// inso export spec
exportCmd
.command('spec [identifier]')
.description('Export an API Specification to a file')

View File

@ -1,11 +1,12 @@
import { exportSpecification } from './export-specification';
import { writeFileWithCliOptions } from '../write-file';
import { writeFileWithCliOptions as _writeFileWithCliOptions } from '../write-file';
import { globalBeforeAll, globalBeforeEach } from '../jest/before';
import { logger } from '../logger';
import { UNKNOWN } from '../types';
jest.mock('../write-file');
const writeFileWithCliOptions = _writeFileWithCliOptions as jest.MockedFunction<typeof _writeFileWithCliOptions>;
describe('exportSpecification()', () => {
beforeAll(() => {
globalBeforeAll();
@ -15,8 +16,6 @@ describe('exportSpecification()', () => {
globalBeforeEach();
});
const mock = (mockFn: UNKNOWN) => mockFn;
afterEach(() => {
jest.restoreAllMocks();
});
@ -32,7 +31,7 @@ describe('exportSpecification()', () => {
it('should output document to a file', async () => {
const outputPath = 'this-is-the-output-path';
mock(writeFileWithCliOptions).mockResolvedValue(outputPath);
writeFileWithCliOptions.mockResolvedValue(outputPath);
const options = {
output: 'output.yaml',
workingDir: 'src/db/fixtures/git-repo',
@ -49,7 +48,7 @@ describe('exportSpecification()', () => {
it('should throw if writing file returns error', async () => {
const error = new Error('error message');
mock(writeFileWithCliOptions).mockRejectedValue(error);
writeFileWithCliOptions.mockRejectedValue(error);
const options = {
output: 'output.yaml',
workingDir: 'src/db/fixtures/git-repo',

View File

@ -1,16 +1,26 @@
import { conversionTypeMap, generateConfig, GenerateConfigOptions } from './generate-config';
import o2k from 'openapi-2-kong';
import { ConversionResult, generate as _generate, generateFromString as _generateFromString } from 'openapi-2-kong';
import path from 'path';
import { writeFileWithCliOptions } from '../write-file';
import { writeFileWithCliOptions as _writeFileWithCliOptions } from '../write-file';
import { globalBeforeAll, globalBeforeEach } from '../jest/before';
import { logger } from '../logger';
import { InsoError } from '../errors';
import os from 'os';
import { UNKNOWN } from '../types';
jest.mock('openapi-2-kong');
jest.mock('../write-file');
const generate = _generate as jest.MockedFunction<typeof _generate>;
const generateFromString = _generateFromString as jest.MockedFunction<typeof _generateFromString>;
const writeFileWithCliOptions = _writeFileWithCliOptions as jest.MockedFunction<typeof _writeFileWithCliOptions>;
const mockConversionResult: ConversionResult = {
documents: ['a', 'b'],
type: 'kong-for-kubernetes',
label: '',
warnings: [],
};
describe('generateConfig()', () => {
beforeAll(() => {
globalBeforeAll();
@ -20,8 +30,6 @@ describe('generateConfig()', () => {
globalBeforeEach();
});
const mock = (mockFn: UNKNOWN) => mockFn;
afterEach(() => {
jest.restoreAllMocks();
});
@ -33,41 +41,41 @@ describe('generateConfig()', () => {
// @ts-expect-error intentionally invalid input
type: 'invalid',
});
expect(o2k.generate).not.toHaveBeenCalled();
expect(generate).not.toHaveBeenCalled();
expect(logger.__getLogs().fatal).toEqual([
'Config type "invalid" not unrecognized. Options are [kubernetes, declarative].',
]);
});
it('should print conversion documents to console', async () => {
mock(o2k.generate).mockResolvedValue({
documents: ['a', 'b'],
});
await generateConfig(filePath, {
type: 'kubernetes',
});
expect(o2k.generate).toHaveBeenCalledWith(filePath, conversionTypeMap.kubernetes);
generate.mockResolvedValue(mockConversionResult);
await generateConfig(filePath, { type: 'kubernetes', tags: 'tag' });
expect(generate).toHaveBeenCalledWith(filePath, conversionTypeMap.kubernetes, ['tag']);
expect(logger.__getLogs().log).toEqual(['a\n---\nb\n']);
});
it('should load identifier from database', async () => {
mock(o2k.generateFromString).mockResolvedValue({
documents: ['a', 'b'],
});
generateFromString.mockResolvedValue(mockConversionResult);
await generateConfig('spc_46c5a4a40e83445a9bd9d9758b86c16c', {
type: 'kubernetes',
workingDir: 'src/db/fixtures/git-repo',
tags: 'first,second',
});
expect(o2k.generateFromString).toHaveBeenCalled();
expect(generateFromString).toHaveBeenCalledWith(
expect.stringMatching(/.+/),
conversionTypeMap.kubernetes,
['first', 'second'],
);
expect(logger.__getLogs().log).toEqual(['a\n---\nb\n']);
});
it('should output generated document to a file', async () => {
mock(o2k.generate).mockResolvedValue({
documents: ['a', 'b'],
});
generate.mockResolvedValue(mockConversionResult);
const outputPath = 'this-is-the-output-path';
mock(writeFileWithCliOptions).mockResolvedValue(outputPath);
writeFileWithCliOptions.mockResolvedValue(outputPath);
const options: Partial<GenerateConfigOptions> = {
type: 'kubernetes',
output: 'output.yaml',
@ -84,11 +92,9 @@ describe('generateConfig()', () => {
});
it('should return false if failed to write file', async () => {
mock(o2k.generate).mockResolvedValue({
documents: ['a', 'b'],
});
generate.mockResolvedValue(mockConversionResult);
const error = new Error('error message');
mock(writeFileWithCliOptions).mockRejectedValue(error);
writeFileWithCliOptions.mockRejectedValue(error);
const options: Partial<GenerateConfigOptions> = {
type: 'kubernetes',
output: 'output.yaml',
@ -99,11 +105,9 @@ describe('generateConfig()', () => {
});
it('should generate documents using workingDir', async () => {
mock(o2k.generate).mockResolvedValue({
documents: ['a', 'b'],
});
generate.mockResolvedValue(mockConversionResult);
const outputPath = 'this-is-the-output-path';
mock(writeFileWithCliOptions).mockResolvedValue(outputPath);
writeFileWithCliOptions.mockResolvedValue(outputPath);
const result = await generateConfig('file.yaml', {
type: 'kubernetes',
workingDir: 'test/dir',
@ -111,47 +115,44 @@ describe('generateConfig()', () => {
});
expect(result).toBe(true); // Read from workingDir
expect(o2k.generate).toHaveBeenCalledWith(
expect(generate).toHaveBeenCalledWith(
path.normalize('test/dir/file.yaml'),
conversionTypeMap.kubernetes,
undefined,
);
expect(logger.__getLogs().log).toEqual([`Configuration generated to "${outputPath}".`]);
});
it('should generate documents using absolute path', async () => {
mock(o2k.generate).mockResolvedValue({
documents: ['a', 'b'],
});
generate.mockResolvedValue(mockConversionResult);
const outputPath = 'this-is-the-output-path';
mock(writeFileWithCliOptions).mockResolvedValue(outputPath);
writeFileWithCliOptions.mockResolvedValue(outputPath);
const absolutePath = path.join(os.tmpdir(), 'dev', 'file.yaml');
const result = await generateConfig(absolutePath, {
type: 'kubernetes',
workingDir: 'test/dir',
output: 'output.yaml',
});
expect(result).toBe(true); // Read from workingDir
expect(result).toBe(true);
expect(o2k.generate).toHaveBeenCalledWith(absolutePath, conversionTypeMap.kubernetes);
// Read from workingDir
expect(generate).toHaveBeenCalledWith(absolutePath, conversionTypeMap.kubernetes, undefined);
expect(logger.__getLogs().log).toEqual([`Configuration generated to "${outputPath}".`]);
});
it('should throw InsoError if there is an error thrown by openapi-2-kong', async () => {
const error = new Error('err');
mock(o2k.generate).mockRejectedValue(error);
const promise = generateConfig('file.yaml', {
type: 'kubernetes',
});
generate.mockRejectedValue(error);
const promise = generateConfig('file.yaml', { type: 'kubernetes' });
await expect(promise).rejects.toThrowError(
new InsoError('There was an error while generating configuration', error),
);
});
it('should warn if no valid spec can be found', async () => {
mock(o2k.generate).mockResolvedValue({});
const result = await generateConfig('file.yaml', {
type: 'kubernetes',
});
// @ts-expect-error intentionally passing in a bad value
generate.mockResolvedValue({});
const result = await generateConfig('file.yaml', { type: 'kubernetes' });
expect(result).toBe(false);
expect(logger.__getLogs().log).toEqual([
'Could not find a valid specification to generate configuration.',

View File

@ -21,6 +21,7 @@ export const conversionTypes: ConversionResultType[] = [
export type GenerateConfigOptions = GlobalOptions & {
type: keyof (typeof conversionTypeMap);
output?: string;
tags?: string,
};
const validateOptions = (
@ -43,7 +44,7 @@ export const generateConfig = async (
return false;
}
const { type, output, appDataDir, workingDir, ci } = options;
const { type, output, tags, appDataDir, workingDir, ci } = options;
const db = await loadDb({
workingDir,
appDataDir,
@ -55,17 +56,23 @@ export const generateConfig = async (
// try get from db
const specFromDb = identifier ? loadApiSpec(db, identifier) : await promptApiSpec(db, !!ci);
const generationTags = tags?.split(',');
try {
if (specFromDb?.contents) {
logger.trace('Generating config from database contents');
result = await generateFromString(specFromDb.contents, conversionTypeMap[type]);
result = await generateFromString(
specFromDb.contents,
conversionTypeMap[type],
generationTags,
);
} else if (identifier) {
// try load as a file
const fileName = path.isAbsolute(identifier)
? identifier
: path.join(workingDir || '.', identifier);
logger.trace(`Generating config from file \`${fileName}\``);
result = await generate(fileName, conversionTypeMap[type]);
result = await generate(fileName, conversionTypeMap[type], generationTags);
}
} catch (e) {
throw new InsoError('There was an error while generating configuration', e);

View File

@ -7,8 +7,8 @@ import { GenerateConfigOptions } from './generate-config';
jest.mock('insomnia-testing');
jest.mock('insomnia-send-request');
const generate = _generate as jest.Mock;
const runTestsCli = _runTestsCli as jest.Mock;
const generate = _generate as jest.MockedFunction<typeof _generate>;
const runTestsCli = _runTestsCli as jest.MockedFunction<typeof _runTestsCli>;
describe('runInsomniaTests()', () => {
beforeAll(() => {

View File

@ -1,14 +1,16 @@
import { emptyDb, loadDb } from './index';
import gitAdapter from './adapters/git-adapter';
import neDbAdapter from './adapters/ne-db-adapter';
import _gitAdapter from './adapters/git-adapter';
import _neDbAdapter from './adapters/ne-db-adapter';
import { globalBeforeAll, globalBeforeEach } from '../jest/before';
import { logger } from '../logger';
import path from 'path';
import { UNKNOWN } from '../types';
jest.mock('./adapters/git-adapter');
jest.mock('./adapters/ne-db-adapter');
const gitAdapter = _gitAdapter as jest.MockedFunction<typeof _gitAdapter>;
const neDbAdapter = _neDbAdapter as jest.MockedFunction<typeof _neDbAdapter>;
describe('loadDb()', () => {
beforeAll(() => {
globalBeforeAll();
@ -19,10 +21,8 @@ describe('loadDb()', () => {
globalBeforeEach();
});
const mock = (mockFn: UNKNOWN) => mockFn;
it('should default to current directory if working dir not defined', async () => {
mock(gitAdapter).mockResolvedValue(emptyDb());
gitAdapter.mockResolvedValue(emptyDb());
await loadDb({
workingDir: undefined,
});
@ -34,7 +34,7 @@ describe('loadDb()', () => {
});
it('should load git data from working directory', async () => {
mock(gitAdapter).mockResolvedValue(emptyDb());
gitAdapter.mockResolvedValue(emptyDb());
await loadDb({
workingDir: 'dir',
filterTypes: ['Environment'],
@ -47,8 +47,8 @@ describe('loadDb()', () => {
});
it('should load nedb from appDataDir', async () => {
mock(gitAdapter).mockResolvedValue(emptyDb());
mock(neDbAdapter).mockResolvedValue(emptyDb());
gitAdapter.mockResolvedValue(emptyDb());
neDbAdapter.mockResolvedValue(emptyDb());
await loadDb({
appDataDir: 'dir',
filterTypes: ['Environment'],
@ -61,7 +61,7 @@ describe('loadDb()', () => {
});
it('should not load from git if appDataDir is defined', async () => {
mock(neDbAdapter).mockResolvedValue(emptyDb());
neDbAdapter.mockResolvedValue(emptyDb());
await loadDb({
appDataDir: 'dir',
});
@ -73,8 +73,8 @@ describe('loadDb()', () => {
});
it('should load from neDb if not loaded from git', async () => {
mock(gitAdapter).mockResolvedValue(null);
mock(neDbAdapter).mockResolvedValue(emptyDb());
gitAdapter.mockResolvedValue(null);
neDbAdapter.mockResolvedValue(emptyDb());
await loadDb(); // Cannot assert the full path because it is application data
expect(logger.__getLogs().debug).toEqual([
@ -85,8 +85,8 @@ describe('loadDb()', () => {
});
it('should warn and return empty db if nothing loaded from git or nedb', async () => {
mock(gitAdapter).mockResolvedValue(null);
mock(neDbAdapter).mockResolvedValue(null);
gitAdapter.mockResolvedValue(null);
neDbAdapter.mockResolvedValue(null);
const db = await loadDb();
expect(logger.__getLogs().warn).toEqual([
'No git or app data store found, re-run `inso` with `--verbose` to see tracing information',

View File

@ -7,7 +7,7 @@ import { loadEnvironment, promptEnvironment } from './environment';
import { globalBeforeAll, globalBeforeEach } from '../../jest/before';
jest.mock('enquirer');
const enquirer = _enquirer as unknown as jest.Mock & {
const enquirer = _enquirer as jest.MockedClass<typeof _enquirer> & {
__mockPromptRun: (str: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- missing types from enquirer
__constructorMock: jest.Mock;