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,6 +98,12 @@ 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

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;