Skip to content

Commit

Permalink
fix: improve CLI messages and move them into separate file (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
Souvikns committed Aug 11, 2021
1 parent dd5ad5c commit 14e94d8
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 63 deletions.
9 changes: 5 additions & 4 deletions src/components/Context/context.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import React from 'react';
import { render } from 'ink-testing-library';
import { ListContexts, ShowCurrentContext, AddContext, SetCurrent } from './Context';
import { ContextTestingHelper } from '../../constants';
import * as messages from '../../messages';

const testing = new ContextTestingHelper();

describe('listing contexts', () => {
test('should render error when no context file found', () => {
testing.deleteDummyContextFile();
const { lastFrame } = render(<ListContexts />);
expect(lastFrame()).toMatch('No contexts saved yet.');
expect(lastFrame()).toMatch(messages.NO_CONTEXTS_SAVED);
});

test('Should render the context list', () => {
Expand All @@ -27,7 +28,7 @@ describe('rendering current context', () => {
testing.deleteDummyContextFile();
const { lastFrame } = render(<ShowCurrentContext />);
const message = lastFrame();
expect(message).toMatch('No contexts saved yet.');
expect(message).toMatch(messages.NO_CONTEXTS_SAVED);
});

test('showing current context ', () => {
Expand All @@ -41,15 +42,15 @@ describe('AddContext ', () => {
test('should return message', () => {
testing.createDummyContextFile();
const { lastFrame } = render(<AddContext options={{}} args={['home', './test/specification.yml']} />);
expect(lastFrame()).toMatch('New context added');
expect(lastFrame()).toMatch(messages.NEW_CONTEXT_ADDED('home'));
});
});

describe('SetContext ', () => {
test('Should render error message is key is not in store', () => {
testing.createDummyContextFile();
const { lastFrame } = render(<SetCurrent args={['name']} options={{}} />);
expect(lastFrame()).toMatch('The context you are trying to use is not present');
expect(lastFrame()).toMatch(messages.CONTEXT_NOT_FOUND('name'));
});

test('Should render the update context', () => {
Expand Down
14 changes: 0 additions & 14 deletions src/components/Context/contexterror.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import React from 'react';
import { Text } from 'ink';

import { ContextFileNotFoundError, DeletingCurrentContextError, KeyNotFoundError } from '../../hooks/context';

const ContextError: React.FunctionComponent<{ error: Error }> = ({ error }) => {
if (error instanceof ContextFileNotFoundError) {
return <Text>No contexts saved yet.</Text>;
}

if (error instanceof KeyNotFoundError) {
return <Text>The context you are trying to use is not present</Text>;
}

if (error instanceof DeletingCurrentContextError) {
return <Text>You are trying to delete a context that is set as current.</Text>;
}

return <Text>{error.message}</Text>;
};

Expand Down
30 changes: 30 additions & 0 deletions src/help-message.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { HelpMessageBuilder } from './help-message';

let helpBuilder: HelpMessageBuilder;

describe('HelpMessageBuilder should', () => {
beforeAll(() => {
helpBuilder = new HelpMessageBuilder();
});
it('return root Help message', () => {
expect(typeof helpBuilder.showHelp()).toMatch('string');
expect(helpBuilder.showHelp()).toMatch(
'usage: asyncapi [options] [command]\n\n'+
'flags:\n'+
' -h, --help display help for command\n'+
' -v, --version output the version number\n'+
'\n'+
'commands:\n'+
' validate [options] [command] Validate asyncapi file\n'+
' context [options] [command] Manage context\n'
);
});

it('return validate help message', () => {
expect(typeof helpBuilder.showCommandHelp('validate')).toMatch('string');
});

it('return context help message', () => {
expect(typeof helpBuilder.showCommandHelp('context')).toMatch('string');
});
});
99 changes: 99 additions & 0 deletions src/help-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { injectable, container } from 'tsyringe';

export type CommandName = 'validate' | 'context';

export type Command = {
[name in CommandName]: {
usage: string;
shortDescription: string;
longDescription?: string;
flags: string[];
subCommands?: string[];
};
};

@injectable()
export class HelpMessage {
private helpFlag = '-h, --help display help for command';

readonly usage: string = 'asyncapi [options] [command]';

readonly flags = [
this.helpFlag,
'-v, --version output the version number',
];

readonly commands: Command = {
validate: {
usage: 'asyncapi validate [options]',
shortDescription: 'Validate asyncapi file',
flags: [
this.helpFlag,
'-f, --file <spec-file-path> Path of the AsyncAPI file',
'-c, --context <saved-context-name> Context to use',
'-w, --watch Watch mode'
]
},
context: {
usage: 'asyncapi context [command] [options]',
shortDescription: 'Manage context',
longDescription: 'Context is what makes it easier for you to work with multiple AsyncAPI files.\nYou can add multiple different files to a context.\nThis way you do not have to pass --file flag with path to the file every time but just --context flag with reference name.\nYou can also set a default context, so neither --file nor --context flags are needed',
flags: [this.helpFlag],
subCommands: [
'list list all saved contexts',
'current see current context',
'use <context-name> set given context as default/current',
'add <context-name> <spec-file-path> add/update context',
'remove <context-name> remove a context'
]
}
}
}

export class HelpMessageBuilder {
private helpMessage: HelpMessage = container.resolve(HelpMessage);

showHelp() {
let helpText = '';
helpText += `usage: ${this.helpMessage.usage}\n\n`;
helpText += 'flags:\n';
for (const flag of this.helpMessage.flags) {
helpText += ` ${flag}\n`;
}
helpText += '\n';

if (this.helpMessage.commands) {
helpText += 'commands:\n';
for (const [name, obj] of Object.entries(this.helpMessage.commands)) {
helpText += ` ${name} [options] [command] ${obj.shortDescription}\n`;
}
}

return helpText;
}

showCommandHelp(command: CommandName) {
let helpText = '';
const commandHelpObject = this.helpMessage.commands[command as CommandName];
helpText += `usage: ${commandHelpObject.usage}\n\n`;

if (commandHelpObject.longDescription) {
helpText += `${commandHelpObject.longDescription}\n\n`;
}

helpText += 'flags: \n';
for (const flag of commandHelpObject.flags) {
helpText += ` ${flag}\n`;
}

if (commandHelpObject.subCommands) {
helpText += '\n';
helpText += 'commands:\n';
for (const command of commandHelpObject.subCommands) {
helpText += ` ${command}\n`;
}
}

return helpText;
}
}
6 changes: 3 additions & 3 deletions src/hooks/context/contextService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { injectable } from 'tsyringe';
import { Context, ContextFileNotFoundError,KeyNotFoundError, SpecFileNotFoundError } from './models';
import { Context, ContextFileNotFoundError, ContextNotFoundError, SpecFileNotFoundError } from './models';
import { CONTEXTFILE_PATH } from '../../constants';
import * as fs from 'fs';
import * as path from 'path';
Expand All @@ -26,7 +26,7 @@ export class ContextService {
}

addContext(context: Context, key: string, specFile: SpecificationFile): Context {
if (specFile.isNotValid()) {throw new SpecFileNotFoundError();}
if (specFile.isNotValid()) {throw new SpecFileNotFoundError(specFile.getSpecificationName());}
context.store[String(key)] = specFile.getSpecificationName();
return context;
}
Expand All @@ -38,7 +38,7 @@ export class ContextService {
}

updateCurrent(context: Context, key: string): Context {
if (!context.store[String(key)]) {throw new KeyNotFoundError();}
if (!context.store[String(key)]) {throw new ContextNotFoundError(key);}
context.current = key;
return context;
}
Expand Down
15 changes: 8 additions & 7 deletions src/hooks/context/hook.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useContextFile, useSpecfile } from './hooks';
import { ContextFileNotFoundError, KeyNotFoundError, ContextNotFoundError } from './models';
import { ContextFileNotFoundError, ContextNotFoundError } from './models';
import { ContextTestingHelper } from '../../constants';
import { SpecificationFile } from '../validation';
import * as messages from '../../messages';

const testingVariables = new ContextTestingHelper();

Expand Down Expand Up @@ -42,22 +43,22 @@ describe('useContextFile().addContext ', () => {
testingVariables.deleteDummyContextFile();
const { response, error } = useContextFile().addContext('home', new SpecificationFile('./test/specification.yml'));
expect(error).toBeUndefined();
expect(response).toMatch('New context added');
expect(response).toMatch(messages.NEW_CONTEXT_ADDED('home'));
testingVariables.deleteDummyContextFile();
});

test('should save when context file is present', () => {
testingVariables.createDummyContextFile();
const { response, error } = useContextFile().addContext('home', new SpecificationFile('./test/specification.yml'));
expect(error).toBeUndefined();
expect(response).toMatch('New context added');
expect(response).toMatch(messages.NEW_CONTEXT_ADDED('home'));
});

test('Auto set current when when adding context for the fist time', () => {
testingVariables.deleteDummyContextFile();
const { response, error } = useContextFile().addContext('home', new SpecificationFile('./test/specification.yml'));
expect(error).toBeUndefined();
expect(response).toMatch('New context added');
expect(response).toMatch(messages.NEW_CONTEXT_ADDED('home'));
const { response: res, error: err } = useContextFile().current();
expect(err).toBeUndefined();
expect(res?.key).toMatch('home');
Expand All @@ -77,7 +78,7 @@ describe('useContextFile.updateCurrent ', () => {
testingVariables.createDummyContextFile();
const { response, error } = useContextFile().setCurrent('name');
expect(response).toBeUndefined();
expect(error instanceof KeyNotFoundError).toBeTruthy();
expect(error instanceof ContextNotFoundError).toBeTruthy();
});

test('Should update the current context', () => {
Expand All @@ -93,13 +94,13 @@ describe('useContextFile().deleteContext ', () => {
testingVariables.createDummyContextFile();
const { response, error } = useContextFile().deleteContext('code');
expect(error).toBeUndefined();
expect(response).toMatch('context deleted successfully');
expect(response).toMatch(messages.CONTEXT_DELETED);
});

test('return error if deleting current context', () => {
testingVariables.createDummyContextFile();
const { response, error } = useContextFile().deleteContext('home');
expect(response).toMatch('context deleted successfully');
expect(response).toMatch(messages.CONTEXT_DELETED);
expect(error).toBeUndefined();
});
});
Expand Down
13 changes: 7 additions & 6 deletions src/hooks/context/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Context, ContextFileNotFoundError, ContextNotFoundError, MissingCurrent
import { ContextService } from './contextService';
import { container } from 'tsyringe';
import { SpecificationFile } from '../validation';
import * as messages from '../../messages';

export type Result = {
response?: any,
Expand Down Expand Up @@ -36,15 +37,15 @@ export const useContextFile = (): any => {
const ctx = contextService.loadContextFile();
const updatedContext = contextService.addContext(ctx, key, specFile);
contextService.save(updatedContext);
const response = 'New context added';
const response = messages.NEW_CONTEXT_ADDED(key);
return { response };
} catch (error) {
if (error instanceof ContextFileNotFoundError) {
const context: Context = { current: '', store: {} };
try {
const newContext = contextService.addContext(context, key, specFile);
contextService.save(contextService.updateCurrent(newContext, key));
const response = 'New context added';
const response = messages.NEW_CONTEXT_ADDED(key);
return { response };
} catch (error) {
return { error };
Expand All @@ -69,11 +70,11 @@ export const useContextFile = (): any => {
const ctx = contextService.loadContextFile();
if (Object.keys(ctx.store).length === 1) {
contextService.deleteContextFile();
return { response: 'context deleted successfully' };
return { response: messages.CONTEXT_DELETED };
}
const updatedContext = contextService.deleteContext(ctx, key);
contextService.save(updatedContext);
const response = 'context deleted successfully';
const response = messages.CONTEXT_DELETED;
return { response };
} catch (error) {
return { error };
Expand Down Expand Up @@ -102,7 +103,7 @@ export const useContextFile = (): any => {
try {
const ctx = contextService.loadContextFile();
const ctxValue = ctx.store[String(key)];
if (!ctxValue) { throw new ContextNotFoundError(); }
if (!ctxValue) { throw new ContextNotFoundError(key); }
const response = new SpecificationFile(ctxValue);
return { response };
} catch (error) {
Expand Down Expand Up @@ -136,7 +137,7 @@ export const useSpecfile = (flags: useSpecFileInput): useSpecFileOutput => {

if (flags.context) {
const ctxFile = ctx.store[flags.context];
if (!ctxFile) { throw new ContextNotFoundError(); }
if (!ctxFile) { throw new ContextNotFoundError(flags.context); }
const specFile = new SpecificationFile(ctxFile);
return { specFile };
}
Expand Down
Loading

0 comments on commit 14e94d8

Please sign in to comment.