diff --git a/package-lock.json b/package-lock.json index 46a7895d43c..49751dfe7c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,8 @@ "strip-ansi": "^6.0.0", "unzipper": "^0.10.11", "uuid": "^9.0.1", - "ws": "^8.2.3" + "ws": "^8.2.3", + "yaml": "^2.6.1" }, "bin": { "asyncapi": "bin/run_bin" @@ -24708,9 +24709,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index fee39b78d78..90626e1e8bd 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "strip-ansi": "^6.0.0", "unzipper": "^0.10.11", "uuid": "^9.0.1", - "ws": "^8.2.3" + "ws": "^8.2.3", + "yaml": "^2.6.1" }, "devDependencies": { "@asyncapi/minimaltemplate": "./test/fixtures/minimaltemplate", diff --git a/src/commands/pretty.ts b/src/commands/pretty.ts new file mode 100644 index 00000000000..260f61c6b45 --- /dev/null +++ b/src/commands/pretty.ts @@ -0,0 +1,67 @@ +import { Args } from '@oclif/core'; +import { promises as fs } from 'fs'; +import * as yaml from 'yaml'; +import Command from '../core/base'; +import { load , retrieveFileFormat} from '../core/models/SpecificationFile'; +import { ValidationError } from '../core/errors/validation-error'; +import { prettyFlags } from '../core/flags/pretty.flags'; + +export default class Pretty extends Command { + static readonly description = 'Format AsyncAPI specification file'; + + static readonly examples = [ + 'asyncapi pretty ./asyncapi.yaml', + 'asyncapi pretty ./asyncapi.yaml --output formatted-asyncapi.yaml', + ]; + + static readonly flags = prettyFlags(); + + static readonly args = { + 'spec-file': Args.string({description: 'spec path, url, or context-name', required: true}), + }; + + async run() { + const { args, flags } = await this.parse(Pretty); + const filePath = args['spec-file']; + const outputPath = flags.output; + + try { + this.specFile = await load(filePath); + } catch (err) { + this.error( + new ValidationError({ + type: 'invalid-file', + filepath: filePath, + }) + ); + } + + const content = this.specFile.text(); + let formatted: string; + + try { + const fileFormat = retrieveFileFormat(this.specFile.text()); + if (fileFormat === 'yaml' || fileFormat === 'yml') { + const yamlDoc = yaml.parseDocument(content); + formatted = yamlDoc.toString({ + lineWidth: 0, + }); + } else if (fileFormat === 'json') { + const jsonObj = JSON.parse(content); + formatted = JSON.stringify(jsonObj, null, 2); + } else { + throw new Error('Unsupported file format'); + } + } catch (err) { + this.error(`Error formatting file: ${err}`); + } + + if (outputPath) { + await fs.writeFile(outputPath, formatted, 'utf8'); + this.log(`Asyncapi document has been beautified ${outputPath}`); + } else { + await fs.writeFile(filePath, formatted, 'utf8'); + this.log(`Asyncapi document ${filePath} has been beautified in-place.`); + } + } +} diff --git a/src/core/flags/pretty.flags.ts b/src/core/flags/pretty.flags.ts new file mode 100644 index 00000000000..102656eb739 --- /dev/null +++ b/src/core/flags/pretty.flags.ts @@ -0,0 +1,10 @@ +import { Flags } from '@oclif/core'; + +export const prettyFlags = () => { + return { + output: Flags.string({ + char: 'o', + description: 'Output file path', + }), + }; +}; diff --git a/test/fixtures/asyncapiValid_v1.yml b/test/fixtures/asyncapiValid_v1.yml new file mode 100644 index 00000000000..7c7fbed0340 --- /dev/null +++ b/test/fixtures/asyncapiValid_v1.yml @@ -0,0 +1,36 @@ +asyncapi: "2.1.0" +info: + title: Streetlights API + version: "1.0.0" + description: | + The Smartylighting Streetlights API allows you + to remotely manage the city lights. + license: + name: Apache 2.0 + url: "https://www.apache.org/licenses/LICENSE-2.0" +servers: + mosquitto: + url: mqtt://test.mosquitto.org + protocol: mqtt +channels: + light/measured: + publish: + summary: Inform about environmental lighting conditions for a particular streetlight. + operationId: onLightMeasured + message: + name: LightMeasured + payload: + type: object + properties: + id: + type: integer + minimum: 0 + description: Id of the streetlight. + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. diff --git a/test/fixtures/badFormatAsyncapi.json b/test/fixtures/badFormatAsyncapi.json new file mode 100644 index 00000000000..db8b7cae44c --- /dev/null +++ b/test/fixtures/badFormatAsyncapi.json @@ -0,0 +1,38 @@ +{ + "asyncapi": "2.2.0", + "info": { + "title": "Account Service", + "version": "1.0.0", + "description": + "This service is in charge of processing user signups" + }, + "channels": { + "user/signedup": { + "subscribe": { + "message": { + "$ref": "#/components/messages/UserSignedUp" + } + } + } + }, + "components": { + "messages": { + "UserSignedUp": { + "payload": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "description": "Name of the user" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email of the user" + } + } + } + } + } + } + } \ No newline at end of file diff --git a/test/integration/pretty.test.ts b/test/integration/pretty.test.ts new file mode 100644 index 00000000000..29dc57244ea --- /dev/null +++ b/test/integration/pretty.test.ts @@ -0,0 +1,64 @@ +import { test } from '@oclif/test'; +import TestHelper, { createMockServer, stopMockServer } from '../helpers'; +import { expect } from '@oclif/test'; + +const testHelper = new TestHelper(); +const badFormatPath = './test/fixtures/asyncapi_v1.yml'; +const validFormatPath = './test/fixtures/asyncapiValid_v1.yml'; +const badFormatPathJson = './test/fixtures/badFormatAsyncapi.json'; + +describe('pretty', () => { + describe('with file paths', () => { + beforeEach(() => { + testHelper.createDummyContextFile(); + }); + + afterEach(() => { + testHelper.deleteDummyContextFile(); + }); + + before(() => { + createMockServer(); + }); + + after(() => { + stopMockServer(); + }); + + test + .stderr() + .stdout() + .command(['pretty', badFormatPath]) + .it('should log the information file has been beautified', (ctx, done) => { + expect(ctx.stdout).to.contain( + `Asyncapi document ${badFormatPath} has been beautified in-place`, + ); + expect(ctx.stderr).to.equal(''); + done(); + }); + + test + .stderr() + .stdout() + .command(['pretty', badFormatPath ,'-o', validFormatPath]) + .it('should log the information file has been beautified', (ctx, done) => { + expect(ctx.stdout).to.contain( + `Asyncapi document has been beautified ${validFormatPath}`, + ); + expect(ctx.stderr).to.equal(''); + done(); + }); + + test + .stderr() + .stdout() + .command(['pretty', badFormatPathJson]) + .it('should log the information file has been beautified json file', (ctx, done) => { + expect(ctx.stdout).to.contain( + `Asyncapi document ${badFormatPathJson} has been beautified in-place`, + ); + expect(ctx.stderr).to.equal(''); + done(); + }); + }); +});