diff --git a/src/components/Context/context.spec.tsx b/src/components/Context/context.spec.tsx index 1c4592e6e1..7da7eb9fb0 100644 --- a/src/components/Context/context.spec.tsx +++ b/src/components/Context/context.spec.tsx @@ -2,6 +2,7 @@ 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(); @@ -9,7 +10,7 @@ describe('listing contexts', () => { test('should render error when no context file found', () => { testing.deleteDummyContextFile(); const { lastFrame } = render(); - expect(lastFrame()).toMatch('No contexts saved yet.'); + expect(lastFrame()).toMatch(messages.NO_CONTEXTS_SAVED); }); test('Should render the context list', () => { @@ -27,7 +28,7 @@ describe('rendering current context', () => { testing.deleteDummyContextFile(); const { lastFrame } = render(); const message = lastFrame(); - expect(message).toMatch('No contexts saved yet.'); + expect(message).toMatch(messages.NO_CONTEXTS_SAVED); }); test('showing current context ', () => { @@ -41,7 +42,7 @@ describe('AddContext ', () => { test('should return message', () => { testing.createDummyContextFile(); const { lastFrame } = render(); - expect(lastFrame()).toMatch('New context added'); + expect(lastFrame()).toMatch(messages.NEW_CONTEXT_ADDED('home')); }); }); @@ -49,7 +50,7 @@ describe('SetContext ', () => { test('Should render error message is key is not in store', () => { testing.createDummyContextFile(); const { lastFrame } = render(); - 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', () => { diff --git a/src/components/Context/contexterror.tsx b/src/components/Context/contexterror.tsx index db5c854522..0c4cca4f86 100644 --- a/src/components/Context/contexterror.tsx +++ b/src/components/Context/contexterror.tsx @@ -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 No contexts saved yet.; - } - - if (error instanceof KeyNotFoundError) { - return The context you are trying to use is not present; - } - - if (error instanceof DeletingCurrentContextError) { - return You are trying to delete a context that is set as current.; - } - return {error.message}; }; diff --git a/src/help-message.spec.ts b/src/help-message.spec.ts new file mode 100644 index 0000000000..76ccf227bf --- /dev/null +++ b/src/help-message.spec.ts @@ -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'); + }); +}); diff --git a/src/help-message.ts b/src/help-message.ts new file mode 100644 index 0000000000..e620d05b66 --- /dev/null +++ b/src/help-message.ts @@ -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 Path of the AsyncAPI file', + '-c, --context 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 set given context as default/current', + 'add add/update context', + 'remove 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; + } +} diff --git a/src/hooks/context/contextService.ts b/src/hooks/context/contextService.ts index fe45399c4b..b5e17324de 100644 --- a/src/hooks/context/contextService.ts +++ b/src/hooks/context/contextService.ts @@ -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'; @@ -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; } @@ -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; } diff --git a/src/hooks/context/hook.spec.ts b/src/hooks/context/hook.spec.ts index 6546cb64da..a786a99c25 100644 --- a/src/hooks/context/hook.spec.ts +++ b/src/hooks/context/hook.spec.ts @@ -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(); @@ -42,7 +43,7 @@ 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(); }); @@ -50,14 +51,14 @@ describe('useContextFile().addContext ', () => { 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'); @@ -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', () => { @@ -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(); }); }); diff --git a/src/hooks/context/hooks.tsx b/src/hooks/context/hooks.tsx index 9dd54a8a3b..1fa8f0b096 100644 --- a/src/hooks/context/hooks.tsx +++ b/src/hooks/context/hooks.tsx @@ -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, @@ -36,7 +37,7 @@ 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) { @@ -44,7 +45,7 @@ export const useContextFile = (): any => { 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 }; @@ -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 }; @@ -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) { @@ -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 }; } diff --git a/src/hooks/context/models.ts b/src/hooks/context/models.ts index 06e728cbaf..46f1a46212 100644 --- a/src/hooks/context/models.ts +++ b/src/hooks/context/models.ts @@ -1,55 +1,42 @@ +import * as messages from '../../messages'; export interface Context { - current: string, - store: { - [name: string]: string - } + current: string, + store: { + [name: string]: string + } } export class SpecFileNotFoundError extends Error { - constructor() { + constructor(specPath: string) { super(); - this.message = 'specification file not found in that path.'; + this.message = messages.ValidationMessage(specPath).error(); } } export class ContextFileNotFoundError extends Error { constructor() { super(); - this.message = 'No contexts saved yet, run asyncapi --help to know more.'; + this.message = messages.NO_CONTEXTS_SAVED; } } export class ContextNotFoundError extends Error { - constructor() { - super(); - this.message = 'This context key does not exist.'; - } -} - -export class KeyNotFoundError extends Error { - constructor() { - super(); - this.message = 'Key not found.'; - } -} - -export class DeletingCurrentContextError extends Error { - constructor() { + constructor(contextName: string) { super(); - this.message = 'You are trying to delete a context that is currently in use.'; + this.message = messages.CONTEXT_NOT_FOUND(contextName); } } export class MissingCurrentContextError extends Error { constructor() { super(); - this.message = 'No context is set as current, please set a current context.'; + this.message = messages.MISSING_CURRENT_CONTEXT; } } export class MissingArgumentstError extends Error { constructor() { super(); - this.message = 'Missing arguments'; + this.message = messages.MISSING_ARGUMENTS; } } diff --git a/src/hooks/validation/hook.spec.ts b/src/hooks/validation/hook.spec.ts index 4422aac7e3..b21bd2c1a3 100644 --- a/src/hooks/validation/hook.spec.ts +++ b/src/hooks/validation/hook.spec.ts @@ -2,6 +2,7 @@ import { container } from 'tsyringe'; import { SpecificationFile, ValidationInput, ValidationResponse } from './models'; import { ValidationService } from './ValidationService'; import { useValidate } from './hook'; +import {ValidationMessage} from '../../messages'; function ValidationServiceMock() { return { @@ -39,7 +40,8 @@ describe('UseValidate should', () => { expect(useValidateResponse.success).toBeFalsy(); expect(useValidateResponse.message).toEqual(''); - expect(useValidateResponse.errors[0]).toBe(`File: ${invalidFileValidationInput.file.getSpecificationName()} does not exists or is not a file!`); + + expect(useValidateResponse.errors[0]).toBe(ValidationMessage(invalidFileValidationInput.file.getSpecificationName()).error()); }); test('return success when the validation is correct', async () => { @@ -47,7 +49,7 @@ describe('UseValidate should', () => { const useValidateResponse = await useValidate().validate(fileThatExistsValidationInput); expect(useValidateResponse.success).toBeTruthy(); - expect(useValidateResponse.message).toEqual(`File: ${fileThatExistsValidationInput.file.getSpecificationName()} successfully validated!`); + expect(useValidateResponse.message).toEqual(ValidationMessage(fileThatExistsValidationInput.file.getSpecificationName()).message()); }); test('return validation service errors when the validation has failed', async () => { diff --git a/src/hooks/validation/hook.tsx b/src/hooks/validation/hook.tsx index 8523b2c5e9..b467044bf2 100644 --- a/src/hooks/validation/hook.tsx +++ b/src/hooks/validation/hook.tsx @@ -2,6 +2,7 @@ import { container } from 'tsyringe'; import { UseValidateResponse, ValidationInput, ValidationResponse } from './models'; import { ValidationService } from './ValidationService'; +import { ValidationMessage } from '../../messages'; export function useValidate() { const validationService: ValidationService = container.resolve(ValidationService); @@ -10,11 +11,11 @@ export function useValidate() { async validate({ file }: ValidationInput): Promise { try { if (file.isNotValid()) { - return Promise.resolve(UseValidateResponse.withError(`File: ${file.getSpecificationName()} does not exists or is not a file!`)); + return Promise.resolve(UseValidateResponse.withError(ValidationMessage(file.getSpecificationName()).error())); } const response: ValidationResponse = await validationService.execute(file); if (response.success) { - return Promise.resolve(UseValidateResponse.withMessage(`File: ${file.getSpecificationName()} successfully validated!`)); + return Promise.resolve(UseValidateResponse.withMessage(ValidationMessage(file.getSpecificationName()).message())); } return Promise.resolve(UseValidateResponse.withErrors(response.errors)); } catch (error) { diff --git a/src/messages.tsx b/src/messages.tsx new file mode 100644 index 0000000000..199689ae98 --- /dev/null +++ b/src/messages.tsx @@ -0,0 +1,16 @@ +export const NO_CONTEXTS_SAVED = 'No contexts saved yet, run asyncapi --help to learn more'; + +export const CONTEXT_NOT_FOUND = (contextName: string) => `Context ${contextName} does not exists.`; + +export const MISSING_CURRENT_CONTEXT = 'No context is set as current, please set a current context.'; + +export const MISSING_ARGUMENTS = 'Missing arguments.'; + +export const NEW_CONTEXT_ADDED = (contextName: string) => `New context added.\n\nYou can set it as your current context:\n asyncapi context use ${contextName}\nYou can use this context when needed with --context flag: asyncapi validate --context ${contextName}`; + +export const CONTEXT_DELETED = 'context deleted successfully'; + +export const ValidationMessage = (filePath: string) => ({ + error: () => `File: ${filePath} does not exists or is not a file!`, + message: () => `File: ${filePath} successfully validated!` +});