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

fix: add v3 checks for all commands #697

Merged
merged 8 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
354 changes: 201 additions & 153 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"strip-ansi": "^6.0.0",
"unzipper": "^0.10.11",
"wrap-ansi": "^4.0.0",
"ws": "^8.2.3"
"ws": "^8.2.3",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@asyncapi/minimaltemplate": "./test/minimaltemplate",
Expand All @@ -61,6 +62,7 @@
"@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"@types/js-yaml": "^4.0.5",
"acorn": "^8.5.0",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
Expand Down
18 changes: 14 additions & 4 deletions src/commands/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Command from '../base';
import bundle from '@asyncapi/bundler';
import { promises } from 'fs';
import path from 'path';
import { load } from '../models/SpecificationFile';
import { Specification, load } from '../models/SpecificationFile';

const { writeFile } = promises;

Expand Down Expand Up @@ -32,9 +32,19 @@ export default class Bundle extends Command {
let baseFile;
const outputFormat = path.extname(argv[0]);
const AsyncAPIFiles = await this.loadFiles(argv);

const containsAsyncAPI3 = AsyncAPIFiles.filter((file) => {
return file.isAsyncAPI3();
});
if (containsAsyncAPI3.length > 0) {
this.error('One of the files you tried to bundle is AsyncAPI v3 format, the bundle command does not support it yet, please checkout https://github.com/asyncapi/bundler/issues/133');
}

if (flags.base) {baseFile = (await load(flags.base)).text();}

const document = await bundle(AsyncAPIFiles,
const fileContents = AsyncAPIFiles.map((file) => file.text());

const document = await bundle(fileContents,
{
referenceIntoComponents: flags['reference-into-components'],
base: baseFile
Expand Down Expand Up @@ -65,11 +75,11 @@ export default class Bundle extends Command {
}
}

async loadFiles(filepaths: string[]): Promise<string[]> {
async loadFiles(filepaths: string[]): Promise<Specification[]> {
const files = [];
for (const filepath of filepaths) {
const file = await load(filepath);
files.push(file.text());
files.push(file);
}
return files;
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default class Convert extends Command {
this.log(`URL ${specFile.getFileURL()} successfully converted!`);
}
}

if (typeof convertedFile === 'object') {
convertedFileFormatted = JSON.stringify(convertedFile, null, 4);
} else {
Expand Down
13 changes: 11 additions & 2 deletions src/commands/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export default class Diff extends Command {

try {
firstDocument = await load(firstDocumentPath);

if (firstDocument.isAsyncAPI3()) {
this.error('Diff command does not support AsyncAPI v3 yet which was your first document, please checkout https://github.com/asyncapi/diff/issues/154');
}

enableWatch(watchMode, {
spec: firstDocument,
handler: this,
Expand All @@ -107,6 +112,11 @@ export default class Diff extends Command {

try {
secondDocument = await load(secondDocumentPath);

if (secondDocument.isAsyncAPI3()) {
this.error('Diff command does not support AsyncAPI v3 yet which was your second document, please checkout https://github.com/asyncapi/diff/issues/154');
}

enableWatch(watchMode, {
spec: secondDocument,
handler: this,
Expand Down Expand Up @@ -265,8 +275,7 @@ function throwOnBreakingChange(diffOutput: AsyncAPIDiff, outputFormat: string) {
const breakingChanges = diffOutput.breaking();
if (
(outputFormat === 'json' && breakingChanges.length !== 0) ||
((outputFormat === 'yaml' || outputFormat === 'yml') && breakingChanges !== '[]\n') ||
(outputFormat === 'md' && breakingChanges.length !== 0)
((outputFormat === 'yaml' || outputFormat === 'yml') && breakingChanges !== '[]\n')
) {
throw new DiffBreakingChangeError();
}
Expand Down
35 changes: 34 additions & 1 deletion src/commands/generate/fromTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ interface ParsedFlags {
mapBaseUrlToFolder: IMapBaseUrlToFlag
}

const templatesNotSupportingV3: Record<string, string> = {
'@asyncapi/minimaltemplate': 'some link', // For testing purpose
'@asyncapi/html-template': 'https://github.com/asyncapi/html-template/issues/430',
'@asyncapi/dotnet-nats-template': 'https://github.com/asyncapi/dotnet-nats-template/issues/384',
'@asyncapi/ts-nats-template': 'https://github.com/asyncapi/ts-nats-template/issues/545',
'@asyncapi/python-paho-template': 'https://github.com/asyncapi/python-paho-template/issues/189',
'@asyncapi/nodejs-ws-template': 'https://github.com/asyncapi/nodejs-ws-template/issues/294',
'@asyncapi/java-spring-cloud-stream-template': 'https://github.com/asyncapi/java-spring-cloud-stream-template/issues/336',
'@asyncapi/go-watermill-template': 'https://github.com/asyncapi/go-watermill-template/issues/243',
'@asyncapi/java-spring-template': 'https://github.com/asyncapi/java-spring-template/issues/308',
'@asyncapi/markdown-template': 'https://github.com/asyncapi/markdown-template/issues/341',
'@asyncapi/nodejs-template': 'https://github.com/asyncapi/nodejs-template/issues/215',
'@asyncapi/java-template': 'https://github.com/asyncapi/java-template/issues/118',
'@asyncapi/php-template': 'https://github.com/asyncapi/php-template/issues/191'
};

/**
* Verify that a given template support v3, if not, return the link to the issue that needs to be solved.
*/
function verifyTemplateSupportForV3(template: string) {
if (templatesNotSupportingV3[`${template}`] !== undefined) {
return templatesNotSupportingV3[`${template}`];
}
return undefined;
}

export default class Template extends Command {
static description = 'Generates whatever you want using templates compatible with AsyncAPI Generator.';

Expand Down Expand Up @@ -99,14 +125,21 @@ export default class Template extends Command {
mapBaseUrlToFolder: parsedFlags.mapBaseUrlToFolder,
disabledHooks: parsedFlags.disableHooks,
};
const asyncapiInput = (await load(asyncapi)) || (await load());

const watchTemplate = flags['watch'];
const genOption: any = {};
if (flags['map-base-url']) {
genOption.resolve = {resolve: this.getMapBaseUrlToFolderResolver(parsedFlags.mapBaseUrlToFolder)};
}

await this.generate(asyncapi, template, output, options, genOption);
if (asyncapiInput.isAsyncAPI3()) {
const v3IssueLink = verifyTemplateSupportForV3(template);
if (v3IssueLink !== undefined) {
this.error(`${template} template does not support AsyncAPI v3 documents, please checkout ${v3IssueLink}`);
}
}
Comment on lines +138 to +141
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when I pass in a custom template that does not support v3?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing, we cant expect the unexpected 🙂

Custom templates can easily be added to the list of templates that don't support v3, so all about contributing 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing, we cant expect the unexpected 🙂

Yeah, that make sense.

I was thinking that since templates have a config in the package.json they might have a parameter that could help us determine which spec versions this template supports, but now that I have checked I don't think there is anything like that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future for sure, as you folks discussed in #629, each tools should have that configured, but it would be a rush to implement it now right before v3, so this is just a temporary solution until something better comes along 🙂

await this.generate(asyncapi, template, output, options, genOption);
if (watchTemplate) {
const watcherHandler = this.watcherHandler(asyncapi, template, output, options, genOption);
await this.runWatchMode(asyncapi, template, output, watcherHandler);
Expand Down
3 changes: 3 additions & 0 deletions src/commands/generate/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ export default class Models extends Command {
const { tsModelType, tsEnumType, tsIncludeComments, tsModuleSystem, tsExportType, tsJsonBinPack, tsMarshalling, tsExampleInstance, namespace, csharpAutoImplement, csharpArrayType, csharpNewtonsoft, csharpHashcode, csharpEqual, csharpSystemJson, packageName, output } = flags;
const { language, file } = args;
const inputFile = (await load(file)) || (await load());
if (inputFile.isAsyncAPI3()) {
this.error('Generate Models command does not support AsyncAPI v3 yet, please checkout https://github.com/asyncapi/modelina/issues/1376');
}
const { document, diagnostics ,status } = await parse(this, inputFile, flags);
if (!document || status === 'invalid') {
const severityErrors = diagnostics.filter((obj) => obj.severity === 0);
Expand Down
28 changes: 21 additions & 7 deletions src/commands/optimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class Optimize extends Command {
'asyncapi optimize ./asyncapi.yaml --optimization=remove-components,reuse-components,move-to-components --no-tty',
'asyncapi optimize ./asyncapi.yaml --optimization=remove-components,reuse-components,move-to-components --output=terminal --no-tty',
];

static flags = {
help: Flags.help({ char: 'h' }),
optimization: Flags.string({char: 'p', default: Object.values(Optimizations), options: Object.values(Optimizations), multiple: true, description: 'select the type of optimizations that you want to apply.'}),
Expand All @@ -48,10 +48,24 @@ export default class Optimize extends Command {
const { args, flags } = await this.parse(Optimize); //NOSONAR
const filePath = args['spec-file'];
let specFile: Specification;
try {
specFile = await load(filePath);
} catch (err) {
this.error(
new ValidationError({
type: 'invalid-file',
filepath: filePath,
})
);
}

if (specFile.isAsyncAPI3()) {
this.error('Optimize command does not support AsyncAPI v3 yet, please checkout https://github.com/asyncapi/optimizer/issues/168');
}

let optimizer: Optimizer;
let report: Report;
try {
specFile = await load(filePath);
optimizer = new Optimizer(specFile.text());
report = await optimizer.getReport();
} catch (err) {
Expand All @@ -65,7 +79,7 @@ export default class Optimize extends Command {
this.isInteractive = !flags['no-tty'];
this.optimizations = flags.optimization as Optimizations[];
this.outputMethod = flags.output as Outputs;

if (!(report.moveToComponents?.length || report.removeComponents?.length || report.reuseComponents?.length)) {
this.log(`No optimization has been applied since ${specFile.getFilePath() ?? specFile.getFileURL()} looks optimized!`);
return;
Expand All @@ -76,13 +90,13 @@ export default class Optimize extends Command {
await this.interactiveRun(report);
}

try {
try {
const optimizedDocument = optimizer.getOptimizedDocument({rules: {
moveToComponents: this.optimizations.includes(Optimizations.MOVE_TO_COMPONETS),
removeComponents: this.optimizations.includes(Optimizations.REMOVE_COMPONENTS),
reuseComponents: this.optimizations.includes(Optimizations.REUSE_COMPONENTS)
}, output: Output.YAML});

const specPath = specFile.getFilePath();
let newPath = '';
if (specPath) {
Expand Down Expand Up @@ -149,7 +163,7 @@ export default class Optimize extends Command {
this.showOptimizations(report.reuseComponents);
choices.push({name: 'reuse components', value: Optimizations.REUSE_COMPONENTS});
}
const optimizationRes = await inquirer.prompt([{
const optimizationRes = await inquirer.prompt([{
name: 'optimization',
message: 'select the type of optimization that you want to apply:',
type: 'checkbox',
Expand All @@ -158,7 +172,7 @@ export default class Optimize extends Command {
}]);

this.optimizations = optimizationRes.optimization;

const outputRes = await inquirer.prompt([{
name: 'output',
message: 'where do you want to save the result:',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class Validate extends Command {
if (watchMode) {
specWatcher({ spec: specFile, handler: this, handlerName: 'validate' });
}

await validate(this, specFile, flags);
}
}
17 changes: 15 additions & 2 deletions src/models/SpecificationFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
import path from 'path';
import { URL } from 'url';
import fetch from 'node-fetch';

import yaml from 'js-yaml';
import { loadContext } from './Context';
import { ErrorLoadingSpec } from '../errors/specification-file';
import { MissingContextFileError } from '../errors/context-error';
Expand Down Expand Up @@ -34,6 +34,19 @@ export class Specification {
}
}

isAsyncAPI3() {
const jsObj = this.toJson();
return jsObj.asyncapi === '3.0.0';
}

toJson(): Record<string, any> {
try {
return yaml.load(this.spec, {json: true}) as Record<string, any>;
} catch (e) {
return JSON.parse(this.spec);
}
}

text() {
return this.spec;
}
Expand Down Expand Up @@ -137,7 +150,7 @@ export async function load(filePathOrContextName?: string, loadType?: LoadType):
if (e instanceof MissingContextFileError) {
throw new ErrorLoadingSpec();
}

throw e;
}
}
Expand Down
15 changes: 15 additions & 0 deletions test/commands/bundle/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,28 @@ import path from 'path';
import { fileCleanup } from '../../testHelper';

const spec = fs.readFileSync('./test/commands/bundle/final-asyncapi.yaml', {encoding: 'utf-8'});
const asyncapiv3 = './test/specification-v3.yml';

function validateGeneratedSpec(filePath, spec) {
const generatedSPec = fs.readFileSync(path.resolve(filePath), { encoding: 'utf-8' });
return generatedSPec === spec;
}

describe('bundle', () => {
describe('should handle AsyncAPI v3 document correctly', () => {
test
.stderr()
.stdout()
.command([
'bundle',
asyncapiv3,
'--output=./test/commands/bundle/final.yaml'])
.it('give error', (ctx, done) => {
expect(ctx.stderr).toEqual('Error: One of the files you tried to bundle is AsyncAPI v3 format, the bundle command does not support it yet, please checkout https://github.com/asyncapi/bundler/issues/133\n');
expect(ctx.stdout).toEqual('');
done();
});
});
test
.stdout()
.command([
Expand Down
Loading