From 573a292e16817b1d36cd600abf3bda8cba5bce47 Mon Sep 17 00:00:00 2001 From: d98762625 Date: Fri, 15 Feb 2019 15:40:29 +0000 Subject: [PATCH] WIP dynamically define async functions in Dish, only if needed --- src/core/Dish.mjs | 345 ++++++++++++++++++++++++++++-------------- src/node/NodeDish.mjs | 114 -------------- src/node/api.mjs | 105 ++++++++----- 3 files changed, 301 insertions(+), 263 deletions(-) diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs index 30b8621d..95d821d8 100755 --- a/src/core/Dish.mjs +++ b/src/core/Dish.mjs @@ -10,6 +10,229 @@ import DishError from "./errors/DishError"; import BigNumber from "bignumber.js"; import log from "loglevel"; + +/** + * Translates the data to the given type format. + * + * @param {number} toType - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + */ +async function _asyncTranslate(toType, notUTF8=false) { + log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); + const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; + + // Convert data to intermediate byteArray type + try { + switch (this.type) { + case Dish.STRING: + this.value = this.value ? Utils.strToByteArray(this.value) : []; + break; + case Dish.NUMBER: + this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; + break; + case Dish.HTML: + this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; + break; + case Dish.ARRAY_BUFFER: + // Array.from() would be nicer here, but it's slightly slower + this.value = Array.prototype.slice.call(new Uint8Array(this.value)); + break; + case Dish.BIG_NUMBER: + this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; + break; + case Dish.JSON: + this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; + break; + case Dish.FILE: + this.value = await Utils.readFile(this.value); + this.value = Array.prototype.slice.call(this.value); + break; + case Dish.LIST_FILE: + this.value = await Promise.all(this.value.map(async f => Utils.readFile(f))); + this.value = this.value.map(b => Array.prototype.slice.call(b)); + this.value = [].concat.apply([], this.value); + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); + } + + this.type = Dish.BYTE_ARRAY; + + // Convert from byteArray to toType + try { + switch (toType) { + case Dish.STRING: + case Dish.HTML: + this.value = this.value ? byteArrayToStr(this.value) : ""; + this.type = Dish.STRING; + break; + case Dish.NUMBER: + this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; + this.type = Dish.NUMBER; + break; + case Dish.ARRAY_BUFFER: + this.value = new Uint8Array(this.value).buffer; + this.type = Dish.ARRAY_BUFFER; + break; + case Dish.BIG_NUMBER: + try { + this.value = new BigNumber(byteArrayToStr(this.value)); + } catch (err) { + this.value = new BigNumber(NaN); + } + this.type = Dish.BIG_NUMBER; + break; + case Dish.JSON: + this.value = JSON.parse(byteArrayToStr(this.value)); + this.type = Dish.JSON; + break; + case Dish.FILE: + this.value = new File(this.value, "unknown"); + break; + case Dish.LIST_FILE: + this.value = [new File(this.value, "unknown")]; + this.type = Dish.LIST_FILE; + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); + } +} + + +/** + * Translates the data to the given type format. + * + * @param {number} toType - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + */ +function _translate(toType, notUTF8=false) { + log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); + const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; + + // Convert data to intermediate byteArray type + try { + switch (this.type) { + case Dish.STRING: + this.value = this.value ? Utils.strToByteArray(this.value) : []; + break; + case Dish.NUMBER: + this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; + break; + case Dish.HTML: + this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; + break; + case Dish.ARRAY_BUFFER: + // Array.from() would be nicer here, but it's slightly slower + this.value = Array.prototype.slice.call(new Uint8Array(this.value)); + break; + case Dish.BIG_NUMBER: + this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; + break; + case Dish.JSON: + this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; + break; + case Dish.FILE: + this.value = Utils.readFileSync(this.value); + this.value = Array.prototype.slice.call(this.value); + break; + case Dish.LIST_FILE: + this.value = this.value.map(f => Utils.readFileSync(f)); + this.value = this.value.map(b => Array.prototype.slice.call(b)); + this.value = [].concat.apply([], this.value); + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); + } + + this.type = Dish.BYTE_ARRAY; + + // Convert from byteArray to toType + try { + switch (toType) { + case Dish.STRING: + case Dish.HTML: + this.value = this.value ? byteArrayToStr(this.value) : ""; + this.type = Dish.STRING; + break; + case Dish.NUMBER: + this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; + this.type = Dish.NUMBER; + break; + case Dish.ARRAY_BUFFER: + this.value = new Uint8Array(this.value).buffer; + this.type = Dish.ARRAY_BUFFER; + break; + case Dish.BIG_NUMBER: + try { + this.value = new BigNumber(byteArrayToStr(this.value)); + } catch (err) { + this.value = new BigNumber(NaN); + } + this.type = Dish.BIG_NUMBER; + break; + case Dish.JSON: + this.value = JSON.parse(byteArrayToStr(this.value)); + this.type = Dish.JSON; + break; + case Dish.FILE: + this.value = new File(this.value, "unknown"); + break; + case Dish.LIST_FILE: + this.value = [new File(this.value, "unknown")]; + this.type = Dish.LIST_FILE; + break; + default: + break; + } + } catch (err) { + throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); + } +} + +/** + * Returns the value of the data in the type format specified. + * + * @param {number} type - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + * @returns {*} - The value of the output data. + */ +async function asyncGet(type, notUTF8=false) { + if (typeof type === "string") { + type = Dish.typeEnum(type); + } + if (this.type !== type) { + await this._translate(type, notUTF8); + } + return this.value; +} + +/** + * Returns the value of the data in the type format specified. + * + * @param {number} type - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + * @returns {*} - The value of the output data. + */ +function get(type, notUTF8=false) { + if (typeof type === "string") { + type = Dish.typeEnum(type); + } + if (this.type !== type) { + this._translate(type, notUTF8); + } + return this.value; +} + + /** * The data being operated on by each operation. */ @@ -24,6 +247,15 @@ class Dish { * literal input */ constructor(dishOrInput=null, type = null) { + + if (Utils.isBrowser()) { + this._translate = _asyncTranslate.bind(this); + this.get = asyncGet.bind(this); + } else { + this._translate = _translate.bind(this); + this.get = get.bind(this); + } + this.value = []; this.type = Dish.BYTE_ARRAY; @@ -74,7 +306,6 @@ class Dish { case "list": return Dish.LIST_FILE; default: - console.log(typeStr); throw new DishError("Invalid data type string. No matching enum."); } } @@ -136,118 +367,6 @@ class Dish { } - /** - * Returns the value of the data in the type format specified. - * - * @param {number} type - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - * @returns {*} - The value of the output data. - */ - async get(type, notUTF8=false) { - if (typeof type === "string") { - type = Dish.typeEnum(type); - } - if (this.type !== type) { - await this._translate(type, notUTF8); - } - return this.value; - } - - - /** - * Translates the data to the given type format. - * - * @param {number} toType - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - */ - async _translate(toType, notUTF8=false) { - log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); - const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; - - // Convert data to intermediate byteArray type - try { - switch (this.type) { - case Dish.STRING: - this.value = this.value ? Utils.strToByteArray(this.value) : []; - break; - case Dish.NUMBER: - this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; - break; - case Dish.HTML: - this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - break; - case Dish.ARRAY_BUFFER: - // Array.from() would be nicer here, but it's slightly slower - this.value = Array.prototype.slice.call(new Uint8Array(this.value)); - break; - case Dish.BIG_NUMBER: - this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; - break; - case Dish.JSON: - this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; - break; - case Dish.FILE: - this.value = await Utils.readFile(this.value); - this.value = Array.prototype.slice.call(this.value); - break; - case Dish.LIST_FILE: - this.value = await Promise.all(this.value.map(async f => Utils.readFile(f))); - this.value = this.value.map(b => Array.prototype.slice.call(b)); - this.value = [].concat.apply([], this.value); - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); - } - - this.type = Dish.BYTE_ARRAY; - - // Convert from byteArray to toType - try { - switch (toType) { - case Dish.STRING: - case Dish.HTML: - this.value = this.value ? byteArrayToStr(this.value) : ""; - this.type = Dish.STRING; - break; - case Dish.NUMBER: - this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; - this.type = Dish.NUMBER; - break; - case Dish.ARRAY_BUFFER: - this.value = new Uint8Array(this.value).buffer; - this.type = Dish.ARRAY_BUFFER; - break; - case Dish.BIG_NUMBER: - try { - this.value = new BigNumber(byteArrayToStr(this.value)); - } catch (err) { - this.value = new BigNumber(NaN); - } - this.type = Dish.BIG_NUMBER; - break; - case Dish.JSON: - this.value = JSON.parse(byteArrayToStr(this.value)); - this.type = Dish.JSON; - break; - case Dish.FILE: - this.value = new File(this.value, "unknown"); - break; - case Dish.LIST_FILE: - this.value = [new File(this.value, "unknown")]; - this.type = Dish.LIST_FILE; - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); - } - } - - /** * Validates that the value is the type that has been specified. * May have to disable parts of BYTE_ARRAY validation if it effects performance. diff --git a/src/node/NodeDish.mjs b/src/node/NodeDish.mjs index 246defbf..bddc96e0 100644 --- a/src/node/NodeDish.mjs +++ b/src/node/NodeDish.mjs @@ -6,10 +6,6 @@ import util from "util"; import Dish from "../core/Dish"; -import Utils from "../core/Utils"; -import DishError from "../core/errors/DishError"; -import BigNumber from "bignumber.js"; - /** * Subclass of Dish where `get` and `_translate` are synchronous. @@ -34,23 +30,6 @@ class NodeDish extends Dish { super(inputOrDish, type); } - /** - * Returns the value of the data in the type format specified. - * - * @param {number} type - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - * @returns {*} - The value of the output data. - */ - get(type, notUTF8=false) { - if (typeof type === "string") { - type = Dish.typeEnum(type); - } - if (this.type !== type) { - this._translate(type, notUTF8); - } - return this.value; - } - /** * alias for get * @param args see get args @@ -88,99 +67,6 @@ class NodeDish extends Dish { return this.get(Dish.typeEnum("number")); } - /** - * Translates the data to the given type format. - * - * @param {number} toType - The data type of value, see Dish enums. - * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - */ - _translate(toType, notUTF8=false) { - log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); - const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; - - // Convert data to intermediate byteArray type - try { - switch (this.type) { - case Dish.STRING: - this.value = this.value ? Utils.strToByteArray(this.value) : []; - break; - case Dish.NUMBER: - this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; - break; - case Dish.HTML: - this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - break; - case Dish.ARRAY_BUFFER: - // Array.from() would be nicer here, but it's slightly slower - this.value = Array.prototype.slice.call(new Uint8Array(this.value)); - break; - case Dish.BIG_NUMBER: - this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : []; - break; - case Dish.JSON: - this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : []; - break; - case Dish.FILE: - this.value = Utils.readFileSync(this.value); - this.value = Array.prototype.slice.call(this.value); - break; - case Dish.LIST_FILE: - this.value = this.value.map(f => Utils.readFileSync(f)); - this.value = this.value.map(b => Array.prototype.slice.call(b)); - this.value = [].concat.apply([], this.value); - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`); - } - - this.type = Dish.BYTE_ARRAY; - - // Convert from byteArray to toType - try { - switch (toType) { - case Dish.STRING: - case Dish.HTML: - this.value = this.value ? byteArrayToStr(this.value) : ""; - this.type = Dish.STRING; - break; - case Dish.NUMBER: - this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; - this.type = Dish.NUMBER; - break; - case Dish.ARRAY_BUFFER: - this.value = new Uint8Array(this.value).buffer; - this.type = Dish.ARRAY_BUFFER; - break; - case Dish.BIG_NUMBER: - try { - this.value = new BigNumber(byteArrayToStr(this.value)); - } catch (err) { - this.value = new BigNumber(NaN); - } - this.type = Dish.BIG_NUMBER; - break; - case Dish.JSON: - this.value = JSON.parse(byteArrayToStr(this.value)); - this.type = Dish.JSON; - break; - case Dish.FILE: - this.value = new File(this.value, "unknown"); - break; - case Dish.LIST_FILE: - this.value = [new File(this.value, "unknown")]; - this.type = Dish.LIST_FILE; - break; - default: - break; - } - } catch (err) { - throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`); - } - } - } export default NodeDish; diff --git a/src/node/api.mjs b/src/node/api.mjs index 35d5bf22..e44a1752 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -28,24 +28,24 @@ import ExludedOperationError from "../core/errors/ExcludedOperationError"; * @param {Object[]} originalArgs - the operation-s args list * @param {Object} newArgs - any inputted args */ -function reconcileOpArgs(operationArgs, objectStyleArgs) { +function transformArgs(opArgsList, newArgs) { - if (Array.isArray(objectStyleArgs)) { - return objectStyleArgs; + if (newArgs && Array.isArray(newArgs)) { + return newArgs; } // Filter out arg values that are list subheadings - they are surrounded in []. // See Strings op for example. - const opArgs = Object.assign([], operationArgs).map((a) => { + const opArgs = Object.assign([], opArgsList).map((a) => { if (Array.isArray(a.value)) { a.value = removeSubheadingsFromArray(a.value); } return a; }); - // transform object style arg objects to the same shape as op's args - if (objectStyleArgs) { - Object.keys(objectStyleArgs).map((key) => { + // Reconcile object style arg info to fit operation args shape. + if (newArgs) { + Object.keys(newArgs).map((key) => { const index = opArgs.findIndex((arg) => { return arg.name.toLowerCase().replace(/ /g, "") === key.toLowerCase().replace(/ /g, ""); @@ -54,22 +54,23 @@ function reconcileOpArgs(operationArgs, objectStyleArgs) { if (index > -1) { const argument = opArgs[index]; if (argument.type === "toggleString") { - if (typeof objectStyleArgs[key] === "string") { - argument.string = objectStyleArgs[key]; + if (typeof newArgs[key] === "string") { + argument.string = newArgs[key]; } else { - argument.string = objectStyleArgs[key].string; - argument.option = objectStyleArgs[key].option; + argument.string = newArgs[key].string; + argument.option = newArgs[key].option; } } else if (argument.type === "editableOption") { // takes key: "option", key: {name, val: "string"}, key: {name, val: [...]} - argument.value = typeof objectStyleArgs[key] === "string" ? objectStyleArgs[key]: objectStyleArgs[key].value; + argument.value = typeof newArgs[key] === "string" ? newArgs[key]: newArgs[key].value; } else { - argument.value = objectStyleArgs[key]; + argument.value = newArgs[key]; } } }); } + // Sanitise args return opArgs.map((arg) => { if (arg.type === "option") { // pick default option if not already chosen @@ -93,7 +94,7 @@ function reconcileOpArgs(operationArgs, objectStyleArgs) { /** - * Ensure an input is a NodeDish object. + * Ensure an input is a SyncDish object. * @param input */ function ensureIsDish(input) { @@ -109,6 +110,22 @@ function ensureIsDish(input) { } +/** + * prepareOp: transform args, make input the right type. + * Also convert any Buffers to ArrayBuffers. + * @param opInstance - instance of the operation + * @param input - operation input + * @param args - operation args + */ +function prepareOp(opInstance, input, args) { + const dish = ensureIsDish(input); + // Transform object-style args to original args array + const transformedArgs = transformArgs(opInstance.args, args); + const transformedInput = dish.get(opInstance.inputType); + return {transformedInput, transformedArgs}; +} + + /** * createArgOptions * @@ -133,6 +150,7 @@ function createArgOptions(op) { return result; } + /** * Wrap an operation to be consumed by node API. * Checks to see if run function is async or not. @@ -147,29 +165,44 @@ export function wrap(OpClass) { // Check to see if class's run function is async. const opInstance = new OpClass(); + const isAsync = opInstance.run.constructor.name === "AsyncFunction"; - /** - * Async wrapped operation run function - * @param {*} input - * @param {Object | String[]} args - either in Object or normal args array - * @returns {Promise} operation's output, on a Dish. - * @throws {OperationError} if the operation throws one. - */ - const wrapped = async (input, args=null) => { - const dish = ensureIsDish(input); + let wrapped; - // Transform object-style args to original args array - const transformedArgs = reconcileOpArgs(opInstance.args, args); - - // ensure the input is the correct type - const transformedInput = await dish.get(opInstance.inputType); - - const result = await opInstance.run(transformedInput, transformedArgs); - return new NodeDish({ - value: result, - type: opInstance.outputType - }); - }; + // If async, wrap must be async. + if (isAsync) { + /** + * Async wrapped operation run function + * @param {*} input + * @param {Object | String[]} args - either in Object or normal args array + * @returns {Promise} operation's output, on a Dish. + * @throws {OperationError} if the operation throws one. + */ + wrapped = async (input, args=null) => { + const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); + const result = await opInstance.run(transformedInput, transformedArgs); + return new NodeDish({ + value: result, + type: opInstance.outputType + }); + }; + } else { + /** + * wrapped operation run function + * @param {*} input + * @param {Object | String[]} args - either in Object or normal args array + * @returns {SyncDish} operation's output, on a Dish. + * @throws {OperationError} if the operation throws one. + */ + wrapped = (input, args=null) => { + const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); + const result = opInstance.run(transformedInput, transformedArgs); + return new NodeDish({ + value: result, + type: opInstance.outputType + }); + }; + } // used in chef.help wrapped.opName = OpClass.name; @@ -251,7 +284,7 @@ export function bake(operations){ * @param {*} input - some input for a recipe. * @param {String | Function | String[] | Function[] | [String | Function]} recipeConfig - * An operation, operation name, or an array of either. - * @returns {NodeDish} of the result + * @returns {SyncDish} of the result * @throws {TypeError} if invalid recipe given. */ return function(input, recipeConfig) {