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: add optimize command #397

Merged
merged 50 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7b5f0b2
feat: integrate optimizer
KhudaDad414 Nov 21, 2022
c579fb2
fix type import
KhudaDad414 Nov 22, 2022
c76b364
fix package lock
KhudaDad414 Nov 22, 2022
2b74306
fix package lock
KhudaDad414 Nov 22, 2022
273d420
Merge branch 'master' into integrage-optimizer
KhudaDad414 Nov 22, 2022
6892503
add tests
KhudaDad414 Dec 5, 2022
fd17cdc
reallocate test
KhudaDad414 Dec 9, 2022
4c64f0e
Merge remote-tracking branch 'upstream/master' into integrage-optimizer
KhudaDad414 Dec 9, 2022
a74922a
add examples
KhudaDad414 Dec 9, 2022
a827d8a
update readme
KhudaDad414 Dec 9, 2022
5361792
Empty-Commit
KhudaDad414 Dec 9, 2022
c1f9a49
Merge branch 'master' into integrage-optimizer
KhudaDad414 Dec 13, 2022
b16d8cb
regenerate package-lock
KhudaDad414 Dec 14, 2022
3ca7cba
remove spinner
KhudaDad414 Dec 14, 2022
618d82d
cleanup
KhudaDad414 Dec 14, 2022
22b2eac
add spinner
KhudaDad414 Dec 14, 2022
935bdde
correct test
KhudaDad414 Dec 14, 2022
8563e20
revert fromTemplate test
KhudaDad414 Dec 14, 2022
45bf555
Merge branch 'master' into integrage-optimizer
KhudaDad414 Dec 19, 2022
dae3b0b
handle validation error.
KhudaDad414 Dec 21, 2022
c78e25c
update package-lock.json
KhudaDad414 Jan 12, 2023
875a753
update package-lock with node 14
KhudaDad414 Jan 12, 2023
2525fa9
Merge branch 'master' into integrage-optimizer
KhudaDad414 Jan 12, 2023
c9f1a5d
Empty commit
KhudaDad414 Jan 12, 2023
fff0a07
update package-lock
KhudaDad414 Jan 13, 2023
31bb508
Merge branch 'master' to integrage-optimizer
KhudaDad414 Jan 13, 2023
a683a4d
update package-lock with node 14
KhudaDad414 Jan 13, 2023
d283f52
resolve test issue
KhudaDad414 Jan 13, 2023
4ba9946
Empty commit
KhudaDad414 Jan 13, 2023
c030e5f
refactor fromTemplate tests
KhudaDad414 Jan 13, 2023
28ba18a
reallocate git test
KhudaDad414 Jan 13, 2023
2c22bf0
fix test
KhudaDad414 Jan 13, 2023
733631e
final touches
KhudaDad414 Jan 13, 2023
78f2744
Empty commit
KhudaDad414 Jan 13, 2023
77ec1d5
Merge remote-tracking branch 'master' into integrage-optimizer
KhudaDad414 Jan 16, 2023
2f48c16
revert test
KhudaDad414 Jan 16, 2023
46c27f2
use node 18
KhudaDad414 Jan 18, 2023
0f3562a
Merge branch 'master' into integrage-optimizer
KhudaDad414 Feb 1, 2023
0d78cbb
regenerate package-lock.json
KhudaDad414 Feb 1, 2023
2f46a7e
Empty-Commit
KhudaDad414 Feb 1, 2023
6726875
Empty-Commit
KhudaDad414 Feb 2, 2023
1f5ecf9
Merge branch 'master' into integrage-optimizer
KhudaDad414 Feb 2, 2023
fb35e0a
Empty-Commit
KhudaDad414 Feb 2, 2023
33d3f1a
Empty-Commit
KhudaDad414 Feb 3, 2023
9d75ef3
Merge branch 'master' into integrage-optimizer
KhudaDad414 Feb 6, 2023
47a004f
move usage guide
KhudaDad414 Feb 8, 2023
2e03c48
Empty-Commit
KhudaDad414 Feb 9, 2023
3329f5b
Empty-Commit
KhudaDad414 Feb 9, 2023
728bc1f
Update docs/usage.md
KhudaDad414 Feb 9, 2023
b63feb7
update usage
KhudaDad414 Feb 9, 2023
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
71 changes: 71 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@asyncapi/diff": "^0.4.0",
"@asyncapi/generator": "^1.9.12",
"@asyncapi/modelina": "^0.59.8",
"@asyncapi/optimizer": "^0.1.13",
"@asyncapi/parser": "^1.17.1",
"@asyncapi/studio": "^0.14.0",
"@oclif/core": "^1.18.0",
Expand Down
152 changes: 152 additions & 0 deletions src/commands/optimize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Flags } from '@oclif/core';
import { Optimizer, Output, Report, ReportElement } from '@asyncapi/optimizer';
import Command from '../base';
import { ValidationError } from '../errors/validation-error';
import { load } from '../models/SpecificationFile';
import * as inquirer from 'inquirer';
import chalk from 'chalk';
import { promises } from 'fs';
const { writeFile } = promises;

