diff --git a/README.md b/README.md index f6c3cc776fb..b8989f05d90 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,11 @@ context Manage contexts ``` USAGE -asyncapi validate [options] +asyncapi validate [options] OPTIONS -h, --help display help for command --f, --file Path of the asyncapi file --c, --context context name to use -w, --watch Enable Watch Mode (not implemented yet) ``` diff --git a/src/CommandsRouter.tsx b/src/CommandsRouter.tsx index d6bc6ea9899..4fd8a5a02b8 100644 --- a/src/CommandsRouter.tsx +++ b/src/CommandsRouter.tsx @@ -5,7 +5,7 @@ import { CliInput } from './CliModels'; import { CommandName, HelpMessageBuilder } from './help-message'; const commandsDictionary = (cliInput: CliInput): Record => ({ - validate: , + validate: , context: contextRouter(cliInput) }); diff --git a/src/components/Validate/Validate.tsx b/src/components/Validate/Validate.tsx index 6d24c2b72c0..cb6736e0b29 100644 --- a/src/components/Validate/Validate.tsx +++ b/src/components/Validate/Validate.tsx @@ -1,17 +1,19 @@ import React, { useEffect, useState } from 'react'; import { Newline, Text } from 'ink'; -import { useSpecfile } from '../../hooks/context'; +import { loadSpecFileForValidation } from '../../hooks/context'; import { UseValidateResponse } from '../../hooks/validation/models'; -import { SpecificationFile, useValidate } from '../../hooks/validation'; +import { useValidate } from '../../hooks/validation'; import { Options } from '../../CliModels'; interface ValidateInput { - options: Options; + options: Options, + parameter?: string | undefined } -const Validate: React.FunctionComponent = ({ options }) => { - const { specFile, error } = useSpecfile({ context: options.context, file: options.file }); +const Validate: React.FunctionComponent = ({ options, parameter }) => { + const {specFile, error} = loadSpecFileForValidation(parameter); + if (error) { return {error.message}; } @@ -21,7 +23,7 @@ const Validate: React.FunctionComponent = ({ options }) => { } const validationInput = { - file: new SpecificationFile(specFile.getSpecificationName()), + file: specFile, watchMode: options.watch, }; diff --git a/src/help-message.tsx b/src/help-message.tsx index 50757cc81d5..8351c2d7514 100644 --- a/src/help-message.tsx +++ b/src/help-message.tsx @@ -7,7 +7,10 @@ export type CommandName = typeof CommandList[number] export type Command = { [name in CommandName]: { - usage: string; + usage: { + command?: string, + options?: string + }; shortDescription: string; longDescription?: string; flags: string[]; @@ -28,17 +31,17 @@ export class HelpMessage { readonly commands: Command = { validate: { - usage: 'asyncapi validate [options]', + usage: { + command: '' + }, shortDescription: 'Validate asyncapi file', flags: [ this.helpFlag, - '-f, --file Path of the AsyncAPI file', - '-c, --context Context to use', '-w, --watch Enable watch mode (not implemented yet)' ] }, context: { - usage: 'asyncapi context [options] [command]', + usage: {}, 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], @@ -103,10 +106,10 @@ export class HelpMessageBuilder { USAGE - {commandHelpObject.usage.split(' ')[0]}{' '} - {commandHelpObject.usage.split(' ')[1]}{' '} - {commandHelpObject.usage.split(' ')[2]}{' '} - {commandHelpObject.usage.split(' ')[3]} + asyncapi{' '} + {command}{' '} + {commandHelpObject.usage.command || '[command]'}{' '} + {commandHelpObject.usage.options || '[options]'} diff --git a/src/hooks/context/contextService.ts b/src/hooks/context/contextService.ts index b5e17324de6..76ef5e52944 100644 --- a/src/hooks/context/contextService.ts +++ b/src/hooks/context/contextService.ts @@ -12,6 +12,11 @@ export class ContextService { return JSON.parse(fs.readFileSync(CONTEXTFILE_PATH, 'utf-8')) as Context; } + isContext(): boolean { + if (!fs.existsSync(CONTEXTFILE_PATH)) {return true;} + return false; + } + deleteContextFile(): void { if (fs.existsSync(CONTEXTFILE_PATH)) {fs.unlinkSync(CONTEXTFILE_PATH);} } diff --git a/src/hooks/context/hooks.tsx b/src/hooks/context/hooks.tsx index 86a5c513130..ef3d57329b1 100644 --- a/src/hooks/context/hooks.tsx +++ b/src/hooks/context/hooks.tsx @@ -1,3 +1,4 @@ +/* eslint-disable security/detect-object-injection */ import { Context, ContextFileNotFoundError, ContextNotFoundError, MissingCurrentContextError, NoSpecPathFoundError } from './models'; import { ContextService } from './contextService'; import { container } from 'tsyringe'; @@ -172,3 +173,59 @@ const autoDetectSpecFileInWorkingDir = (specFile: string | undefined, command: s if (typeof specFile === 'undefined') { throw new NoSpecPathFoundError(command); } return new SpecificationFile(specFile); }; + +const loadCurrentContext = (contextService: ContextService): { currentContext?: string | undefined, error?: Error } => { + let currentContext: string | undefined; + try { + const ctx = contextService.loadContextFile(); + currentContext = ctx.store[ctx.current]; + return { currentContext }; + } catch (error) { + return { error }; + } +}; + +export const loadSpecFileForValidation = (input: string | undefined): useSpecFileOutput => { + const contextService: ContextService = container.resolve(ContextService); + const cliService: CLIService = container.resolve(CLIService); + let specFile: SpecificationFile; + + try { + if (!input) { + const { currentContext, error } = loadCurrentContext(contextService); + if (error) { + specFile = autoDetectSpecFileInWorkingDir(contextService.autoDetectSpecFile(), cliService.command()); + return { specFile }; + } + if (currentContext) { + specFile = new SpecificationFile(currentContext); + return { specFile }; + } + throw new NoSpecPathFoundError(cliService.command()); + } + + specFile = new SpecificationFile(input); + + if (!specFile.isNotValid()) { + return {specFile}; + } + const ctx = contextService.loadContextFile(); + if (Object.keys(ctx.store).includes(input)) { + specFile = new SpecificationFile(ctx.store[input] as string); + return { specFile }; + } + + throw new NoSpecPathFoundError(cliService.command()); + } catch (error) { + if (error instanceof ContextFileNotFoundError) { + try { + specFile = autoDetectSpecFileInWorkingDir(contextService.autoDetectSpecFile(), cliService.command()); + return { specFile }; + } catch (error) { + return { error }; + } + } + + return { error }; + } +}; diff --git a/src/messages.tsx b/src/messages.tsx index 0fa1d77864d..2c2ff5372c4 100644 --- a/src/messages.tsx +++ b/src/messages.tsx @@ -7,7 +7,7 @@ export const MISSING_CURRENT_CONTEXT = 'No context is set as current, please set 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: asyncapi context use ${contextName}\nYou can use this context when needed with --context flag: asyncapi validate --context ${contextName}`; +export const NEW_CONTEXT_ADDED = (contextName: string) => `New context added.\n\nYou can set it as your current context: asyncapi context use ${contextName}\nYou can use this context when needed by passing ${contextName} as a parameter: asyncapi valiate ${contextName}`; export const CONTEXT_DELETED = 'context deleted successfully'; @@ -16,7 +16,7 @@ export const ValidationMessage = (filePath: string) => ({ message: () => `File: ${filePath} successfully validated!` }); -export const NO_SPEC_FOUND = (command: string) => `${FALLBACK_MESSAGES[command]}\n\nThese are your options to specify in the CLI what AsyncAPI file should be used:\n- You can use --file flag to provide a path to the AsyncAPI file: asyncapi ${command} --file path/to/file/asyncapi.yml\n- You can use --context flag to provide a name of the context that points to your AsyncAPI file: asyncapi ${command} --context mycontext\n- In case you did not specify a context that you want to use, the CLI checks if there is a default context and uses it. To set default context run: asyncapi context use mycontext\n- In case you did not provide any reference to AsyncAPI file and there is no default context, the CLI detects if in your current working directory you have files like asyncapi.json, asyncapi.yaml, asyncapi.yml. Just rename your file accordingly.`; +export const NO_SPEC_FOUND = (command: string) => `${FALLBACK_MESSAGES[command]}\n\nThese are your options to specify in the CLI what AsyncAPI file should be used:\n- You can provide a path to the AsyncAPI file: asyncapi ${command} path/to/file/asyncapi.yml\n- You can also pass a saved context that points to your AsyncAPI file: asyncapi ${command} mycontext\n- In case you did not specify a context that you want to use, the CLI checks if there is a default context and uses it. To set default context run: asyncapi context use mycontext\n- In case you did not provide any reference to AsyncAPI file and there is no default context, the CLI detects if in your current working directory you have files like asyncapi.json, asyncapi.yaml, asyncapi.yml. Just rename your file accordingly.`; export const FALLBACK_MESSAGES: {[name: string]: string} = { validate: 'Unable to perform validation. Specify what AsyncAPI file should be validated.'