From f17d52d11859dd6c05376f027e9a5de0c0e7f63b Mon Sep 17 00:00:00 2001 From: catosaurusrex2003 Date: Sat, 19 Oct 2024 13:48:38 +0530 Subject: [PATCH] added a format command its helpers functions. --- .prettierrc | 3 + package-lock.json | 10 ++- src/commands/format.ts | 121 ++++++++++++++++++++++++++ src/core/errors/specification-file.ts | 7 ++ src/core/flags/format.flags.ts | 22 +++++ src/core/models/SpecificationFile.ts | 37 ++++++++ 6 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 .prettierrc create mode 100644 src/commands/format.ts create mode 100644 src/core/flags/format.flags.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000000..dc2fb828f03 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2dc99e0ba33..6502b759600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8951,7 +8951,8 @@ "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true }, "node_modules/are-we-there-yet": { "version": "3.0.1", @@ -9890,6 +9891,7 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, "funding": [ { "type": "github", @@ -31343,7 +31345,8 @@ "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true }, "are-we-there-yet": { "version": "3.0.1", @@ -32038,7 +32041,8 @@ "ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true }, "classcat": { "version": "5.0.5", diff --git a/src/commands/format.ts b/src/commands/format.ts new file mode 100644 index 00000000000..d68e12acba5 --- /dev/null +++ b/src/commands/format.ts @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { promises as fPromises } from 'fs'; +import { Args } from '@oclif/core'; +import Command from '../core/base'; + +import { + convertToJSON, + convertToYaml, + load, + retrieveFileFormat, +} from '../core/models/SpecificationFile'; +import { SpecificationWrongFileFormat } from '../core/errors/specification-file'; +import { cyan, green } from 'picocolors'; +import { + convertFormatFlags, + fileFormat, +} from '../core/flags/format.flags'; + +export default class Convert extends Command { + static specFile: any; + static metricsMetadata: any = {}; + static description = + 'Convert asyncapi documents from any format to yaml, yml or JSON'; + + static flags = convertFormatFlags(); + + static args = { + 'spec-file': Args.string({ + description: 'spec path, url, or context-name', + required: false, + }), + }; + + async run() { + const { args, flags } = await this.parse(Convert); + const filePath = args['spec-file']; + const outputFileFormat = flags['format'] as fileFormat; + let convertedFile; + try { + this.specFile = await load(filePath); + // eslint-disable-next-line sonarjs/no-duplicate-string + this.metricsMetadata.to_version = flags['target-version']; + + const ff = retrieveFileFormat(this.specFile.text()); + const isSpecFileJson = ff == 'json'; + const isSpecFileYaml = ff == 'yaml'; + + if (!isSpecFileJson && !isSpecFileYaml) { + throw new SpecificationWrongFileFormat(filePath); + } + + convertedFile = this.handleConversion( + isSpecFileJson, + isSpecFileYaml, + outputFileFormat, + ); + + if (!convertedFile) return; + await this.handleOutput(flags.output, convertedFile, outputFileFormat); + } catch (err) { + this.error(err as Error); + } + } + + private handleConversion( + isSpecFileJson: boolean, + isSpecFileYaml: boolean, + outputFileFormat: fileFormat, + ): string | undefined { + const text = this.specFile?.text(); + if (isSpecFileJson && text) { + if (outputFileFormat == 'json') { + throw new Error(`Your document is already a ${cyan('JSON')}`); + } + return convertToYaml(text); + } + if (isSpecFileYaml && text) { + if (outputFileFormat == 'yaml' || outputFileFormat == 'yml') { + throw new Error(`Your document is already a ${cyan('YAML')}`); + } + return convertToJSON(text); + } + } + + private async handleOutput( + outputPath: string | undefined, + formattedFile: string, + outputFileFormat: fileFormat, + ) { + if (outputPath) { + outputPath = this.removeExtensionFromOutputPath(outputPath); + try { + const finalFileName = `${outputPath}.${outputFileFormat}`; + await fPromises.writeFile(finalFileName, formattedFile, { + encoding: 'utf8', + }); + this.log(`converted to ${outputFileFormat} at ${green(finalFileName)}`); + } catch (err) {} + } else { + this.log(formattedFile); + } + } + + private removeExtensionFromOutputPath(filename: string): string { + // Removes the extension from a filename if it is .json, .yaml, or .yml + // this is so that we can remove the provided extension name in the -o flag and + // apply our own extension name according to the content of the file + const validExtensions = ['json', 'yaml', 'yml']; + + const parts = filename.split('.'); + + if (parts.length > 1) { + const extension = parts.pop()?.toLowerCase(); + if (extension && validExtensions.includes(extension)) { + return parts.join('.'); + } + } + + return filename; + } +} diff --git a/src/core/errors/specification-file.ts b/src/core/errors/specification-file.ts index 9df74db9218..e7f9e134137 100644 --- a/src/core/errors/specification-file.ts +++ b/src/core/errors/specification-file.ts @@ -17,6 +17,13 @@ export class SpecificationFileNotFound extends SpecificationFileError { } } +export class SpecificationWrongFileFormat extends SpecificationFileError { + constructor(filePath?: string) { + super(); + this.message = `File ${filePath} is not of correct format.`; + } +} + export class SpecificationURLNotFound extends SpecificationFileError { constructor(URL: string) { super(); diff --git a/src/core/flags/format.flags.ts b/src/core/flags/format.flags.ts new file mode 100644 index 00000000000..7b6e0a6ea08 --- /dev/null +++ b/src/core/flags/format.flags.ts @@ -0,0 +1,22 @@ +import { Flags } from '@oclif/core'; + +export type fileFormat = 'yaml' | 'yml' | 'json'; + +const availFileFormats: fileFormat[] = ['yaml', 'yml', 'json']; + +export const convertFormatFlags = () => { + return { + help: Flags.help({ char: 'h' }), + output: Flags.string({ + char: 'o', + description: 'path to the file where the result is saved', + }), + format: Flags.string({ + char: 'f', + description: 'Specify the format to convert to', + options: availFileFormats, + required: true, + default: 'json', + }), + }; +}; diff --git a/src/core/models/SpecificationFile.ts b/src/core/models/SpecificationFile.ts index 30051beb80e..be3681e6158 100644 --- a/src/core/models/SpecificationFile.ts +++ b/src/core/models/SpecificationFile.ts @@ -6,6 +6,7 @@ import yaml from 'js-yaml'; import { loadContext } from './Context'; import { ErrorLoadingSpec } from '../errors/specification-file'; import { MissingContextFileError } from '../errors/context-error'; +import { fileFormat } from 'core/flags/format.flags'; const { readFile, lstat } = fs; const allowedFileNames: string[] = [ @@ -222,3 +223,39 @@ async function detectSpecFile(): Promise { })); return existingFileNames.find(filename => filename !== undefined); } + +export function retrieveFileFormat(content: string): fileFormat | undefined { + try { + if (content.trimStart()[0] === '{') { + JSON.parse(content); + return 'json'; + } + // below yaml.load is not a definitive way to determine if a file is yaml or not. + // it is able to load .txt text files also. + yaml.load(content); + return 'yaml'; + } catch (err) { + return undefined; + } +} + +export function convertToYaml(spec: string) { + try { + // JS object -> YAML string + const jsonContent = yaml.load(spec); + return yaml.dump(jsonContent); + } catch (err) { + console.error(err); + } +} + +export function convertToJSON(spec: string) { + try { + // JSON or YAML String -> JS object + const jsonContent = yaml.load(spec); + // JS Object -> pretty JSON string + return JSON.stringify(jsonContent, null, 2); + } catch (err) { + console.error(err); + } +}