export enum Optimizations {
REMOVE_COMPONENTS='remove-components',
REUSE_COMPONENTS='reuse-components',
MOVE_TO_COMPONETS='move-to-components'
}

export enum Outputs {
TERMINAL='terminal',
NEW_FILE='new-file',
OVERWRITE='overwrite'
}
export default class Optimize extends Command {
static description = 'optimize asyncapi specification file';
isInteractive = false;
optimizations?: Optimizations[];
outputMethod?: Outputs;

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.'}),
output: Flags.string({char: 'o', default: Outputs.TERMINAL, options: Object.values(Outputs), description: 'select where you want the output.'}),
'no-tty': Flags.boolean({ description: 'do not use an interactive terminal', default: false }),
};

static args = [
{ name: 'spec-file', description: 'spec path, url, or context-name', required: false },
];

async run() {
const { args, flags } = await this.parse(Optimize); //NOSONAR
const filePath = args['spec-file'];
const specFile = await load(filePath);
KhudaDad414 marked this conversation as resolved.
Show resolved Hide resolved
this.isInteractive = !flags['no-tty'];
this.optimizations = flags.optimization as Optimizations[];
this.outputMethod = flags.output as Outputs;
const optimizer = new Optimizer(specFile.text());
const report = await optimizer.getReport();
KhudaDad414 marked this conversation as resolved.
Show resolved Hide resolved
const isURL = !!specFile.getFileURL();

if (!(report.moveToComponents.length || report.removeComponents.length || report.reuseComponents.length)) {
if (isURL) {
this.log(`URL ${specFile.getFileURL()} looks optimized!`);
} else {
this.log(`File ${specFile.getFilePath()} looks optimized!`);
KhudaDad414 marked this conversation as resolved.
Show resolved Hide resolved
}
return;
}
const isTTY = process.stdout.isTTY;
if (this.isInteractive && isTTY) {
await this.interactiveRun(report);
}

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) {
const pos = specPath.lastIndexOf('.');
newPath = `${specPath.substring(0,pos) }_optimized.${ specPath.substring(pos+1)}`;
} else {
newPath = 'asyncapi_optimized.yaml';
}
KhudaDad414 marked this conversation as resolved.
Show resolved Hide resolved

if (this.outputMethod === Outputs.TERMINAL) {
this.log(optimizedDocument);
} else if (this.outputMethod === Outputs.NEW_FILE) {
await writeFile(newPath, optimizedDocument, { encoding: 'utf8' });
this.log(`Created file ${newPath}...`);
} else if (this.outputMethod === Outputs.OVERWRITE) {
await writeFile(specPath ?? 'asyncapi.yaml', optimizedDocument, { encoding: 'utf8' });
this.log(`Created file ${newPath}...`);
}
} catch (error) {
throw new ValidationError({
type: 'parser-error',
err: error
});
}
}
private showOptimizations(elements: ReportElement[]) {
for (let i = 0; i < elements.length; i++) {
const element = elements[+i];
if (element.action==='move') {
this.log(`${chalk.green('move')} ${element.path} to ${element.target} and reference it.`);
} else if (element.action==='reuse') {
this.log(`${chalk.green('reuse')} ${element.target} in ${element.path}.`);
} else if (element.action === 'remove') {
this.log(`${chalk.red('remove')} ${element.path}.`);
}
}

this.log('\n');
}
private async interactiveRun(report: Report) {
const canMove = report.moveToComponents.length;
const canRemove = report.removeComponents.length;
const canReuse = report.reuseComponents.length;
const choices = [];

if (canMove) {
const totalMove = report.moveToComponents.filter((e) => e.action === 'move').length;
this.log(`\n${chalk.green(totalMove)} components can be moved to the components sections.\nthe following changes will be made:`);
this.showOptimizations(report.moveToComponents);
choices.push({name: 'move to components section', value: Optimizations.MOVE_TO_COMPONETS});
}
if (canRemove) {
const totalMove = report.removeComponents.length;
this.log(`${chalk.green(totalMove)} unused components can be removed.\nthe following changes will be made:`);
this.showOptimizations(report.removeComponents);
choices.push({name: 'remove components', value: Optimizations.REMOVE_COMPONENTS});
}
if (canReuse) {
const totalMove = report.reuseComponents.length;
this.log(`${chalk.green(totalMove)} components can be reused.\nthe following changes will be made:`);
this.showOptimizations(report.reuseComponents);
choices.push({name: 'reuse components', value: Optimizations.REUSE_COMPONENTS});
}
const optimizationRes = await inquirer.prompt([{
name: 'optimization',
message: 'select the type of optimization that you want to apply:',
type: 'checkbox',
default: 'all',
choices
}]);

this.optimizations = optimizationRes.optimization;

const outputRes = await inquirer.prompt([{
name: 'output',
message: 'where do you want to save the result:',
type: 'list',
default: 'log to terminal',
choices: [{name: 'log to terminal',value: Outputs.TERMINAL}, {name: 'create new file', value: Outputs.NEW_FILE}, {name: 'update original', value: Outputs.OVERWRITE}]
}]);
this.outputMethod = outputRes.output;
}
}
131 changes: 131 additions & 0 deletions test/commands/optimize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import path from 'path';
import { test } from '@oclif/test';
import { NO_CONTEXTS_SAVED } from '../../src/errors/context-error';
import TestHelper from '../testHelper';
import inquirer from 'inquirer';
import {Optimizations, Outputs} from '../../src/commands/optimize';

