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/config/Config.js b/packages/ant/lib/config/Config.js index 7cb29fb..5377af6 100644 --- a/packages/ant/lib/config/Config.js +++ b/packages/ant/lib/config/Config.js @@ -813,7 +813,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); @@ -828,7 +828,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..0b731b2 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,13 @@ class LibFunction extends AntFunction { * @private */ this._runtime = runtime; + + /** + * Contains the fixed arguments that will be used when running the function. + * @type {Array} + * @private + */ + this._args = args || []; } /** @@ -71,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. @@ -80,16 +96,22 @@ class LibFunction extends AntFunction { run() { logger.log(`Running lib function ${this.name}...`); + const args = JSON.stringify( + this._args.concat(Array.from(arguments)) + ); 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; + 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 64e9024..c026bf8 100644 --- a/packages/ant/spec/lib/config/Config.spec.js +++ b/packages/ant/spec/lib/config/Config.spec.js @@ -1136,7 +1136,8 @@ Template category value is not an object!' }, Foo: { handler: '/foo/bar', - runtime: 'python' + runtime: 'python', + args: ['--foo', '--bar'] } }; const runtimeController = { @@ -1158,6 +1159,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 557dab2..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'); @@ -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 new file mode 100755 index 0000000..f4a05dc --- /dev/null +++ b/plugins/ant-core/functions/javaRuntime.js @@ -0,0 +1,63 @@ +#!/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); + +// 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())); +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 f85ada2..430c79e 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/spec/lib/Core.spec.js b/plugins/ant-core/spec/lib/Core.spec.js index 0a7371a..ed6bc75 100644 --- a/plugins/ant-core/spec/lib/Core.spec.js +++ b/plugins/ant-core/spec/lib/Core.spec.js @@ -1836,9 +1836,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]):'); @@ -1848,6 +1848,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'); }); }); }); 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/lib/GraphQL.js b/plugins/ant-graphql/lib/GraphQL.js index a03353b..704978f 100644 --- a/plugins/ant-graphql/lib/GraphQL.js +++ b/plugins/ant-graphql/lib/GraphQL.js @@ -20,6 +20,7 @@ const Map = require('yaml/map').default; const Pair = require('yaml/pair').default; const Scalar = require('yaml/scalar').default; +const { stdout, stderr } = process; const defaultServerPath = path.dirname( require.resolve('@back4app/ant-graphql-express') ); @@ -340,7 +341,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 '; @@ -360,7 +361,7 @@ directory "${cwd}"` }); this._serverProcess.stderr.on('data', (data) => { - console.error(`Server => ${data}`); + stderr.write(`Server => ${data}`); }); const promise = new Promise((resolve, reject) => { diff --git a/plugins/ant-graphql/spec/lib/GraphQL.spec.js b/plugins/ant-graphql/spec/lib/GraphQL.spec.js index 7ea1717..f55d884 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,18 +191,18 @@ 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; }); - test('should show success message', async (done) => { + test('should show success message', async () => { expect.hasAssertions(); const originalCwd = process.cwd(); process.chdir(path.resolve( @@ -234,7 +233,6 @@ for requests on http://localhost:3000') expect(e.message).toEqual( expect.stringContaining('Server process closed') ); - done(); } }); @@ -912,7 +910,7 @@ plugins: const graphQL = new GraphQL(ant); graphQL.directiveController.loadDirectives([ new Directive(ant, 'myDir1', 'myDef1', new AntFunction(ant, 'myFunc1', () => {})), - new Directive(ant, 'myDir2', 'myDef2', new LibFunction(ant, 'myFunc2', 'myHandler2', new Runtime(ant, 'myRuntime2', 'myBin2', ['foo']))) + new Directive(ant, 'myDir2', 'myDef2', new LibFunction(ant, 'myFunc2', 'myHandler2', new Runtime(ant, 'myRuntime2', 'myBin2', ['foo'], undefined, '1'))) ]); graphQL.listDirectives(); expect(console.log).toHaveBeenCalledTimes(3);