Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: format command to convert asyncapi document to multiple format #1549

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ node_modules
test.asyncapi-cli
asyncapi.json
test/fixtures/minimaltemplate/__transpiled
test/fixtures/specification-conv.yml
test/fixtures/specification-conv.yaml
test/fixtures/specification-conv.json
.vscode

/action/
Expand Down
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
18,148 changes: 18,148 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions src/commands/format.ts
Original file line number Diff line number Diff line change
@@ -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);
const finalFileName = `${outputPath}.${outputFileFormat}`;
await fPromises.writeFile(finalFileName, formattedFile, {
encoding: 'utf8',
});
this.log(
`succesfully formatted to ${outputFileFormat} at ${green(finalFileName)} ✅`,
);
} else {
this.log(formattedFile);
this.log(`succesfully logged after formatting to ${outputFileFormat} ✅`);
}
}

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;
}
}
7 changes: 7 additions & 0 deletions src/core/errors/specification-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions src/core/flags/format.flags.ts
Original file line number Diff line number Diff line change
@@ -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' }),
format: Flags.string({
char: 'f',
description: 'Specify the format to convert to',
options: availFileFormats,
required: true,
default: 'json',
}),
output: Flags.string({
char: 'o',
description: 'path to the file where the result is saved',
}),
};
};
37 changes: 37 additions & 0 deletions src/core/models/SpecificationFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -222,3 +223,39 @@ async function detectSpecFile(): Promise<string | undefined> {
}));
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);
}
}
Loading
Loading