const testHelper = new TestHelper();
const optimizedFilePath = './test/specification.yml';
const unoptimizedFile = './test/dummyspec/unoprimizedSpec.yml';

describe('optimize', () => {
describe('no optimization needed', () => {
beforeEach(() => {
testHelper.createDummyContextFile();
});

afterEach(() => {
testHelper.deleteDummyContextFile();
});

test
.stderr()
.stdout()
.command(['optimize', optimizedFilePath])
.it('works when file path is passed', (ctx, done) => {
expect(ctx.stdout).toContain(`File ${optimizedFilePath} looks optimized!`);
expect(ctx.stderr).toEqual('');
done();
});

test
.stderr()
.stdout()
.command(['optimize', './test/not-found.yml'])
.it('should throw error if file path is wrong', (ctx, done) => {
expect(ctx.stdout).toEqual('');
expect(ctx.stderr).toEqual('error loading AsyncAPI document from file: ./test/not-found.yml file does not exist.\n');
done();
});

test
.stderr()
.stdout()
.command(['optimize', 'https://bit.ly/asyncapi'])
.it('works when url is passed', (ctx, done) => {
expect(ctx.stdout).toContain('URL https://bit.ly/asyncapi looks optimized!');
expect(ctx.stderr).toEqual('');
done();
});
});

describe('with no arguments', () => {
beforeEach(() => {
testHelper.createDummyContextFile();
});

afterEach(() => {
testHelper.setCurrentContext('home');
testHelper.deleteDummyContextFile();
});

test
.stderr()
.stdout()
.command(['optimize'])
.it('converts from current context', (ctx, done) => {
expect(ctx.stdout).toContain(`File ${path.resolve(__dirname, '../specification.yml')} looks optimized!`);
expect(ctx.stderr).toEqual('');
done();
});

test
.stderr()
.stdout()
.do(() => {
testHelper.unsetCurrentContext();
testHelper.createDummyContextFile();
})
.command(['optimize'])
.it('throws error message if no current context', (ctx, done) => {
expect(ctx.stdout).toEqual('');
expect(ctx.stderr).toEqual('ContextError: No context is set as current, please set a current context.\n');
done();
});

test
.stderr()
.stdout()
.do(() => {
testHelper.deleteDummyContextFile();
})
.command(['optimize'])
.it('throws error message if no context file exists', (ctx, done) => {
expect(ctx.stdout).toEqual('');
expect(ctx.stderr).toEqual(`error locating AsyncAPI document: ${NO_CONTEXTS_SAVED}\n`);
done();
});
});

describe('no-tty flag', () => {
test
.stderr()
.stdout()
.command(['optimize', unoptimizedFile, '--no-tty'])
.it('process without going to interactive mode.', (ctx, done) => {
expect(ctx.stdout).toContain('asyncapi: 2.0.0');
expect(ctx.stderr).toEqual('');
done();
});
});

describe('interactive terminal', () => {
test
.stub(inquirer, 'prompt', () => {
return Promise.resolve({optimization: [Optimizations.REMOVE_COMPONENTS] , output: Outputs.TERMINAL});
})
.stderr()
.stdout()
.command(['optimize', unoptimizedFile])
.it('interactive terminal, only remove components and outputs to terminal', (ctx, done) => {
expect(ctx.stdout).toContain('components can be moved to the components sections.');
expect(ctx.stdout).toContain('unused components can be removed.');
expect(ctx.stdout).toContain('components can be reused.');
expect(ctx.stdout).toContain('asyncapi: 2.0.0');
expect(ctx.stderr).toEqual('');
done();
});
});
});

Loading