From a6412d63111d5f867b6dfc5c39c8b4006b8b5dfe Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Thu, 18 Oct 2018 14:08:35 -0300 Subject: [PATCH 1/4] feat: Support for runtimes with multiple versions Added support for runtimes with multiple versions. Now runtimes must have the `version` attribute defined in order to be supported by the framework. The `runtime add`, `runtime remove` CLI commands now requires the `version` argument, in order to manipulate the correct runtime instance. The `function add` command can now receive the `runtimeVersion` argument, which allows users to specify the version of the runtime that will execute the created function. The `runtime ls` CLI command shows the runtime version and which runtimes are the defaults. The `RuntimeController` API has changed as well. The `getRuntime` function can now receive the `version` parameter, which when defined, tries to retrieve the runtime with this specific version. If not defined, returns the default version of the given runtime `name`. --- packages/ant-cli/spec/bin/ant.spec.js | 38 +++-- packages/ant/lib/config/Config.js | 46 +++--- .../ant/lib/functions/runtimes/Runtime.js | 39 ++++- .../functions/runtimes/RuntimeController.js | 39 +++-- packages/ant/package.json | 1 + packages/ant/spec/lib/Ant.spec.js | 62 ++++---- packages/ant/spec/lib/config/Config.spec.js | 68 ++++++--- .../lib/functions/FunctionController.spec.js | 2 +- .../spec/lib/functions/LibFunction.spec.js | 10 +- .../runtimes/RuntimeController.spec.js | 140 +++++++++++++++--- .../spec/lib/plugins/PluginController.spec.js | 4 +- plugins/ant-core/lib/Core.js | 105 ++++++++----- plugins/ant-core/spec/lib/Core.spec.js | 135 +++++++++++------ .../directives/DirectiveController.spec.js | 2 +- .../spec/lib/Serverless.spec.js | 4 +- 15 files changed, 493 insertions(+), 202 deletions(-) diff --git a/packages/ant-cli/spec/bin/ant.spec.js b/packages/ant-cli/spec/bin/ant.spec.js index a43c944..f943440 100644 --- a/packages/ant-cli/spec/bin/ant.spec.js +++ b/packages/ant-cli/spec/bin/ant.spec.js @@ -773,14 +773,15 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addFunction = jest.fn(async (name, func, runtime, type, isGlobal) => { + .addFunction = jest.fn(async (name, func, runtime, version, type, isGlobal) => { expect(name).toBe('myfunc'); expect(func).toBe(path.resolve(process.cwd(), func)); expect(runtime).toBe('nodejs'); + expect(version).toBe('6'); expect(type).toBe('lib'); expect(isGlobal).toBe(false); }); - antCli._yargs.parse('function add myfunc path/to/myfunc nodejs'); + antCli._yargs.parse('function add myfunc path/to/myfunc nodejs 6'); }); test('should work with global flag', done => { @@ -797,14 +798,15 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addFunction = jest.fn(async (name, path, runtime, type, isGlobal) => { + .addFunction = jest.fn(async (name, path, runtime, version, type, isGlobal) => { expect(name).toBe('myfunc'); expect(path).toBe('/path/to/myfunc'); expect(runtime).toBe('nodejs'); + expect(version).toBe('6'); expect(type).toBe('lib'); expect(isGlobal).toBe(true); }); - antCli._yargs.parse('function add myfunc /path/to/myfunc nodejs --global'); + antCli._yargs.parse('function add myfunc /path/to/myfunc nodejs 6 --global'); }); test('should work with template', done => { @@ -821,7 +823,7 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addFunction = jest.fn(async (name, path, runtime, type, isGlobal, template) => { + .addFunction = jest.fn(async (name, path, runtime, version, type, isGlobal, template) => { expect(name).toBe('myfunc'); expect(template).toBe('/path/to/my/template'); }); @@ -1062,13 +1064,14 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addRuntime = jest.fn(async (name, bin, extensions, isGlobal) => { + .addRuntime = jest.fn(async (name, version, bin, extensions, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('10'); expect(bin).toBe(path.resolve(process.cwd(), bin)); - expect(extensions).toEqual(['nodejs', 'python']); + expect(extensions).toEqual(['foo', 'bar']); expect(isGlobal).toBe(false); }); - antCli._yargs.parse('runtime add myruntime path/to/myruntime nodejs python'); + antCli._yargs.parse('runtime add myruntime 10 path/to/myruntime foo bar'); }); test('should work with global flag', done => { @@ -1085,14 +1088,15 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .addRuntime = jest.fn(async (name, bin, extensions, isGlobal) => { + .addRuntime = jest.fn(async (name, version, bin, extensions, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('10'); expect(bin).toBe('/path/to/myruntime'); console.log(extensions); expect(extensions).toEqual(['nodejs', 'python']); expect(isGlobal).toBe(true); }); - antCli._yargs.parse('runtime add myruntime /path/to/myruntime nodejs python --global'); + antCli._yargs.parse('runtime add myruntime 10 /path/to/myruntime nodejs python --global'); }); test('should handle any errors', done => { @@ -1113,7 +1117,7 @@ ant.js --help plugin add`) .addRuntime = jest.fn(async () => { throw new Error('Mocked error'); }); - antCli._yargs.parse('runtime add myruntime /path/to/myruntime nodejs python'); + antCli._yargs.parse('runtime add myruntime 10 /path/to/myruntime nodejs python'); }); }); @@ -1132,11 +1136,12 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .removeRuntime = jest.fn(async (name, isGlobal) => { + .removeRuntime = jest.fn(async (name, version, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('5'); expect(isGlobal).toBe(false); }); - antCli._yargs.parse('runtime remove myruntime'); + antCli._yargs.parse('runtime remove myruntime 5'); }); test('should work with global flag', done => { @@ -1153,11 +1158,12 @@ ant.js --help plugin add`) ._ant .pluginController .getPlugin('Core') - .removeRuntime = jest.fn(async (name, isGlobal) => { + .removeRuntime = jest.fn(async (name, version, isGlobal) => { expect(name).toBe('myruntime'); + expect(version).toBe('5'); expect(isGlobal).toBe(true); }); - antCli._yargs.parse('runtime remove myruntime --global'); + antCli._yargs.parse('runtime remove myruntime 5 --global'); }); test('should handle any errors', done => { @@ -1178,7 +1184,7 @@ ant.js --help plugin add`) .removeRuntime = jest.fn(async () => { throw new Error('Mocked error'); }); - antCli._yargs.parse('runtime remove myruntime'); + antCli._yargs.parse('runtime remove myruntime 5'); }); }); diff --git a/packages/ant/lib/config/Config.js b/packages/ant/lib/config/Config.js index c741eb4..60b7f27 100644 --- a/packages/ant/lib/config/Config.js +++ b/packages/ant/lib/config/Config.js @@ -283,9 +283,11 @@ template remove command should do nothing`); * Adds a {@link BinFunction} or {@link LibFunction} into this configuration. * * @param {!BinFunction|LibFunction} antFunction The function to be added + * @param {Boolean} specifyRuntimeVersion Flag indicating the runtime version + * should be specified * @returns {Config} This configuration instance. */ - addFunction(antFunction) { + addFunction(antFunction, specifyRuntimeVersion = false) { assert(antFunction, 'Param "antFunction" is required'); assert((antFunction instanceof BinFunction || antFunction instanceof LibFunction), 'Param "antFunction" must be an instance of BinFunction or LibFunction'); @@ -318,7 +320,9 @@ function add command will OVERRIDE the current function`); attributes.items.push(new Pair(new Scalar('bin'), new Scalar(bin))); } else { attributes.items.push(new Pair(new Scalar('handler'), new Scalar(handler))); - attributes.items.push(new Pair(new Scalar('runtime'), new Scalar(runtime.name))); + attributes.items.push(new Pair(new Scalar('runtime'), new Scalar( + specifyRuntimeVersion ? `${runtime.name} ${runtime.version}` : runtime.name + ))); } console.log(`Function "${name}" successfully added on configuration file ${this._path}`); // Document has changed, resets the cached JSON @@ -371,21 +375,21 @@ function remove command should do nothing`); assert(runtime, 'Param "runtime" is required'); assert(runtime instanceof Runtime, 'Param "runtime" must be an instance of Runtime'); - const { name, bin, extensions } = runtime; + const { name, bin, extensions, version } = runtime; // Ensure the "runtimes" root element exists, // and it is a Pair<"runtimes", Map> const runtimes = this._ensureRootCollectionNode('runtimes', Map); - - if (this._filterNodeFromCollectionByKey(runtimes, name)) { - console.log(`Runtime "${name}" already found on the configuration file. \ + const entryName = `${name} ${version}`; + if (this._filterNodeFromCollectionByKey(runtimes, entryName)) { + console.log(`Runtime "${entryName}" already found on the configuration file. \ runtime add command will OVERRIDE the current runtime`); } - logger.log(`Adding runtime ${name} into configuration file ${this._path}`); + logger.log(`Adding runtime ${entryName} into configuration file ${this._path}`); // Creates the brand new Runtime node and adds its to the runtimes Map. runtimes.items.push(new Pair( - new Scalar(name), + new Scalar(entryName), this._createAttributeMap({ bin, extensions }) )); @@ -398,28 +402,35 @@ runtime add command will OVERRIDE the current runtime`); * Removes an {@link Runtime} from this configuration. * * @param {!String} runtime The name of the {@link Runtime} to be removed + * @param {!String} version The version of the {@link Runtime} to be removed * @returns {Config} This configuration instance. */ - removeRuntime(runtime) { + removeRuntime(runtime, version) { assert(runtime, 'Could not remove runtime: param "runtime" is required'); assert( typeof runtime === 'string', 'Could not remove runtime: param "runtime" should be String' ); + assert(version, 'Could not remove runtime: param "version" is required'); + assert( + typeof version === 'string', + 'Could not remove runtime: param "version" should be String' + ); const runtimes = this._findRootCollectionNode('runtimes', Map); if (!runtimes) { console.log('No "runtimes" was found on configuration file. runtime \ remove command should do nothing'); return this; } - if(this._filterNodeFromCollectionByKey(runtimes, runtime)) { - console.log(`Runtime "${runtime}" successfully removed from \ + const entryName = `${runtime} ${version}`; + if(this._filterNodeFromCollectionByKey(runtimes, entryName)) { + console.log(`Runtime "${entryName}" successfully removed from \ configuration file ${this._path}`); // Document has changed, resets the cached JSON this._cachedJson = null; } else { - console.log(`Runtime "${runtime}" was not found on configuration file. \ + console.log(`Runtime "${entryName}" was not found on configuration file. \ runtime remove command should do nothing`); } return this; @@ -675,9 +686,10 @@ provider "${providerName}"` } else if (handler) { let runtimeInstance; if (runtime) { - runtimeInstance = runtimeController.getRuntime(runtime); + const [name, version] = runtime.split(' '); + runtimeInstance = runtimeController.getRuntime(name, version); if (!runtimeInstance) { - throw new AntError(`Runtime ${runtime} was not found`); + throw new AntError(`Runtime ${name} ${version} was not found`); } } else { runtimeInstance = runtimeController.defaultRuntime; @@ -705,14 +717,14 @@ configuration file', e); return []; } return Object.keys(runtimes).map( - name => new Runtime(ant, name, runtimes[name].bin, runtimes[name].extensions) + key => new Runtime(ant, key.split(' ')[0], runtimes[key].bin, runtimes[key].extensions, undefined, key.split(' ')[1]) ); } /** - * Parses the {@link Runtime} from the configuration file "defaultRuntime" string. + * Parses the {@link Runtime} from the configuration file "runtime" string. * - * @param {Object} defaultRuntime The "defaultRuntime" object from the configuration file + * @param {String} defaultRuntime The "runtime" value from the configuration file * @param {!RuntimeController} runtimeController The {@link RuntimeController} * used to find the default {@link Runtime} * @returns {Runtime} The {@link Runtime} instance, given the default runtime name diff --git a/packages/ant/lib/functions/runtimes/Runtime.js b/packages/ant/lib/functions/runtimes/Runtime.js index 320f82f..b096330 100644 --- a/packages/ant/lib/functions/runtimes/Runtime.js +++ b/packages/ant/lib/functions/runtimes/Runtime.js @@ -4,6 +4,7 @@ const assert = require('assert'); const BinFunction = require('../BinFunction'); +const semver = require('semver'); /** * @class ant/Runtime @@ -21,16 +22,22 @@ class Runtime extends BinFunction { * runtime supports to execute. * @param {String} template The path to the file to be used as template * when creating new functions with this runtime + * @param {!String} version The runtime version supported + * @param {Boolean} isDefault Flag indicating it should be set as default * @throws {AssertionError} If "ant", "name", "bin" or "extensions" params are * not valid. */ - constructor(ant, name, bin, extensions, template) { + constructor(ant, name, bin, extensions, template, version, isDefault) { super(ant, name, bin); assert( !extensions || extensions instanceof Array, 'Could not initialize Runtime: param "extensions" should be Array' ); + assert( + typeof version === 'string' && version.trim() !== '', + 'Could not initialize Runtime: param "version" should be non empty String' + ); /** * Contains the extensions that this runtime supports to execute. @@ -46,6 +53,20 @@ class Runtime extends BinFunction { * @private */ this._template = template; + + /** + * The runtime version supported. + * @type {String} + * @private + */ + this._version = semver.major(semver.coerce(version)).toString(); + + /** + * Flag indicating it should be set as default + * @type {Boolean} + * @private + */ + this._isDefault = isDefault; } /** @@ -65,6 +86,22 @@ class Runtime extends BinFunction { get template() { return this._template; } + + /** + * Returns the runtime version supported. + * @type {String} + */ + get version() { + return this._version; + } + + /** + * Returns the flag indicating it should be set as default. + * @type {Boolean} + */ + get isDefault() { + return this._isDefault; + } } module.exports = Runtime; diff --git a/packages/ant/lib/functions/runtimes/RuntimeController.js b/packages/ant/lib/functions/runtimes/RuntimeController.js index ff084bc..ac8291c 100644 --- a/packages/ant/lib/functions/runtimes/RuntimeController.js +++ b/packages/ant/lib/functions/runtimes/RuntimeController.js @@ -4,6 +4,7 @@ const assert = require('assert'); const Runtime = require('./Runtime'); +const semver = require('semver'); /** * @class ant/RuntimeController @@ -87,7 +88,6 @@ class RuntimeController { runtimes instanceof Array, 'Could not load runtimes: param "runtimes" should be Array' ); - for (const runtime of runtimes) { assert( runtime instanceof Runtime, @@ -100,29 +100,50 @@ be an instance of Runtime` `Could not load runtime ${runtime.name}: the framework used to \ initialize the runtime is different to this controller's` ); - this._runtimes.set( - runtime.name, - runtime - ); + let runtimeByVersion = this._runtimes.get(runtime.name); + if (!runtimeByVersion) { + runtimeByVersion = new Map(); + // If this is the first runtime by its name, + // set it as the default + runtimeByVersion.set('default', runtime); + this._runtimes.set(runtime.name, runtimeByVersion); + } else if (runtime.isDefault){ + // If isDefault is true, overrides the actual default runtime + runtimeByVersion.set('default', runtime); + } + runtimeByVersion.set(runtime.version, runtime); } } /** * Contains the loaded runtimes. - * @type {Runtime[]} + * @type {Map>} * @readonly */ get runtimes() { - return Array.from(this._runtimes.values()); + return this._runtimes; } /** * Gets a specific runtime by its name. * @param {String} name The name of the runtime to be gotten. + * @param {String} version The target runtime version. * @return {Runtime} The runtime object. */ - getRuntime(name) { - return this._runtimes.get(name) || null; + getRuntime(name, version) { + assert(typeof name === 'string', 'Could not get runtime. "name" should be String'); + const runtimeByVersion = this._runtimes.get(name); + if (!runtimeByVersion || runtimeByVersion.length === 0) { + return null; + } + // If version was not provided, returns the default + // runtime by its name + if (!version) { + return runtimeByVersion.get('default'); + } + assert(typeof version === 'string' && version.length, 'Could not get runtime. "version" \ +should be non-empty String'); + return runtimeByVersion.get(semver.major(semver.coerce(version)).toString()) || null; } /** diff --git a/packages/ant/package.json b/packages/ant/package.json index 51ce64c..7202030 100644 --- a/packages/ant/package.json +++ b/packages/ant/package.json @@ -55,6 +55,7 @@ "@back4app/ant-serverless": "^0.0.17", "@back4app/ant-util": "^0.0.17", "@back4app/ant-util-rxjs": "^0.0.17", + "semver": "^5.5.1", "fs-extra": "^7.0.0", "mustache": "^3.0.0", "rxjs": "^6.3.2", diff --git a/packages/ant/spec/lib/Ant.spec.js b/packages/ant/spec/lib/Ant.spec.js index edbfc8e..7dab62e 100644 --- a/packages/ant/spec/lib/Ant.spec.js +++ b/packages/ant/spec/lib/Ant.spec.js @@ -191,60 +191,63 @@ Template category value is not an object!' test('should load runtimes from config', () => { const runtimes = { - Bin: { - bin: '/path/to/bin', + 'Foo 0': { + bin: '/path/to/foo', extensions: ['js', 'py'] }, - Lib: { - bin: '/path/to/lib', + 'Bar 1': { + bin: '/path/to/bar', extensions: ['cpp', 'c', 'h'] } }; const ant = new Ant({ runtimes }); - const binRuntime = ant.runtimeController.getRuntime('Bin'); + const binRuntime = ant.runtimeController.getRuntime('Foo'); expect(binRuntime).toBeInstanceOf(Runtime); - expect(binRuntime.name).toBe('Bin'); - expect(binRuntime.bin).toBe(runtimes.Bin.bin); - expect(binRuntime.extensions).toBe(runtimes.Bin.extensions); - - const libRuntime = ant.runtimeController.getRuntime('Lib'); - expect(libRuntime.name).toBe('Lib'); - expect(libRuntime.bin).toBe(runtimes.Lib.bin); - expect(libRuntime.extensions).toBe(runtimes.Lib.extensions); + expect(binRuntime.name).toBe('Foo'); + expect(binRuntime.bin).toBe(runtimes['Foo 0'].bin); + expect(binRuntime.extensions).toBe(runtimes['Foo 0'].extensions); + expect(binRuntime.version).toBe('0'); + + const libRuntime = ant.runtimeController.getRuntime('Bar', '1'); + expect(libRuntime.name).toBe('Bar'); + expect(libRuntime.bin).toBe(runtimes['Bar 1'].bin); + expect(libRuntime.extensions).toBe(runtimes['Bar 1'].extensions); + expect(libRuntime.version).toBe('1'); }); test('should load default runtime from config', () => { const runtimes = { - Bin: { - bin: '/path/to/bin', + 'Foo 0': { + bin: '/path/to/foo', extensions: ['js', 'py'] }, - Lib: { - bin: '/path/to/lib', + 'Bar 1': { + bin: '/path/to/bar', extensions: ['cpp', 'c', 'h'] } }; - const runtime = 'Bin'; + const runtime = 'Foo'; const ant = new Ant({ runtimes, runtime }); const fooRuntime = ant.runtimeController.defaultRuntime; expect(fooRuntime).toBeInstanceOf(Runtime); - expect(fooRuntime.name).toBe('Bin'); - expect(fooRuntime.bin).toBe(runtimes.Bin.bin); - expect(fooRuntime.extensions).toBe(runtimes.Bin.extensions); + expect(fooRuntime.name).toBe('Foo'); + expect(fooRuntime.bin).toBe(runtimes['Foo 0'].bin); + expect(fooRuntime.extensions).toBe(runtimes['Foo 0'].extensions); + expect(fooRuntime.version).toBe('0'); }); test('should load default runtime from global config', () => { const runtimes = { - Bin: { - bin: '/path/to/bin', + 'Foo 0': { + bin: '/path/to/foo', extensions: ['js', 'py'] }, - Lib: { - bin: '/path/to/lib', + 'Bar 1': { + bin: '/path/to/bar', extensions: ['cpp', 'c', 'h'] } }; - const defaultRuntimeName = 'Bin'; + const defaultRuntimeName = 'Foo'; jest.spyOn(Ant.prototype, '_getGlobalConfig').mockImplementation(() => { return { runtimes, @@ -254,9 +257,10 @@ Template category value is not an object!' try { const ant = new Ant(); const defaultRuntime = ant.runtimeController.defaultRuntime; - expect(defaultRuntime.name).toBe('Bin'); - expect(defaultRuntime.bin).toBe('/path/to/bin'); - expect(defaultRuntime.extensions).toEqual([ 'js', 'py' ]); + expect(defaultRuntime.name).toBe(defaultRuntimeName); + expect(defaultRuntime.bin).toBe(runtimes['Foo 0'].bin); + expect(defaultRuntime.extensions).toEqual(runtimes['Foo 0'].extensions); + expect(defaultRuntime.version).toBe('0'); } finally { jest.restoreAllMocks(); } diff --git a/packages/ant/spec/lib/config/Config.spec.js b/packages/ant/spec/lib/config/Config.spec.js index 0e210a9..bba87c5 100644 --- a/packages/ant/spec/lib/config/Config.spec.js +++ b/packages/ant/spec/lib/config/Config.spec.js @@ -684,7 +684,7 @@ configuration. template remove command should do nothing'); config.addFunction( new LibFunction(ant, 'LibFunc', '/myhandler', - new Runtime(ant, 'MyRuntime', '/my/runtime') + new Runtime(ant, 'MyRuntime', '/my/runtime', [], undefined, '1') ) ); functions = config.config.functions; @@ -697,6 +697,26 @@ configuration. template remove command should do nothing'); runtime: 'MyRuntime' } }); + + config.addFunction( + new LibFunction(ant, 'LibFuncVersionDefined', '/myhandler', + new Runtime(ant, 'MyRuntime', '/my/runtime', [], undefined, '1') + ), true + ); + functions = config.config.functions; + expect(functions).toEqual({ + BinFunc: { + bin: '/my/bin' + }, + LibFunc: { + handler: '/myhandler', + runtime: 'MyRuntime' + }, + LibFuncVersionDefined: { + handler: '/myhandler', + runtime: 'MyRuntime 1' + } + }); }); test('should override if function already exists', () => { @@ -737,7 +757,7 @@ found on the configuration file. function add command will OVERRIDE the current const config = new Config({}); config.addFunction(new BinFunction(ant, 'BinFunc', '/my/bin')); config.addFunction(new LibFunction(ant, 'LibFunc', '/myhandler', - new Runtime(ant, 'MyRuntime', 'my/runtime') + new Runtime(ant, 'MyRuntime', 'my/runtime', [], undefined, '1') )); config.removeFunction('BinFunc'); const { functions } = config.config; @@ -774,10 +794,10 @@ found on the configuration file. function add command will OVERRIDE the current test('should add a runtime', () => { const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); expect(config.addRuntime(runtime)).toBe(config); expect(config.config.runtimes).toEqual({ - runtime: { + 'runtime 1': { bin: '/my/bin', extensions: ['js'] } @@ -789,13 +809,13 @@ found on the configuration file. function add command will OVERRIDE the current console.log = jest.fn(); const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); config.addRuntime(runtime); - config.addRuntime(new Runtime(ant, 'runtime', '/alternative/bin', [ 'py' ])); - expect(console.log).toHaveBeenCalledWith('Runtime "runtime" already \ + config.addRuntime(new Runtime(ant, 'runtime', '/alternative/bin', [ 'py' ], undefined, '1')); + expect(console.log).toHaveBeenCalledWith('Runtime "runtime 1" already \ found on the configuration file. runtime add command will OVERRIDE the current runtime'); expect(config.config.runtimes).toEqual({ - runtime: { + 'runtime 1': { bin: '/alternative/bin', extensions: [ 'py' ] } @@ -807,16 +827,16 @@ found on the configuration file. runtime add command will OVERRIDE the current r test('should remove a runtime', () => { const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); config.addRuntime(runtime); - config.removeRuntime('runtime'); + config.removeRuntime('runtime', '1'); expect(config.config.runtimes).toEqual({}); }); test('should do nothing because "runtimes" does not exists', () => { console.log = jest.fn(); const config = new Config({}); - config.removeRuntime('runtime'); + config.removeRuntime('runtime', '1'); expect(console.log).toHaveBeenCalledWith('No "runtimes" was found \ on configuration file. runtime remove command should do nothing'); }); @@ -825,10 +845,10 @@ on configuration file. runtime remove command should do nothing'); console.log = jest.fn(); const ant = new Ant(); const config = new Config({}); - const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ]); + const runtime = new Runtime(ant, 'runtime', '/my/bin', [ 'js' ], undefined, '1'); config.addRuntime(runtime); - config.removeRuntime('foo'); - expect(console.log).toHaveBeenCalledWith('Runtime "foo" was not \ + config.removeRuntime('foo', '1'); + expect(console.log).toHaveBeenCalledWith('Runtime "foo 1" was not \ found on configuration file. runtime remove command should do nothing'); }); }); @@ -967,7 +987,7 @@ Template category value is not an object!' }; const runtimeController = { getRuntime: jest.fn().mockImplementation( - runtime => new Runtime(ant, runtime, 'foo') + runtime => new Runtime(ant, runtime, 'foo', [], undefined, '1') ), ant: new Ant() }; @@ -1028,11 +1048,11 @@ Template category value is not an object!' describe('ParseConfigRuntimes', () => { test('should parse runtimes from config', () => { const runtimes = { - Runtime: { + 'Runtime 1': { bin: '/my/runtime', extensions: ['py'] }, - Node: { + 'Node 2': { bin: '/node', extensions: ['js'] }, @@ -1043,20 +1063,24 @@ Template category value is not an object!' expect(results[0]).toBeInstanceOf(Runtime); expect(results[0].name).toBe('Runtime'); - expect(results[0].bin).toBe(runtimes.Runtime.bin); - expect(results[0].extensions).toBe(runtimes.Runtime.extensions); + expect(results[0].version).toBe('1'); + expect(results[0].bin).toBe(runtimes['Runtime 1'].bin); + expect(results[0].extensions).toBe(runtimes['Runtime 1'].extensions); expect(results[1]).toBeInstanceOf(Runtime); expect(results[1].name).toBe('Node'); - expect(results[1].bin).toBe(runtimes.Node.bin); - expect(results[1].extensions).toBe(runtimes.Node.extensions); + expect(results[1].version).toBe('2'); + expect(results[1].bin).toBe(runtimes['Node 2'].bin); + expect(results[1].extensions).toBe(runtimes['Node 2'].extensions); }); }); describe('ParseConfigDefaultRuntime', () => { test('should parse default runtime from config', () => { const myDefaultRuntime = 'myDefaultRuntime'; - const runtimeStub = new Runtime(new Ant(), 'runtimeStub', '/my/runtime/stub', ['foo', 'bar']); + const runtimeStub = new Runtime( + new Ant(), 'runtimeStub', '/my/runtime/stub', ['foo', 'bar'], undefined, '1' + ); const getRuntimeMock = jest.fn(name => { expect(name).toBe(myDefaultRuntime); return runtimeStub; diff --git a/packages/ant/spec/lib/functions/FunctionController.spec.js b/packages/ant/spec/lib/functions/FunctionController.spec.js index 11239c9..664359f 100644 --- a/packages/ant/spec/lib/functions/FunctionController.spec.js +++ b/packages/ant/spec/lib/functions/FunctionController.spec.js @@ -124,7 +124,7 @@ describe('lib/functions/FunctionController.js', () => { const function1 = new AntFunction(antWithFunctions, 'ant'); const function2 = new BinFunction(antWithFunctions, 'bin', '/path/to/bin'); const function3 = new LibFunction(antWithFunctions, 'lib', '/path/to/lib', - new Runtime(antWithFunctions, 'runtime', '/path/to/runtime') + new Runtime(antWithFunctions, 'runtime', '/path/to/runtime', [], undefined, '7') ); antWithFunctions.functionController.loadFunctions([function1, function2, function3]); diff --git a/packages/ant/spec/lib/functions/LibFunction.spec.js b/packages/ant/spec/lib/functions/LibFunction.spec.js index 4fc786a..557dab2 100644 --- a/packages/ant/spec/lib/functions/LibFunction.spec.js +++ b/packages/ant/spec/lib/functions/LibFunction.spec.js @@ -19,7 +19,10 @@ const ant = new Ant(); const fooRuntime = new Runtime( ant, 'fooRuntime', - path.resolve(utilPath, 'functions/fooRuntime.js') + path.resolve(utilPath, 'functions/fooRuntime.js'), + [], + undefined, + '1' ); const libFunction = new LibFunction( @@ -78,7 +81,10 @@ describe('lib/functions/LibFunction.js', () => { const runtime = new Runtime( ant, 'fooBinFunction', - 'it/will/fail' + 'it/will/fail', + [], + undefined, + '1' ); runtime.run = () => { throw new Error('Some error'); }; expect(() => { diff --git a/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js b/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js index 1890a82..10c1e00 100644 --- a/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js +++ b/packages/ant/spec/lib/functions/runtimes/RuntimeController.spec.js @@ -19,10 +19,9 @@ describe('lib/functions/runtimes/RuntimeController.js', () => { test('should load plugins\' runtimes', () => { const antWithRuntimes = new Ant(); - - const runtime1 = new Runtime(antWithRuntimes, 'runtime1', '/foo/bin'); - const runtime2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin'); - const runtime2v2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin'); + const runtime1 = new Runtime(antWithRuntimes, 'runtime1', '/foo/bin', [], undefined, '1'); + const runtime2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin', [], undefined, '2'); + const runtime2v2 = new Runtime(antWithRuntimes, 'runtime2', '/foo/bin', [], undefined, '3'); /** * Represents a {@link Plugin} with runtimes for testing purposes. @@ -36,17 +35,12 @@ describe('lib/functions/runtimes/RuntimeController.js', () => { } antWithRuntimes.pluginController.loadPlugins([PluginWithRuntimes]); - expect(antWithRuntimes.runtimeController.runtimes) - .toEqual(expect.any(Array)); - expect( - antWithRuntimes.runtimeController.runtimes[0].name - ).toEqual('Node'); - expect( - antWithRuntimes.runtimeController.runtimes[1] - ).toEqual(runtime1); - expect( - antWithRuntimes.runtimeController.runtimes[2] - ).toEqual(runtime2v2); + const { runtimes } = antWithRuntimes.runtimeController; + expect(runtimes).toEqual(expect.any(Map)); + expect(runtimes.get('Node').get('default')).toBeDefined(); + expect(runtimes.get('runtime1').get('default')).toEqual(runtime1); + expect(runtimes.get('runtime2').get('default')).toEqual(runtime2); + expect(runtimes.get('runtime2').get('3')).toEqual(runtime2v2); }); test('should fail if "ant" param is not passed', () => { @@ -83,7 +77,8 @@ should be Ant' 'myCustomRuntime', '/foo/bin', ['extension'], - '/foo/template' + '/foo/template', + '1' ); const runtimes = [myCustomRuntime]; const runtimeController = new RuntimeController(ant, runtimes); @@ -92,6 +87,34 @@ should be Ant' expect(loadedRuntime.template).toEqual('/foo/template'); }); + test('should load runtimes and set a new default', () => { + const myCustomRuntime = new Runtime( + ant, + 'myCustomRuntime', + '/foo/bin', + ['extension'], + '/foo/template', + '1' + ); + const myNewDefault = new Runtime( + ant, + 'myCustomRuntime', + '/bar/bin', + ['newextension'], + '/bar/template', + '1', + true + ); + const runtimes = [myCustomRuntime]; + const runtimeController = new RuntimeController(ant, runtimes); + let loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name); + expect(loadedRuntime).toEqual(myCustomRuntime); + + runtimeController.loadRuntimes([myNewDefault]); + loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name); + expect(loadedRuntime).toEqual(myNewDefault); + }); + describe('RuntimeController.ant', () => { test('should be readonly', () => { expect(runtimeController.ant).toEqual(ant); @@ -101,9 +124,90 @@ should be Ant' }); describe('RuntimeController.getRuntime', () => { - test('should return null if runtime not found', () => { - expect(runtimeController.getRuntime('NotExistent')) + test('should return null if runtime list is empty', () => { + const runtimeController = new RuntimeController(ant); + expect(runtimeController.getRuntime('any runtime')) .toEqual(null); }); + + test('should return null if runtime was not found', () => { + const runtimes = [ + new Runtime( + ant, 'myCustomRuntime', '/foo/bin', ['extension'], '/foo/template', '1.0.0' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin2', ['extension'], null, '0.0.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin3', ['extension'], null, '2.0.1' + ) + ]; + const runtimeController = new RuntimeController(ant, runtimes); + expect(runtimeController.getRuntime('foo')).toBeNull(); + }); + + test('should fail due to invalid version param', () => { + try { + runtimeController.getRuntime('name', 1.2); + } catch (err) { + expect(err.message).toBe('Could not get runtime. "version" \ +should be non-empty String'); + } + }); + + test('should return the default runtime', () => { + const myCustomRuntime = new Runtime( + ant, + 'myCustomRuntime', + '/foo/bin', + ['extension'], + '/foo/template', + '1.0.0' + ); + const runtimes = [ + myCustomRuntime, + new Runtime( + ant, 'myCustomRuntime', '/foo/bin2', ['extension'], null, '0.0.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin3', ['extension'], null, '2.0.1' + ) + ]; + const runtimeController = new RuntimeController(ant, runtimes); + const loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name); + expect(loadedRuntime).toEqual(myCustomRuntime); + }); + + test('should return given a version', () => { + const myCustomRuntime = new Runtime( + ant, + 'myCustomRuntime', + '/foo/bin', + ['extension'], + '/foo/template', + '1.0' + ); + const runtimes = [ myCustomRuntime ]; + const runtimeController = new RuntimeController(ant, runtimes); + const loadedRuntime = runtimeController.getRuntime(myCustomRuntime.name, '1'); + expect(loadedRuntime).toEqual(myCustomRuntime); + }); + + test('should return null due to version out of range', () => { + const runtimes = [ + new Runtime( + ant, 'myCustomRuntime', '/foo/bin1', ['extension'], null, '1.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin2', ['extension'], null, '2.1' + ), + new Runtime( + ant, 'myCustomRuntime', '/foo/bin3', ['extension'], null, '3.0.2' + ) + ]; + const runtimeController = new RuntimeController(ant, runtimes); + const loadedRuntime = runtimeController.getRuntime('myCustomRuntime', '4'); + expect(loadedRuntime).toBeNull(); + }); }); }); diff --git a/packages/ant/spec/lib/plugins/PluginController.spec.js b/packages/ant/spec/lib/plugins/PluginController.spec.js index f3ab3a8..c4d76ca 100644 --- a/packages/ant/spec/lib/plugins/PluginController.spec.js +++ b/packages/ant/spec/lib/plugins/PluginController.spec.js @@ -159,8 +159,8 @@ class PluginWithNotValidFunction extends Plugin { } } -const runtime1 = new Runtime(ant, 'runtime1', '/foo/path'); -const runtime2 = new Runtime(ant, 'runtime2', '/foo/path'); +const runtime1 = new Runtime(ant, 'runtime1', '/foo/path', [], undefined, '1'); +const runtime2 = new Runtime(ant, 'runtime2', '/foo/path', [], undefined, '2'); /** * Represents a {@link Plugin} with runtimes for testing purposes. diff --git a/plugins/ant-core/lib/Core.js b/plugins/ant-core/lib/Core.js index 56db1c1..cb6aa5c 100644 --- a/plugins/ant-core/lib/Core.js +++ b/plugins/ant-core/lib/Core.js @@ -51,7 +51,8 @@ class Core extends Plugin { 'Node', path.resolve(__dirname, '../functions/nodeRuntime.js'), ['js'], - path.resolve(__dirname, '../templates/function/node.js.mustache') + path.resolve(__dirname, '../templates/function/node.js.mustache'), + '10' ) ]; } @@ -230,42 +231,48 @@ using template "${argv.template}"` 'function ', 'Manage functions of Ant framework', yargs => { yargs.command( - 'add [function] [runtime]', + 'add [function] [runtime] [runtimeVersion]', 'Adds/overrides a function', yargs => { yargs.positional('name', { describe: 'The name of the function', - string: true + type: 'string' }).positional('function', { describe: 'The path to the function', - string: true + type: 'string' }).positional('runtime', { describe: 'The runtime to run the function', - string: true, + type: 'string', + }).positional('runtimeVersion', { + describe: 'The runtime version to run the function', + type: 'string', + require: false }).option('global', { alias: 'g', describe: 'Adds the function into global configuration file', - boolean: true, + type: 'boolean', nargs: 0, default: false }).option('type', { alias: 'f', describe: 'Specifies which type of function will be added', choices: ['lib', 'bin'], - default: 'lib' + default: 'lib', + type: 'string' }).option('template', { alias: 't', describe: 'The template to render the function in case no source \ -file is found at the given path' +file is found at the given path', + type: 'string' }); }, - async ({ name, function: func, runtime, type, configPath, global, template }) => { + async ({ name, function: func, runtime, runtimeVersion, type, configPath, global, template }) => { try { // If func is relative, we must resolve it with our current working // directory before saving it into the configuration file if (func && typeof func === 'string' && !func.startsWith('/')){ func = path.resolve(process.cwd(), func); } - await this.addFunction(name, func, runtime, type, configPath || global, template); + await this.addFunction(name, func, runtime, runtimeVersion, type, configPath || global, template); process.exit(0); } catch (e) { yargsHelper.handleErrorMessage(e.message, e, 'function add'); @@ -346,14 +353,17 @@ file is found at the given path' 'runtime ', 'Manage runtimes of Ant framework', yargs => { yargs.command( - 'add [extensions..]', + 'add [extensions..]', 'Adds new runtime', yargs => { yargs.positional('name', { describe: 'The name of the runtime', - string: true + type: 'string' + }).positional('runtimeVersion', { + describe: 'The version of the runtime', + type: 'string' }).positional('bin', { describe: 'The path to the runtime', - string: true + type: 'string' }).positional('extensions', { describe: 'The extensions supported by the runtime', array: true @@ -365,37 +375,40 @@ file is found at the given path' default: false }); }, - async ({ name, bin, extensions, configPath, global }) => { + async ({ name, runtimeVersion, bin, extensions, configPath, global }) => { try { // If bin is relative, we must resolve it with our current working // directory before saving it into the configuration file if (bin && typeof bin === 'string' && !bin.startsWith('/')) { bin = path.resolve(process.cwd(), bin); } - await this.addRuntime(name, bin, extensions, configPath || global); + await this.addRuntime(name, runtimeVersion, bin, extensions, configPath || global); process.exit(0); } catch (e) { yargsHelper.handleErrorMessage(e.message, e, 'runtime add'); } } ).command( - 'remove [--global]', + 'remove [--global]', 'Removes a runtime', yargs => { yargs.positional('name', { describe: 'The name of the runtime to be removed', - string: true + type: 'string' + }).positional('runtimeVersion', { + describe: 'The version of the runtime to be removed', + type: 'string' }).option('global', { alias: 'g', describe: 'Removes runtime from global configuration file', - boolean: true, + type: 'boolean', nargs: 0, default: false }); }, async (argv) => { try { - const { name, configPath, global } = argv; - await this.removeRuntime(name, configPath || global); + const { name, runtimeVersion, configPath, global } = argv; + await this.removeRuntime(name, runtimeVersion, configPath || global); process.exit(0); } catch (e) { yargsHelper.handleErrorMessage(e.message, e, 'runtime remove'); @@ -539,14 +552,14 @@ file is found at the given path' case 'add': command = 'runtime add'; if (msg.includes('Not enough non-option arguments')) { - msg = 'Runtime add command requires name and bin arguments'; + msg = 'Runtime add command requires name, runtimeVersion and bin arguments'; createError = true; } break; case 'remove': command = 'runtime remove'; if (msg.includes('Not enough non-option arguments')) { - msg = 'Runtime remove command requires name argument'; + msg = 'Runtime remove command requires name and runtimeVersion arguments'; createError = true; } break; @@ -722,6 +735,7 @@ Considering "${template}" as the template files path.`); * @param {!String} name The name of the function to be added * @param {String} func The absolute path of the function * @param {String} runtime The name of the runtime that will run the function + * @param {String} version The runtime version * @param {String} type The type of the AntFunction that will be added * @param {String|Boolean} config The configuration file path whose function * will be added; or a flag indicating this change should be done on the @@ -729,7 +743,7 @@ Considering "${template}" as the template files path.`); * @param {String} template The name or path to the template under the category * "Function" to render the function source file when it does not exists */ - async addFunction(name, func, runtime, type = 'lib', config, template) { + async addFunction(name, func, runtime, version, type = 'lib', config, template) { config = Core._getConfig(config); assert(!template || typeof template === 'string', 'Param "template" must be a String'); if (template) { @@ -747,8 +761,8 @@ Considering "${template}" as the template files path.`); /* eslint-disable no-case-declarations */ let runtimeInstance; if (runtime) { - runtimeInstance = this.ant.runtimeController.getRuntime(runtime); - assert(runtimeInstance, `Runtime "${runtime}" was not found`); + runtimeInstance = this.ant.runtimeController.getRuntime(runtime, version); + assert(runtimeInstance, `Runtime "${runtime}${version ? ` ${version}` : ''}" was not found`); } else { runtimeInstance = this.ant.runtimeController.defaultRuntime; } @@ -773,7 +787,7 @@ Considering "${template}" as the template files path.`); const runtimeTemplate = this.ant.templateController.getTemplate('Function', runtime); template = runtimeTemplate || runtimeInstance.template; } - config.addFunction(new LibFunction(this.ant, name, func, runtimeInstance)); + config.addFunction(new LibFunction(this.ant, name, func, runtimeInstance), !!version); break; case 'bin': config.addFunction(new BinFunction(this.ant, name, func)); @@ -817,7 +831,7 @@ Considering "${template}" as the template files path.`); const additionalInfo = func instanceof BinFunction ? `: ${func.bin}` : func instanceof LibFunction - ? `: ${func.handler} ${func.runtime.name}` + ? `: ${func.handler} ${func.runtime.name} ${func.runtime.version}` : ''; console.log(`${func.constructor.name} ${func.name}${additionalInfo}`); }); @@ -844,16 +858,17 @@ Considering "${template}" as the template files path.`); * Adds a runtime into the configuration file and saves it. * * @param {!String} name The name of the runtime to be added + * @param {!String} version The version of the runtime * @param {!String} bin The absolute path to the runtime * @param {Array} extensions The extensions supported by the runtime * @param {String|Boolean} config The configuration file path whose runtime * will be added; or a flag indicating this change should be done on the * global configuration (if true), or local configuration (if false). */ - async addRuntime(name, bin, extensions, config) { + async addRuntime(name, version, bin, extensions, config) { config = Core._getConfig(config); return config.addRuntime(new Runtime( - this.ant, name, bin, extensions + this.ant, name, bin, extensions, undefined, version )).save(); } @@ -861,13 +876,14 @@ Considering "${template}" as the template files path.`); * Removes a runtime from the configuration file and saves it. * * @param {!String} name The name of the runtime to be removed + * @param {!String} version The version of the runtime to be removed * @param {String|Boolean} config The configuration file path whose runtime will be removed; * or a flag indicating this change should be done on the global configuration (if true), * or local configuration (if false). */ - async removeRuntime(name, config) { + async removeRuntime(name, version, config) { config = Core._getConfig(config); - return config.removeRuntime(name).save(); + return config.removeRuntime(name, version).save(); } /** @@ -877,9 +893,30 @@ Considering "${template}" as the template files path.`); async listRuntimes() { const runtimes = this.ant.runtimeController.runtimes; console.log('Listing all runtimes available \ -( [extensions]):'); - runtimes.forEach(({ name, bin, extensions }) => { - console.log(`${name} ${bin}${extensions ? ` ${extensions.join(' ')}` : ''}`); +([default] [extensions] [template]):'); + const defaultRuntimes = new Set(); // Needed to avoid printing the default runtime twice + Array.from(runtimes.values()).forEach(runtimeByVersion => { + for(const [key, runtimeInstance] of runtimeByVersion.entries()) { + if (defaultRuntimes.has(runtimeInstance)) { + continue; + } + const isDefault = key === 'default'; + if (isDefault) { + defaultRuntimes.add(runtimeInstance); + } + + // Building the console.log content + const { name, bin, extensions, version, template } = runtimeInstance; + let runtime = isDefault ? `default ${name}` : name; + runtime += ` ${version} ${bin}`; + if (extensions && extensions.length) { + runtime += ` [${extensions.join(', ')}]`; + } + if (template) { + runtime += ` ${template}`; + } + console.log(runtime); + } }); } diff --git a/plugins/ant-core/spec/lib/Core.spec.js b/plugins/ant-core/spec/lib/Core.spec.js index ce71011..494a439 100644 --- a/plugins/ant-core/spec/lib/Core.spec.js +++ b/plugins/ant-core/spec/lib/Core.spec.js @@ -1028,7 +1028,7 @@ describe('lib/Core.js', () => { }); jest.spyOn(Config.prototype, 'save'); const core = new Core(ant); - await core.addFunction(name, func, null, 'bin'); + await core.addFunction(name, func, null, null, 'bin'); expect(getLocalConfigPath).toHaveBeenCalled(); expect(Config.prototype.save).toHaveBeenCalled(); }); @@ -1045,7 +1045,7 @@ describe('lib/Core.js', () => { }; jest.spyOn(Core, '_getConfig').mockImplementation(() => configMock); const core = new Core(ant); - await core.addFunction(name, func, null, 'bin', true); + await core.addFunction(name, func, null, null, 'bin', true); expect(Core._getConfig).toHaveBeenCalledWith(true); expect(configMock.addFunction).toHaveBeenCalled(); expect(configMock.save).toHaveBeenCalled(); @@ -1055,7 +1055,7 @@ describe('lib/Core.js', () => { const ant = new Ant(); const name = 'myFunc'; const func = '/path/to/func'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime'); + const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', [], undefined, '1'); const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1072,9 +1072,9 @@ describe('lib/Core.js', () => { }); const save = jest.spyOn(Config.prototype, 'save'); const core = new Core(ant); - await core.addFunction(name, func, runtimeInstance.name, 'lib'); + await core.addFunction(name, func, runtimeInstance.name, runtimeInstance.version, 'lib'); expect(getLocalConfigPath).toHaveBeenCalled(); - expect(getRuntime).toHaveBeenCalledWith(runtimeInstance.name); + expect(getRuntime).toHaveBeenCalledWith(runtimeInstance.name, runtimeInstance.version); expect(save).toHaveBeenCalled(); }); @@ -1082,7 +1082,7 @@ describe('lib/Core.js', () => { const ant = new Ant(); const name = 'myFunc'; const func = '/path/to/func'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime'); + const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', [], undefined, '1'); const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1106,7 +1106,7 @@ describe('lib/Core.js', () => { test('should add LibFunction with default runtime and no defined path', async () => { const ant = new Ant(); const name = 'myFunc'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', ['foo']); + const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime', ['foo'], undefined, '1'); const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1153,7 +1153,7 @@ describe('lib/Core.js', () => { const name = 'myFunc'; const core = new Core(ant); fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); - const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js']); + const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js'], undefined, '1'); ant.runtimeController.loadRuntimes([fooRuntime]); const originalRender = Template.prototype.render; const render = Template.prototype.render = jest.fn(); @@ -1182,7 +1182,7 @@ describe('lib/Core.js', () => { const name = 'myFunc'; const core = new Core(ant); fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); - const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js']); + const fooRuntime = new Runtime(ant, 'Foo', '/bin/foo', ['js'], undefined, '1'); ant.runtimeController.loadRuntimes([fooRuntime]); const templateMocked = new Template('Function', 'myTemplate', '/myTemplate/path'); @@ -1190,7 +1190,7 @@ describe('lib/Core.js', () => { ant.templateController.loadTemplates([templateMocked]); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); const funcPath = path.resolve(outPath, 'foo/bar/myFunc.js'); - await core.addFunction(name, funcPath, 'Foo', undefined, undefined, 'myTemplate'); + await core.addFunction(name, funcPath, 'Foo', undefined, undefined, undefined, 'myTemplate'); expect(templateMocked.render).toHaveBeenCalledWith( funcPath, expect.any(Object) @@ -1235,7 +1235,7 @@ describe('lib/Core.js', () => { fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); try { - await core.addFunction(name, null, null, undefined, undefined, '/my/invalid/path'); + await core.addFunction(name, null, null, undefined, undefined, undefined, '/my/invalid/path'); } catch (err) { expect(err.message).toBe('Param "template" is not a valid path: /my/invalid/path'); } @@ -1246,9 +1246,9 @@ describe('lib/Core.js', () => { const core = new Core(ant); fs.ensureFileSync(path.resolve(outPath, 'ant.yml')); try { - await core.addFunction(null, null, 'should not find me'); + await core.addFunction(null, null, 'should not find me', '1'); } catch (err) { - expect(err.message).toBe('Runtime "should not find me" was not found'); + expect(err.message).toBe('Runtime "should not find me 1" was not found'); } }); @@ -1258,9 +1258,8 @@ describe('lib/Core.js', () => { const ant = new Ant(); const name = 'myFunc'; const func = '/path/to/func'; - const runtimeInstance = new Runtime(ant, 'myRuntime', '/path/to/runtime'); const core = new Core(ant); - expect(core.addFunction(name, func, runtimeInstance.name, 'foo')) + expect(core.addFunction(name, func, null, null, 'foo')) .rejects.toThrowError('AntFunction type "foo" is unknown'); }); @@ -1398,12 +1397,12 @@ describe('lib/Core.js', () => { }); describe('function ls command', () => { - test('should print templates', async () => { + test('should print functions', async () => { console.log = jest.fn(); const functions = [ new AntFunction(ant, 'ant', () => {}), new BinFunction(ant, 'foo', '/path/to/foo'), - new LibFunction(ant, 'bar', '/path/to/bar', new Runtime(ant, 'barRuntime', '/path/to/runtime')) + new LibFunction(ant, 'bar', '/path/to/bar', new Runtime(ant, 'barRuntime', '/path/to/runtime', [], undefined, '1')) ]; ant.functionController.getAllFunctions = jest.fn().mockImplementation(() => functions); const core = new Core(ant); @@ -1413,7 +1412,7 @@ describe('lib/Core.js', () => { ( [: (| )]):'); expect(console.log.mock.calls[1][0]).toBe('AntFunction ant'); expect(console.log.mock.calls[2][0]).toBe('BinFunction foo: /path/to/foo'); - expect(console.log.mock.calls[3][0]).toBe('LibFunction bar: /path/to/bar barRuntime'); + expect(console.log.mock.calls[3][0]).toBe('LibFunction bar: /path/to/bar barRuntime 1'); }); test('should handle error message', done => { @@ -1588,6 +1587,7 @@ describe('lib/Core.js', () => { describe('runtime add command', () => { test('should add runtime and save locally', async () => { const name = 'runtime'; + const version = '1'; const bin = '/my/runtime'; const extensions = [ 'js' ]; const configFilePath = path.resolve(outPath, 'ant.yml'); @@ -1600,12 +1600,13 @@ describe('lib/Core.js', () => { jest.spyOn(Config.prototype, 'addRuntime') .mockImplementation(runtime => { expect(runtime.name).toBe(name); + expect(runtime.version).toBe(version); expect(runtime.bin).toBe(bin); expect(runtime.extensions).toBe(extensions); return configMock; }); const core = new Core(ant); - await core.addRuntime(name, bin, extensions); + await core.addRuntime(name, version, bin, extensions); expect(getLocalConfigPath).toHaveBeenCalled(); expect(configMock.save).toHaveBeenCalled(); }); @@ -1613,7 +1614,8 @@ describe('lib/Core.js', () => { test('should add runtime and save locally v2', (done) => { const antCli = new AntCli(); const name = 'runtime'; - const bin = 'my/runtime'; + const version = '1'; + const bin = '/my/runtime'; const extensions = 'js'; const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); @@ -1625,11 +1627,12 @@ describe('lib/Core.js', () => { jest.spyOn(Config.prototype, 'addRuntime') .mockImplementation(runtime => { expect(runtime.name).toBe(name); + expect(runtime.version).toBe(version); expect(runtime.bin).toEqual(expect.stringContaining(bin)); expect(runtime.extensions).toEqual([extensions]); return configMock; }); - antCli._yargs.parse(`runtime add ${name} ${bin} ${extensions}`); + antCli._yargs.parse(`runtime add ${name} ${version} ${bin} ${extensions}`); process.exit = jest.fn((code) => { expect(code).toEqual(0); expect(getLocalConfigPath).toHaveBeenCalled(); @@ -1666,11 +1669,13 @@ describe('lib/Core.js', () => { test('should add runtime and save globally', async () => { const name = 'runtime'; + const version = '1'; const bin = '/my/runtime'; const extensions = [ 'js' ]; const configMock = { addRuntime: jest.fn(runtime => { expect(runtime.name).toBe(name); + expect(runtime.version).toBe(version); expect(runtime.bin).toBe(bin); expect(runtime.extensions).toBe(extensions); return configMock; @@ -1679,7 +1684,7 @@ describe('lib/Core.js', () => { }; jest.spyOn(Core, '_getConfig').mockImplementation(() => configMock); const core = new Core(ant); - await core.addRuntime(name, bin, extensions, true); + await core.addRuntime(name, version, bin, extensions, true); expect(Core._getConfig).toHaveBeenCalledWith(true); expect(configMock.addRuntime).toHaveBeenCalled(); expect(configMock.save).toHaveBeenCalled(); @@ -1690,7 +1695,7 @@ describe('lib/Core.js', () => { process.argv = ['runtime', 'add']; process.exit = jest.fn(code => { expect(handleErrorMessage).toHaveBeenCalledWith( - 'Runtime add command requires name and bin arguments', null, 'runtime add' + 'Runtime add command requires name, runtimeVersion and bin arguments', null, 'runtime add' ); expect(code).toEqual(1); done(); @@ -1698,12 +1703,25 @@ describe('lib/Core.js', () => { new Core(ant)._yargsFailed('Not enough non-option arguments'); }); - test('should show friendly error when bin was not passed', done => { + test('should show friendly error when version was not passed', done => { const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); process.argv = ['runtime', 'add', 'myruntime']; process.exit = jest.fn((code) => { expect(handleErrorMessage).toHaveBeenCalledWith( - 'Runtime add command requires name and bin arguments', null, 'runtime add' + 'Runtime add command requires name, runtimeVersion and bin arguments', null, 'runtime add' + ); + expect(code).toEqual(1); + done(); + }); + new Core(ant)._yargsFailed('Not enough non-option arguments'); + }); + + test('should show friendly error when bin was not passed', done => { + const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); + process.argv = ['runtime', 'add', 'myruntime 123']; + process.exit = jest.fn((code) => { + expect(handleErrorMessage).toHaveBeenCalledWith( + 'Runtime add command requires name, runtimeVersion and bin arguments', null, 'runtime add' ); expect(code).toEqual(1); done(); @@ -1722,6 +1740,7 @@ describe('lib/Core.js', () => { describe('runtime remove command', () => { test('should remove runtime and save locally', async () => { const name = 'myRuntime'; + const version = '1'; const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1729,15 +1748,16 @@ describe('lib/Core.js', () => { const removeRuntime = jest.spyOn(Config.prototype, 'removeRuntime'); const save = jest.spyOn(Config.prototype, 'save'); const core = new Core(ant); - await core.removeRuntime(name); + await core.removeRuntime(name, version); expect(getLocalConfigPath).toHaveBeenCalled(); - expect(removeRuntime).toHaveBeenCalled(); + expect(removeRuntime).toHaveBeenCalledWith(name, version); expect(save).toHaveBeenCalled(); }); test('should handle error message', async () => { const antCli = new AntCli(); const name = 'myRuntime'; + const version = '1'; const configFilePath = path.resolve(outPath, 'ant.yml'); fs.ensureFileSync(configFilePath); const getLocalConfigPath = jest.spyOn(Config, 'GetLocalConfigPath') @@ -1750,7 +1770,7 @@ describe('lib/Core.js', () => { yargsHelper, 'handleErrorMessage' ); - antCli._yargs.parse(`runtime remove ${name}`); + antCli._yargs.parse(`runtime remove ${name} ${version}`); process.exit = jest.fn(code => { expect(code).toEqual(1); expect(getLocalConfigPath).toHaveBeenCalled(); @@ -1762,18 +1782,20 @@ describe('lib/Core.js', () => { test('should remove runtime and save globally', async () => { const name = 'myRuntime'; + const version = '1'; const configMock = { - removeRuntime: jest.fn().mockImplementation(runtimeName => { + removeRuntime: jest.fn().mockImplementation((runtimeName, runtimeVersion) => { expect(runtimeName).toBe(name); + expect(runtimeVersion).toBe(version); return configMock; }), save: jest.fn() }; jest.spyOn(Core, '_getConfig').mockImplementation(() => configMock); const core = new Core(ant); - await core.removeRuntime(name, true); + await core.removeRuntime(name, version, true); expect(Core._getConfig).toHaveBeenCalledWith(true); - expect(configMock.removeRuntime).toHaveBeenCalled(); + expect(configMock.removeRuntime).toHaveBeenCalledWith(name, version); expect(configMock.save).toHaveBeenCalled(); }); @@ -1782,7 +1804,20 @@ describe('lib/Core.js', () => { process.argv = ['runtime', 'remove']; process.exit = jest.fn((code) => { expect(handleErrorMessage).toHaveBeenCalledWith( - 'Runtime remove command requires name argument', null, 'runtime remove' + 'Runtime remove command requires name and runtimeVersion arguments', null, 'runtime remove' + ); + expect(code).toEqual(1); + done(); + }); + new Core(ant)._yargsFailed('Not enough non-option arguments'); + }); + + test('should show friendly error when runtimeVersion was not passed', async done => { + const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); + process.argv = ['runtime', 'remove', 'node']; + process.exit = jest.fn((code) => { + expect(handleErrorMessage).toHaveBeenCalledWith( + 'Runtime remove command requires name and runtimeVersion arguments', null, 'runtime remove' ); expect(code).toEqual(1); done(); @@ -1802,41 +1837,45 @@ describe('lib/Core.js', () => { test('should print runtimes', async () => { console.log = jest.fn(); const runtimes = [ - new Runtime(ant, 'foo', '/path/to/foo', ['foo', 'js']), - new Runtime(ant, 'bar', '/path/to/bar', ['bar']), - new Runtime(ant, 'lorem', '/ipsum') + new Runtime(ant, 'foo', '/path/to/foo', ['foo', 'js'], '/foo/template', '4.0.0'), + new Runtime(ant, 'bar', '/path/to/bar', ['bar'], undefined, '3.2.1'), + new Runtime(ant, 'lorem', '/ipsum', [], undefined, '1'), + new Runtime(ant, 'lorem', '/ipsum', [], undefined, '2') ]; ant.runtimeController._runtimes = new Map(); ant.runtimeController.loadRuntimes(runtimes); const core = new Core(ant); await core.listRuntimes(); - expect(console.log.mock.calls.length).toBe(4); + expect(console.log.mock.calls.length).toBe(5); expect(console.log.mock.calls[0][0]).toBe('Listing all runtimes available \ -( [extensions]):'); - expect(console.log.mock.calls[1][0]).toBe('foo /path/to/foo foo js'); - expect(console.log.mock.calls[2][0]).toBe('bar /path/to/bar bar'); - expect(console.log.mock.calls[3][0]).toBe('lorem /ipsum'); +([default] [extensions] [template]):'); + expect(console.log.mock.calls[1][0]).toBe('default foo 4 /path/to/foo [foo, js] /foo/template'); + expect(console.log.mock.calls[2][0]).toBe('default bar 3 /path/to/bar [bar]'); + expect(console.log.mock.calls[3][0]).toBe('default lorem 1 /ipsum'); + expect(console.log.mock.calls[4][0]).toBe('lorem 2 /ipsum'); }); test('should print runtimes v2', (done) => { console.log = jest.fn(); const antCli = new AntCli(); const runtimes = [ - new Runtime(antCli._ant, 'foo', '/path/to/foo', ['foo', 'js']), - new Runtime(antCli._ant, 'bar', '/path/to/bar', ['bar']), - new Runtime(antCli._ant, 'lorem', '/ipsum') + new Runtime(antCli._ant, 'foo', '/path/to/foo', ['foo', 'js'], '/foo/template', '4.0.0'), + new Runtime(antCli._ant, 'bar', '/path/to/bar', ['bar'], undefined, '3.2.1'), + new Runtime(antCli._ant, 'lorem', '/ipsum', [], undefined, '1'), + new Runtime(antCli._ant, 'lorem', '/ipsum', [], undefined, '2') ]; antCli._ant.runtimeController._runtimes = new Map(); antCli._ant.runtimeController.loadRuntimes(runtimes); antCli._yargs.parse('runtime ls'); process.exit = jest.fn(code => { expect(code).toEqual(1); - expect(console.log.mock.calls.length).toBe(4); + expect(console.log.mock.calls.length).toBe(5); expect(console.log.mock.calls[0][0]).toBe('Listing all runtimes available \ -( [extensions]):'); - expect(console.log.mock.calls[1][0]).toBe('foo /path/to/foo foo js'); - expect(console.log.mock.calls[2][0]).toBe('bar /path/to/bar bar'); - expect(console.log.mock.calls[3][0]).toBe('lorem /ipsum'); +([default] [extensions] [template]):'); + expect(console.log.mock.calls[1][0]).toBe('default foo 4 /path/to/foo [foo, js] /foo/template'); + expect(console.log.mock.calls[2][0]).toBe('default bar 3 /path/to/bar [bar]'); + expect(console.log.mock.calls[3][0]).toBe('default lorem 1 /ipsum'); + expect(console.log.mock.calls[4][0]).toBe('lorem 2 /ipsum'); done(); }); }); diff --git a/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js b/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js index 2331f9d..04d3cc8 100644 --- a/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js +++ b/plugins/ant-graphql/spec/lib/directives/DirectiveController.spec.js @@ -13,7 +13,7 @@ const DirectiveController = require( const ant = new Ant(); const fooFunction = new AntFunction(ant, 'fooFunction'); const barFunction = new LibFunction(ant, 'barFunction', '/my/handler', - new Runtime(ant, 'libRuntime', '/lib/runtime', ['js']) + new Runtime(ant, 'libRuntime', '/lib/runtime', ['js'], undefined, '1') ); const handler = '/foo/handler'; const runtime = 'Node'; diff --git a/plugins/ant-serverless/spec/lib/Serverless.spec.js b/plugins/ant-serverless/spec/lib/Serverless.spec.js index 75f3475..360a8cc 100644 --- a/plugins/ant-serverless/spec/lib/Serverless.spec.js +++ b/plugins/ant-serverless/spec/lib/Serverless.spec.js @@ -79,7 +79,7 @@ describe('lib/Serverless.js', () => { try { fs.ensureDirSync(basePath); } finally { - (new Template( + await (new Template( 'service', 'FooService', path.resolve( @@ -266,7 +266,7 @@ describe('lib/Serverless.js', () => { try { fs.ensureDirSync(basePath); } finally { - (new Template( + await (new Template( 'service', 'FooService', path.resolve( From 0689458c8cdbe10c13c5880f793baac9f1674002 Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Fri, 19 Oct 2018 19:38:44 -0300 Subject: [PATCH 2/4] feat: Add Java runtime --- packages/ant/lib/config/Config.js | 4 +- packages/ant/lib/functions/LibFunction.js | 20 +++++- .../spec/lib/functions/LibFunction.spec.js | 2 +- plugins/ant-core/functions/javaRuntime.js | 49 +++++++++++++ plugins/ant-core/lib/Core.js | 8 +++ .../templates/function/java.java.mustache | 69 +++++++++++++++++++ plugins/ant-graphql/functions/resolve.js | 25 +++++-- plugins/ant-graphql/lib/GraphQL.js | 25 ++++--- 8 files changed, 180 insertions(+), 22 deletions(-) create mode 100755 plugins/ant-core/functions/javaRuntime.js create mode 100644 plugins/ant-core/templates/function/java.java.mustache diff --git a/packages/ant/lib/config/Config.js b/packages/ant/lib/config/Config.js index 60b7f27..d73762f 100644 --- a/packages/ant/lib/config/Config.js +++ b/packages/ant/lib/config/Config.js @@ -679,7 +679,7 @@ provider "${providerName}"` } return Object.keys(functions).map(name => { const func = functions[name]; - const { bin, handler, runtime } = func; + const { bin, handler, runtime, args } = func; try { if (bin) { return new BinFunction(runtimeController.ant, name, bin); @@ -694,7 +694,7 @@ provider "${providerName}"` } else { runtimeInstance = runtimeController.defaultRuntime; } - return new LibFunction(runtimeController.ant, name, handler, runtimeInstance); + return new LibFunction(runtimeController.ant, name, handler, runtimeInstance, args); } throw new AntError(`Function type unknown: ${JSON.stringify(func)}`); } catch (e) { diff --git a/packages/ant/lib/functions/LibFunction.js b/packages/ant/lib/functions/LibFunction.js index ea3d6a1..61a8666 100644 --- a/packages/ant/lib/functions/LibFunction.js +++ b/packages/ant/lib/functions/LibFunction.js @@ -25,7 +25,7 @@ class LibFunction extends AntFunction { * @throws {AssertionError} If "ant", "name", "handler" or "runtime" params * are not valid. */ - constructor(ant, name, handler, runtime) { + constructor(ant, name, handler, runtime, args) { super(ant, name); assert( @@ -51,6 +51,8 @@ class LibFunction extends AntFunction { * @private */ this._runtime = runtime; + + this._args = args || []; } /** @@ -80,16 +82,28 @@ class LibFunction extends AntFunction { run() { logger.log(`Running lib function ${this.name}...`); + const args = JSON.stringify( + this._args.concat( + // Filters null or undefined arguments and strigifies the rest + Array.from(arguments).filter(arg => arg).map(arg => { + try { + return typeof arg === 'string' ? arg : JSON.stringify(arg); + } catch (err) { + return arg.toString(); + } + }) + ) + ); try { return this._runtime.run([ this._handler, - JSON.stringify(Array.from(arguments)) + args ]).pipe(map(data => { // JSON fails to parse 'undefined', but not '"undefined"' try { return JSON.parse(data); } catch (e) { - return undefined; + return data ? data : undefined; } })); } catch (e) { diff --git a/packages/ant/spec/lib/functions/LibFunction.spec.js b/packages/ant/spec/lib/functions/LibFunction.spec.js index 557dab2..72bc25f 100644 --- a/packages/ant/spec/lib/functions/LibFunction.spec.js +++ b/packages/ant/spec/lib/functions/LibFunction.spec.js @@ -74,7 +74,7 @@ describe('lib/functions/LibFunction.js', () => { const runReturn = undefinedlibFunction.run(); expect(runReturn).toEqual(expect.any(Observable)); expect(await runReturn.toPromise()) - .toEqual(undefined); + .toEqual(expect.stringContaining('undefined')); }); test('should fail if runtime fails', () => { diff --git a/plugins/ant-core/functions/javaRuntime.js b/plugins/ant-core/functions/javaRuntime.js new file mode 100755 index 0000000..6679ebf --- /dev/null +++ b/plugins/ant-core/functions/javaRuntime.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +/** + * @fileoverview Java runtime for Ant Framework. + */ +const { spawn, spawnSync } = require('child_process'); +const { parse } = require('path'); +const { argv, stdout, stderr } = process; + +/** + * Flushes the stdout and stderr and exits the process. + * + * @param {Number} code The status code + */ +const exit = code => { + // On "close", waits stdout and stderr to flush + // and then exits. If it is already done, + // the cb is called anyway + stdout.end(() => { + stderr.end(() => { + process.exit(code); + }); + }); +}; + +let javaFile = argv[2]; +const { dirname, name: fileName, ext } = parse(javaFile); + +let args = []; +const options = {}; +if (ext === '.jar') { + args.push('-cp'); +} else if (ext === '.java') { + const { error, status, stderr: javacStderr, stdout: javacStdout } = spawnSync('javac', [javaFile]); + if (error) { + stdout.write(javacStdout); + stderr.write(javacStderr); + exit(status); + } + options.cwd = dirname; + javaFile = fileName; +} +args.push(javaFile); +args = args.concat(JSON.parse(argv[3])); + +const javaProgram = spawn('java', args, options); +javaProgram.stdout.on('data', data => stdout.write(data.toString())); +javaProgram.stderr.on('data', data => stderr.write(data.toString())); +javaProgram.on('close', exit); diff --git a/plugins/ant-core/lib/Core.js b/plugins/ant-core/lib/Core.js index 1e68052..8bfe9d3 100644 --- a/plugins/ant-core/lib/Core.js +++ b/plugins/ant-core/lib/Core.js @@ -53,6 +53,14 @@ class Core extends Plugin { ['js'], path.resolve(__dirname, '../templates/function/node.js.mustache'), '10' + ), + new Runtime( + this._ant, + 'Java', + path.resolve(__dirname, '../functions/javaRuntime.js'), + ['jar', 'java'], + path.resolve(__dirname, '../templates/function/java.java.mustache'), + '8' ) ]; } diff --git a/plugins/ant-core/templates/function/java.java.mustache b/plugins/ant-core/templates/function/java.java.mustache new file mode 100644 index 0000000..5d744b7 --- /dev/null +++ b/plugins/ant-core/templates/function/java.java.mustache @@ -0,0 +1,69 @@ +import java.lang.StringBuffer; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An example class which invokes a RESTful API and returns its content + * to the Ant Framework GraphQL API. + */ +public class {{name}} { + /** + * The args[0] represents the field parameters from the GraphQL query. + */ + public static void main(String[] args) throws IOException { + /** + * The parameters are stored in a stringified JSON object. + * So, in order to make use of them, we need to parse it. + */ + String cityName = parseJson(args[0]); + try { + // Opening connection to the RESTful API + String url = "http://api.openweathermap.org/data/2.5/weather?q=" + + URLEncoder.encode(cityName, "UTF-8") + + "&appid=464ddf23d2c714ee8ecbc5b39f1f7eae"; + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + con.setRequestMethod("GET"); + + // Retrieving the response content + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + // Prints the response to the stdout, which Ant is watching. + System.out.print(response.toString()); + } catch (Exception e) { + // If any exception occurs, we should also let Ant be aware of it. + System.out.print(e.toString()); + } + } + + /** + * Since this file is only for testing purposes and we aren't importing any + * external libs, we parse the JSON using a regex. + * + * In this example, we are expecting the "city" parameter. So + * all we need to do here is retrieve it from the JSON and return + * its value, which is a String. + */ + public static String parseJson(jsonAsString) { + String cityJsonRegex = "(?:\\\"city\\\":\\\")(.*?)(?:\\\")"; + Pattern pattern = Pattern.compile(cityJsonRegex); + Matcher matcher = pattern.matcher(jsonAsString); + if (matcher.find()) { + String cityName = matcher.group().split(":")[1].replaceAll("\\\"", ""); + return cityName; + } + return null; + } +} diff --git a/plugins/ant-graphql/functions/resolve.js b/plugins/ant-graphql/functions/resolve.js index 4a75ece..4c60c13 100644 --- a/plugins/ant-graphql/functions/resolve.js +++ b/plugins/ant-graphql/functions/resolve.js @@ -23,9 +23,12 @@ async function resolve (ant, resolveArgs, fieldArgs, currentValue, model) { return null; } try { - currentValue = antFunction.run( - currentValue !== undefined ? currentValue : fieldArgs - ); + const args = currentValue || + (fieldArgs && (!typeof fieldArgs === 'object' || (typeof fieldArgs === 'object' && Object.keys(fieldArgs).length > 0)) + ? fieldArgs + : undefined); + currentValue = antFunction.run(args); + let functionResult; if (currentValue instanceof Observable) { if ( field && @@ -33,13 +36,23 @@ async function resolve (ant, resolveArgs, fieldArgs, currentValue, model) { field.astNode.type && field.astNode.type.kind === 'ListType' ) { - return await currentValue.pipe(toArray()).toPromise(); + functionResult = await currentValue.pipe(toArray()).toPromise(); } else { - return await currentValue.toPromise(); + functionResult = await currentValue.toPromise(); } } else { - return currentValue; + functionResult = currentValue; } + // Handles Objects in order to avoid GraphQL responding + // "[object Object]", which is useless + if (Array.isArray(functionResult)) { + functionResult = functionResult.map( + result => typeof result === 'object' ? JSON.stringify(result) : result + ); + } else if (typeof functionResult === 'object') { + functionResult = JSON.stringify(functionResult); + } + return functionResult; } catch (e) { logger.error(new AntError( `Could not run "${resolveArgs.to}" function`, diff --git a/plugins/ant-graphql/lib/GraphQL.js b/plugins/ant-graphql/lib/GraphQL.js index cd13e7a..1abdcf1 100644 --- a/plugins/ant-graphql/lib/GraphQL.js +++ b/plugins/ant-graphql/lib/GraphQL.js @@ -17,6 +17,7 @@ const mock = require('../functions/mock'); const resolve = require('../functions/resolve'); const subscribe = require('../functions/subscribe'); +const { stdout, stderr } = process; const defaultServerPath = path.dirname( require.resolve('@back4app/ant-graphql-express') ); @@ -249,7 +250,7 @@ directory "${cwd}"` this._serverProcess.stdout.on('data', (data) => { data = data.toString(); - console.log(`Server => ${data}`); + stdout.write(`Server => ${data}`); const successMessage = 'GraphQL API server listening for requests on '; @@ -269,7 +270,7 @@ directory "${cwd}"` }); this._serverProcess.stderr.on('data', (data) => { - console.error(`Server => ${data}`); + stderr.write(`Server => ${data}`); }); const promise = new Promise((resolve, reject) => { @@ -281,14 +282,18 @@ directory "${cwd}"` }); this._serverProcess.on('close', (code) => { - const message = `Server process closed with code "${code}"`; - logger.log(message); - logger.log('Stopping service...'); - if (code === 0) { - resolve(); - } else { - reject(new AntError(message)); - } + stdout.end(() => { + stderr.end(() => { + const message = `Server process closed with code "${code}"`; + logger.log(message); + logger.log('Stopping service...'); + if (code === 0) { + resolve(); + } else { + reject(new AntError(message)); + } + }); + }); }); }); From 1882afc249cd4760ef0af17b355d0c2df8203650 Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Mon, 22 Oct 2018 17:31:19 -0300 Subject: [PATCH 3/4] feat: Add function args definition on configuration files Adds the `args` attribute on function definition, which allows users to defined fixed arguments to the function when executed. It is useful for defining the main class, for instance, when executing Java functions inside JAR files. --- .../functions/objectLibFunction.js | 15 ++++++++++ packages/ant/lib/functions/LibFunction.js | 30 ++++++++++++------- packages/ant/spec/lib/config/Config.spec.js | 4 ++- .../spec/lib/functions/LibFunction.spec.js | 25 ++++++++++++++-- plugins/ant-core/functions/javaRuntime.js | 16 +++++++++- plugins/ant-graphql/functions/resolve.js | 25 ++++------------ plugins/ant-graphql/lib/GraphQL.js | 20 +++++-------- plugins/ant-graphql/spec/lib/GraphQL.spec.js | 25 ++++++++-------- 8 files changed, 101 insertions(+), 59 deletions(-) create mode 100644 packages/ant-util-tests/functions/objectLibFunction.js diff --git a/packages/ant-util-tests/functions/objectLibFunction.js b/packages/ant-util-tests/functions/objectLibFunction.js new file mode 100644 index 0000000..c4a6e9e --- /dev/null +++ b/packages/ant-util-tests/functions/objectLibFunction.js @@ -0,0 +1,15 @@ +/** + * @fileoverview Object lib function for testing purposes. + */ + +const { Observable } = require('rxjs'); + +module.exports = () => { + return Observable.create( + subscriber => { + subscriber.next({ foo: 'bar' }); + subscriber.complete(); + return () => {}; + } + ); +}; diff --git a/packages/ant/lib/functions/LibFunction.js b/packages/ant/lib/functions/LibFunction.js index 61a8666..0b731b2 100644 --- a/packages/ant/lib/functions/LibFunction.js +++ b/packages/ant/lib/functions/LibFunction.js @@ -52,6 +52,11 @@ class LibFunction extends AntFunction { */ this._runtime = runtime; + /** + * Contains the fixed arguments that will be used when running the function. + * @type {Array} + * @private + */ this._args = args || []; } @@ -73,6 +78,15 @@ class LibFunction extends AntFunction { return this._runtime; } + /** + * Contains the function fixed execution argumnets. + * @type {Array} + * @readonly + */ + get args() { + return this._args; + } + /** * Runs the function. It can receive different arguments depending on the * function instance. @@ -83,16 +97,7 @@ class LibFunction extends AntFunction { logger.log(`Running lib function ${this.name}...`); const args = JSON.stringify( - this._args.concat( - // Filters null or undefined arguments and strigifies the rest - Array.from(arguments).filter(arg => arg).map(arg => { - try { - return typeof arg === 'string' ? arg : JSON.stringify(arg); - } catch (err) { - return arg.toString(); - } - }) - ) + this._args.concat(Array.from(arguments)) ); try { return this._runtime.run([ @@ -103,7 +108,10 @@ class LibFunction extends AntFunction { try { return JSON.parse(data); } catch (e) { - return data ? data : undefined; + if (typeof data === 'string' && data.trim() === 'undefined') { + return undefined; + } + return data; } })); } catch (e) { diff --git a/packages/ant/spec/lib/config/Config.spec.js b/packages/ant/spec/lib/config/Config.spec.js index bba87c5..f6aa05a 100644 --- a/packages/ant/spec/lib/config/Config.spec.js +++ b/packages/ant/spec/lib/config/Config.spec.js @@ -982,7 +982,8 @@ Template category value is not an object!' }, Foo: { handler: '/foo/bar', - runtime: 'python' + runtime: 'python', + args: ['--foo', '--bar'] } }; const runtimeController = { @@ -1004,6 +1005,7 @@ Template category value is not an object!' expect(func).toBeInstanceOf(LibFunction); expect(func.name).toBe('Foo'); expect(func.handler).toBe(functions.Foo.handler); + expect(func.args).toBe(functions.Foo.args); expect(runtimeController.getRuntime.mock.calls.length).toBe(2); expect(runtimeController.getRuntime.mock.calls[0][0]).toBe(functions.MyLib.runtime); diff --git a/packages/ant/spec/lib/functions/LibFunction.spec.js b/packages/ant/spec/lib/functions/LibFunction.spec.js index 72bc25f..4d46e01 100644 --- a/packages/ant/spec/lib/functions/LibFunction.spec.js +++ b/packages/ant/spec/lib/functions/LibFunction.spec.js @@ -3,7 +3,7 @@ */ const path = require('path'); -const { Observable } = require('rxjs'); +const { Observable, of } = require('rxjs'); const { toArray } = require('rxjs/operators'); const Ant = require('../../../lib/Ant'); const LibFunction = require('../../../lib/functions/LibFunction'); @@ -74,7 +74,7 @@ describe('lib/functions/LibFunction.js', () => { const runReturn = undefinedlibFunction.run(); expect(runReturn).toEqual(expect.any(Observable)); expect(await runReturn.toPromise()) - .toEqual(expect.stringContaining('undefined')); + .toEqual(undefined); }); test('should fail if runtime fails', () => { @@ -96,5 +96,26 @@ describe('lib/functions/LibFunction.js', () => { )).run(); }).toThrowError('Could not run lib function fooLibFunction'); }); + + test('should run and handle multiple types result', async () => { + // Mocks the runtime.run function + const mockRuntime = new Runtime(ant, 'mockRuntime', 'bin', [], undefined, '1'); + const values = [ + 1, + 'a', + true, + { foo: { + bar: false + }}, + ['lorem', 'ipsum'], + undefined, + null + ]; + mockRuntime.run = jest.fn(() => of(...values)); + const typeslibAntFunction = new LibFunction(ant, 'foo', 'bar', mockRuntime); + const runReturn = typeslibAntFunction.run(); + expect(await runReturn.pipe(toArray()).toPromise()) + .toEqual(values); + }); }); }); diff --git a/plugins/ant-core/functions/javaRuntime.js b/plugins/ant-core/functions/javaRuntime.js index 6679ebf..f4a05dc 100755 --- a/plugins/ant-core/functions/javaRuntime.js +++ b/plugins/ant-core/functions/javaRuntime.js @@ -41,7 +41,21 @@ if (ext === '.jar') { javaFile = fileName; } args.push(javaFile); -args = args.concat(JSON.parse(argv[3])); + +// Filters null or undefined arguments and non serializable items +args = args.concat(JSON.parse(argv[3])) + .filter(arg => { + if (arg === undefined || arg === null) { + return false; + } + try { + JSON.stringify(arg); + return true; + } catch (err) { + return false; + } + }) + .map(arg => typeof arg === 'string' ? arg : JSON.stringify(arg)); const javaProgram = spawn('java', args, options); javaProgram.stdout.on('data', data => stdout.write(data.toString())); diff --git a/plugins/ant-graphql/functions/resolve.js b/plugins/ant-graphql/functions/resolve.js index 4c60c13..4a75ece 100644 --- a/plugins/ant-graphql/functions/resolve.js +++ b/plugins/ant-graphql/functions/resolve.js @@ -23,12 +23,9 @@ async function resolve (ant, resolveArgs, fieldArgs, currentValue, model) { return null; } try { - const args = currentValue || - (fieldArgs && (!typeof fieldArgs === 'object' || (typeof fieldArgs === 'object' && Object.keys(fieldArgs).length > 0)) - ? fieldArgs - : undefined); - currentValue = antFunction.run(args); - let functionResult; + currentValue = antFunction.run( + currentValue !== undefined ? currentValue : fieldArgs + ); if (currentValue instanceof Observable) { if ( field && @@ -36,23 +33,13 @@ async function resolve (ant, resolveArgs, fieldArgs, currentValue, model) { field.astNode.type && field.astNode.type.kind === 'ListType' ) { - functionResult = await currentValue.pipe(toArray()).toPromise(); + return await currentValue.pipe(toArray()).toPromise(); } else { - functionResult = await currentValue.toPromise(); + return await currentValue.toPromise(); } } else { - functionResult = currentValue; + return currentValue; } - // Handles Objects in order to avoid GraphQL responding - // "[object Object]", which is useless - if (Array.isArray(functionResult)) { - functionResult = functionResult.map( - result => typeof result === 'object' ? JSON.stringify(result) : result - ); - } else if (typeof functionResult === 'object') { - functionResult = JSON.stringify(functionResult); - } - return functionResult; } catch (e) { logger.error(new AntError( `Could not run "${resolveArgs.to}" function`, diff --git a/plugins/ant-graphql/lib/GraphQL.js b/plugins/ant-graphql/lib/GraphQL.js index 1abdcf1..171265c 100644 --- a/plugins/ant-graphql/lib/GraphQL.js +++ b/plugins/ant-graphql/lib/GraphQL.js @@ -282,18 +282,14 @@ directory "${cwd}"` }); this._serverProcess.on('close', (code) => { - stdout.end(() => { - stderr.end(() => { - const message = `Server process closed with code "${code}"`; - logger.log(message); - logger.log('Stopping service...'); - if (code === 0) { - resolve(); - } else { - reject(new AntError(message)); - } - }); - }); + const message = `Server process closed with code "${code}"`; + logger.log(message); + logger.log('Stopping service...'); + if (code === 0) { + resolve(); + } else { + reject(new AntError(message)); + } }); }); diff --git a/plugins/ant-graphql/spec/lib/GraphQL.spec.js b/plugins/ant-graphql/spec/lib/GraphQL.spec.js index ace2dc8..6009076 100644 --- a/plugins/ant-graphql/spec/lib/GraphQL.spec.js +++ b/plugins/ant-graphql/spec/lib/GraphQL.spec.js @@ -73,9 +73,7 @@ describe('lib/GraphQL.js', () => { }); test('should fail if server crashes', async () => { - const originalError = console.error; - console.error = jest.fn(); - expect.hasAssertions(); + const write = jest.spyOn(process.stderr, 'write').mockImplementation(() => {}); const bin = path.resolve( utilPath, 'templates/crashServerTemplate/server.js' @@ -92,10 +90,11 @@ describe('lib/GraphQL.js', () => { expect(e.message).toEqual( expect.stringContaining('Server process closed with code "1"') ); - expect(console.error).toHaveBeenCalledWith(expect.stringContaining( + expect(write).toHaveBeenCalledWith(expect.stringContaining( 'Crashed' )); - console.error = originalError; + } finally { + jest.restoreAllMocks(); } }); @@ -177,10 +176,10 @@ describe('lib/GraphQL.js', () => { expect.hasAssertions(); const originalExec = childProcess.exec; childProcess.exec = jest.fn(); - const originalError = console.error; - const originalLog = console.log; - console.error = jest.fn(); - console.log = jest.fn(); + const originalErrWrite = process.stderr.write; + process.stderr.write = jest.fn(); + const originalOutWrite = process.stdout.write; + process.stdout.write = jest.fn(); const model = path.resolve( utilPath, 'configs/graphQLPluginConfig/model.graphql' @@ -192,14 +191,14 @@ describe('lib/GraphQL.js', () => { const server = { bin }; const graphQL = new GraphQL(ant, { model, server }); await graphQL.startService(); - expect(console.error).toHaveBeenCalledWith( + expect(process.stderr.write).toHaveBeenCalledWith( expect.stringContaining('Some server error') ); - expect(console.log).toHaveBeenCalledWith( + expect(process.stdout.write).toHaveBeenCalledWith( expect.stringContaining('Some other log') ); - console.error = originalError; - console.log = originalLog; + process.stderr.write = originalErrWrite; + process.stdout.write = originalOutWrite; childProcess.exec = originalExec; }); From 59b4615f24d702dba1320f9055f82972d9fff7ea Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Mon, 22 Oct 2018 17:34:17 -0300 Subject: [PATCH 4/4] test: Fix Core.spec.js tests and coverage --- plugins/ant-core/spec/lib/Core.spec.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/ant-core/spec/lib/Core.spec.js b/plugins/ant-core/spec/lib/Core.spec.js index 6dbc5b0..e8a359a 100644 --- a/plugins/ant-core/spec/lib/Core.spec.js +++ b/plugins/ant-core/spec/lib/Core.spec.js @@ -1864,9 +1864,9 @@ describe('lib/Core.js', () => { ]; antCli._ant.runtimeController._runtimes = new Map(); antCli._ant.runtimeController.loadRuntimes(runtimes); - antCli._yargs.parse('runtime ls'); + process.exit = jest.fn(code => { - expect(code).toEqual(1); + expect(code).toBe(0); expect(console.log.mock.calls.length).toBe(5); expect(console.log.mock.calls[0][0]).toBe('Listing all runtimes available \ ([default] [extensions] [template]):'); @@ -1876,6 +1876,21 @@ describe('lib/Core.js', () => { expect(console.log.mock.calls[4][0]).toBe('lorem 2 /ipsum'); done(); }); + antCli._yargs.parse('runtime ls'); + }); + + test('should not show "runtime ls" friendly error when error is unknown', () => { + const handleErrorMessage = jest.spyOn(yargsHelper, 'handleErrorMessage'); + const error = new Error('Mocked error'); + const antCli = new AntCli(); + antCli._ant.pluginController.getPlugin('Core').listRuntimes = jest.fn(async () => { + throw error; + }); + process.exit = jest.fn(code => { + expect(code).toBe(1); + expect(handleErrorMessage).toHaveBeenCalledWith(error.message, error, 'runtime ls'); + }); + antCli._yargs.parse('runtime ls'); }); }); });